Skip to content

Commit 8518443

Browse files
committed
Document freezing to handle SVG and other XML documents impervious to CSP on Mozilla.
1 parent 1c76173 commit 8518443

File tree

5 files changed

+160
-105
lines changed

5 files changed

+160
-105
lines changed

build.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ fi
7171

7272
if ./html5_events/html5_events.pl; then
7373
# update full event list as an array in src/content/syncFetchPolicy.js
74-
EVENTS=$(egrep '^on[a-z]+$' html5_events/html5_events_archive.txt | sed "s/^on//;s/.*/'&'/;H;1h;"'$!d;x;s/\n/, /g');
75-
perl -pi -e 's/(\blet eventTypes\s*=\s*)\[.*?\]/$1['"$EVENTS"']/' src/content/syncFetchPolicy.js
74+
EVENTS=$(grep '^on[a-z]\+$' html5_events/html5_events_archive.txt | sed "s/^on//;s/.*/'&'/;H;1h;"'$!d;x;s/\n/, /g');
75+
perl -pi -e 's/(\bconst eventTypes\s*=\s*)\[.*?\]/$1['"$EVENTS"']/' src/lib/DocumentFreezer.js
7676
fi
7777

7878
rm -rf "$BUILD" "$XPI"

src/content/DocumentCSP.js

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ class DocumentCSP {
33
constructor(document) {
44
this.document = document;
55
this.builder = new CapsCSP();
6-
this.root = document.documentElement;
76
}
87

98
apply(capabilities, embedding = CSP.isEmbedType(this.document.contentType)) {
@@ -17,10 +16,7 @@ class DocumentCSP {
1716
debug("Fallback beforexecutescript listener blocked ", e.target);
1817
}, true);
1918
}
20-
if (!(document instanceof HTMLDocument)) {
21-
// this is not HTML, hence we cannot inject a <meta> CSP
22-
return false;
23-
}
19+
2420
let csp = this.builder;
2521
let blocker = csp.buildFromCapabilities(capabilities, embedding);
2622
if (!blocker) return true;
@@ -35,10 +31,7 @@ class DocumentCSP {
3531
let root = document.documentElement;
3632

3733
let {head} = document;
38-
let parent = head ||
39-
(root instanceof HTMLElement
40-
? document.documentElement.appendChild(createHTMLElement("head"))
41-
: root);
34+
let parent = head || document.documentElement.appendChild(createHTMLElement("head"))
4235

4336
try {
4437
parent.insertBefore(meta, parent.firstElementChild);

src/content/syncFetchPolicy.js

Lines changed: 60 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -18,113 +18,92 @@
1818
if (UA.isMozilla) {
1919
// Mozilla has already parsed the <head> element, we must take extra steps...
2020

21-
let softReloading = true;
22-
let suppressedScripts = 0;
23-
debug("Early parsing: preemptively suppressing events and script execution.");
24-
2521
try {
26-
27-
if (document.body && document.body.onload) {
28-
// special treatment for body[onload], which could not be suppressed otherwise
29-
document.body._onload = document.body.getAttribute("onload");
30-
document.body.removeAttribute("onload");
31-
document.body.onload = null;
32-
}
33-
34-
// List updated by build.sh from https://hg.mozilla.org/mozilla-central/raw-file/tip/xpcom/ds/StaticAtoms.py
35-
// whenever html5_events/html5_events.pl retrieves something new.
36-
let eventTypes = ['abort', 'mozaccesskeynotfound', 'activate', 'afterprint', 'afterscriptexecute', 'animationcancel', 'animationend', 'animationiteration', 'animationstart', 'audioprocess', 'auxclick', 'beforecopy', 'beforecut', 'beforeinput', 'beforepaste', 'beforeprint', 'beforescriptexecute', 'beforeunload', 'blocked', 'blur', 'bounce', 'boundschange', 'broadcast', 'bufferedamountlow', 'cached', 'cancel', 'change', 'chargingchange', 'chargingtimechange', 'checking', 'click', 'close', 'command', 'commandupdate', 'complete', 'compositionend', 'compositionstart', 'compositionupdate', 'connect', 'connectionavailable', 'contextmenu', 'copy', 'cut', 'dblclick', 'dischargingtimechange', 'downloading', 'data', 'drag', 'dragdrop', 'dragend', 'dragenter', 'dragexit', 'dragleave', 'dragover', 'dragstart', 'drain', 'drop', 'error', 'finish', 'focus', 'focusin', 'focusout', 'fullscreenchange', 'fullscreenerror', 'get', 'hashchange', 'input', 'inputsourceschange', 'install', 'invalid', 'keydown', 'keypress', 'keyup', 'languagechange', 'levelchange', 'load', 'loading', 'loadingdone', 'loadingerror', 'popstate', 'merchantvalidation', 'message', 'messageerror', 'midimessage', 'mousedown', 'mouseenter', 'mouseleave', 'mouselongtap', 'mousemove', 'mouseout', 'mouseover', 'mouseup', 'mozfullscreenchange', 'mozfullscreenerror', 'mozkeydownonplugin', 'mozkeyuponplugin', 'mozpointerlockchange', 'mozpointerlockerror', 'mute', 'notificationclick', 'notificationclose', 'noupdate', 'obsolete', 'online', 'offline', 'open', 'orientationchange', 'overflow', 'pagehide', 'pageshow', 'paste', 'payerdetailchange', 'paymentmethodchange', 'pointerlockchange', 'pointerlockerror', 'popuphidden', 'popuphiding', 'popuppositioned', 'popupshowing', 'popupshown', 'processorerror', 'push', 'pushsubscriptionchange', 'readystatechange', 'rejectionhandled', 'remove', 'requestprogress', 'resourcetimingbufferfull', 'responseprogress', 'reset', 'resize', 'scroll', 'select', 'selectionchange', 'selectend', 'selectstart', 'set', 'shippingaddresschange', 'shippingoptionchange', 'show', 'squeeze', 'squeezeend', 'squeezestart', 'statechange', 'storage', 'submit', 'success', 'typechange', 'terminate', 'text', 'toggle', 'tonechange', 'touchstart', 'touchend', 'touchmove', 'touchcancel', 'transitioncancel', 'transitionend', 'transitionrun', 'transitionstart', 'underflow', 'unhandledrejection', 'unload', 'unmute', 'updatefound', 'updateready', 'upgradeneeded', 'versionchange', 'visibilitychange', 'voiceschanged', 'vrdisplayactivate', 'vrdisplayconnect', 'vrdisplaydeactivate', 'vrdisplaydisconnect', 'vrdisplaypresentchange', 'webkitanimationend', 'webkitanimationiteration', 'webkitanimationstart', 'webkittransitionend', 'wheel', 'zoom', 'begin', 'end', 'repeat', 'pointerdown', 'pointermove', 'pointerup', 'pointercancel', 'pointerover', 'pointerout', 'pointerenter', 'pointerleave', 'gotpointercapture', 'lostpointercapture', 'devicemotion', 'deviceorientation', 'absolutedeviceorientation', 'deviceproximity', 'mozorientationchange', 'userproximity', 'devicelight', 'devicechange', 'mozvisualresize', 'mozvisualscroll', 'mozshowdropdown', 'scrollend', 'loadend', 'loadstart', 'progress', 'suspend', 'emptied', 'stalled', 'play', 'pause', 'loadedmetadata', 'loadeddata', 'waiting', 'playing', 'canplay', 'canplaythrough', 'seeking', 'seeked', 'timeout', 'timeupdate', 'ended', 'formdata', 'ratechange', 'durationchange', 'volumechange', 'addtrack', 'controllerchange', 'cuechange', 'enter', 'exit', 'encrypted', 'waitingforkey', 'keystatuseschange', 'removetrack', 'dataavailable', 'warning', 'start', 'stop', 'photo', 'gamepadbuttondown', 'gamepadbuttonup', 'gamepadaxismove', 'gamepadconnected', 'gamepaddisconnected', 'fetch', 'audiostart', 'audioend', 'soundstart', 'soundend', 'speechstart', 'speechend', 'result', 'nomatch', 'resume', 'mark', 'boundary', 'activated', 'deactivated', 'metadatachange', 'playbackstatechange', 'positionstatechange', 'supportedkeyschange', 'sourceopen', 'sourceended', 'sourceclosed', 'updatestart', 'update', 'updateend', 'addsourcebuffer', 'removesourcebuffer', 'appinstalled', 'activestatechanged', 'adapteradded', 'adapterremoved', 'alerting', 'antennaavailablechange', 'attributechanged', 'attributereadreq', 'attributewritereq', 'beforeevicted', 'busy', 'callschanged', 'cardstatechange', 'cfstatechange', 'characteristicchanged', 'clirmodechange', 'connected', 'connecting', 'connectionstatechanged', 'currentchannelchanged', 'currentsourcechanged', 'datachange', 'dataerror', 'deleted', 'deliveryerror', 'deliverysuccess', 'devicefound', 'devicepaired', 'deviceunpaired', 'dialing', 'disabled', 'disconnect', 'disconnected', 'disconnecting', 'displaypasskeyreq', 'draggesture', 'eitbroadcasted', 'emergencycbmodechange', 'enabled', 'enterpincodereq', 'evicted', 'failed', 'frequencychange', 'groupchange', 'headphoneschange', 'held', 'hfpstatuschanged', 'hidstatuschanged', 'holding', 'iccchange', 'iccdetected', 'iccinfochange', 'iccundetected', 'incoming', 'mapfolderlistingreq', 'mapgetmessagereq', 'mapmessageslistingreq', 'mapmessageupdatereq', 'mapsendmessagereq', 'mapsetmessagestatusreq', 'mousewheel', 'mozbrowserafterkeydown', 'mozbrowserafterkeyup', 'mozbrowserbeforekeydown', 'mozbrowserbeforekeyup', 'mozinterruptbegin', 'mozinterruptend', 'moznetworkdownload', 'moznetworkupload', 'moztimechange', 'newrdsgroup', 'obexpasswordreq', 'otastatuschange', 'overflowchanged', 'paint', 'pairingaborted', 'pairingconfirmationreq', 'pairingconsentreq', 'pendingchange', 'pichange', 'pschange', 'ptychange', 'pullphonebookreq', 'pullvcardentryreq', 'pullvcardlistingreq', 'radiostatechange', 'rdsdisabled', 'rdsenabled', 'readerror', 'readsuccess', 'ready', 'received', 'reloadpage', 'remoteheld', 'remoteresumed', 'requestmediaplaystatus', 'resuming', 'retrieving', 'rtchange', 'scanningstatechanged', 'scostatuschanged', 'sending', 'sent', 'speakerforcedchange', 'statuschanged', 'stkcommand', 'stksessionend', 'storageareachanged', 'ussdreceived', 'voicechange', 'websocket'];
37-
let eventSuppressor = e => {
38-
try {
39-
debug("Event suppressor called for ", e.type, e.target); // DEV_ONLY
40-
41-
if (softReloading) {
42-
e.stopPropagation();
43-
debug(`Suppressing ${e.type} on `, e.target); // DEV_ONLY
44-
} else {
45-
debug("Stopping event suppression");
46-
for (let et of eventTypes) document.removeEventListener(et, eventSuppressor, true);
47-
}
48-
} catch (e) {
49-
error(e);
50-
}
51-
}
52-
debug("Starting event suppression");
53-
for (let et of eventTypes) document.addEventListener(et, eventSuppressor, true);
22+
DocumentFreezer.freeze();
5423

5524
ns.on("capabilities", () => {
56-
if (document.body && document.body._onload) {
57-
document.body.setAttribute("onload", document.body._onload);
58-
}
5925

6026
let {readyState} = document;
61-
debug("Readystate: %s, %suppressedScripts %s, canScript = %s", readyState, suppressedScripts, ns.canScript);
27+
debug("Readystate: %s, suppressedScripts = %s, canScript = %s", readyState, DocumentFreezer.suppressedScripts, ns.canScript);
28+
// CSP works on HTML documents only on Mozilla: we'll keep frozen elsewhere
29+
let honorsCSP = document instanceof HTMLDocument;
30+
6231
if (!ns.canScript) {
63-
for (let node of document.querySelectorAll("*")) {
64-
let evAttrs = [...node.attributes].filter(a => a.name.toLowerCase().startsWith("on"));
65-
for (let a of evAttrs) {
66-
debug("Reparsing event attribute", a, node);
67-
node.removeAttributeNode(a);
68-
node.setAttributeNodeNS(a);
69-
}
32+
if (honorsCSP) {
33+
DocumentFreezer.unfreeze();
7034
}
71-
softReloading = false;
7235
return;
7336
}
7437

75-
if (suppressedScripts === 0 && readyState === "loading") {
38+
if (DocumentFreezer.suppressedScripts === 0 && readyState === "loading") {
7639
// we don't care reloading, if no script has been suppressed
7740
// and no readyState change has been fired yet
78-
softReloading = false;
41+
DocumentFreezer.unfreeze();
7942
return;
8043
}
8144

8245
let softReload = ev => {
83-
let html = document.documentElement.outerHTML;
8446
try {
85-
debug("Soft reload", ev, html);
86-
softReloading = false;
47+
//let html = document.documentElement.outerHTML;
48+
debug("Soft reload", ev); // DEV_ONLY
8749
try {
8850
let doc = window.wrappedJSObject.document;
8951
removeEventListener("DOMContentLoaded", softReload, true);
9052
doc.open();
91-
doc.write(html);
92-
doc.close();
93-
debug("Written", html)
53+
console.debug("Opened", doc.documentElement);
54+
DocumentFreezer.unfreeze();
55+
(async () => {
56+
let html = await ((await fetch(document.URL)).text());
57+
doc.write(html);
58+
doc.close();
59+
debug("Written", html)
60+
})();
9461
} catch (e) {
9562
debug("Can't use document.write(), XML document?");
9663
try {
97-
Promise.all([...document.querySelectorAll("script")].map(s => {
98-
let clone = document.createElement("script");
99-
for (let a of s.attributes) {
100-
clone.setAttribute(a.name, a.value);
101-
}
102-
clone.textContent = s.textContent;
103-
let doneEvents = ["afterscriptexecute", "load", "error"];
104-
return new Promise(resolve => {
105-
let listener = ev => {
106-
if (ev.target !== clone) return;
107-
debug("Resolving on ", ev.type, ev.target);
108-
resolve(ev.target);
109-
for (let et of doneEvents) removeEventListener(et, listener, true);
110-
};
111-
for (let et of doneEvents) {
112-
addEventListener(et, listener, true);
113-
}
114-
s.replaceWith(clone);
115-
debug("Replaced", clone);
64+
DocumentFreezer.unfreeze();
65+
let scripts = [], deferred = [];
66+
// push deferred scripts, if any, to the end
67+
for (let s of [...document.querySelectorAll("script")]) {
68+
(s.defer && !s.text ? deferred : scripts).push(s);
69+
s.addEventListener("beforescriptexecute", e => {
70+
console.debug("Suppressing", script);
71+
e.preventDefault();
11672
});
117-
})).then(r => {
118-
debug("All scripts done", r);
119-
document.dispatchEvent(new Event("readystatechange"));
120-
document.dispatchEvent(new Event("DOMContentLoaded", {
121-
bubbles: true,
122-
cancelable: true
123-
}));
124-
if (document.readyState === "complete") {
125-
window.dispatchEvent(new Event("load"));
73+
}
74+
if (deferred.length) scripts.push(...deferred);
75+
let doneEvents = ["afterscriptexecute", "load", "error"];
76+
(async () => {
77+
for (let s of scripts) {
78+
let clone = document.createElement("script");
79+
for (let a of s.attributes) {
80+
clone.setAttribute(a.name, a.value);
12681
}
127-
});
82+
clone.text = s.text;
83+
await new Promise(resolve => {
84+
let listener = ev => {
85+
if (ev.target !== clone) return;
86+
debug("Resolving on ", ev.type, ev.target);
87+
resolve(ev.target);
88+
for (let et of doneEvents) removeEventListener(et, listener, true);
89+
};
90+
for (let et of doneEvents) {
91+
addEventListener(et, listener, true);
92+
}
93+
s.replaceWith(clone);
94+
debug("Replaced", clone);
95+
});
96+
}
97+
debug("ALl scripts done, firing completion events.");
98+
document.dispatchEvent(new Event("readystatechange"));
99+
document.dispatchEvent(new Event("DOMContentLoaded", {
100+
bubbles: true,
101+
cancelable: true
102+
}));
103+
if (document.readyState === "complete") {
104+
window.dispatchEvent(new Event("load"));
105+
}
106+
})();
128107
} catch (e) {
129108
error(e);
130109
}
@@ -145,19 +124,6 @@
145124
} catch (e) {
146125
error(e);
147126
}
148-
149-
let scriptSuppressor = e => {
150-
if (!e.isTrusted) return;
151-
debug(e.type, e.target, softReloading); // DEV_ONLY
152-
if (softReloading) {
153-
e.preventDefault();
154-
++suppressedScripts;
155-
debug(`Suppressed early script #${suppressedScripts}`, e.target);
156-
} else {
157-
removeEventListener(e.type, scriptSuppressor);
158-
}
159-
};
160-
addEventListener("beforescriptexecute", scriptSuppressor, true);
161127
}
162128

163129
let setup = policy => {

0 commit comments

Comments
 (0)