Revision 96efcc33
Added by koszko over 1 year ago
content/policy_enforcing.js | ||
---|---|---|
45 | 45 |
|
46 | 46 |
#FROM common/misc.js IMPORT gen_nonce, csp_header_regex |
47 | 47 |
|
48 |
const html_ns = "http://www.w3.org/1999/xhtml"; |
|
49 |
const svg_ns = "http://www.w3.org/2000/svg"; |
|
50 |
|
|
48 | 51 |
document.content_loaded = document.readyState === "complete"; |
49 | 52 |
const wait_loaded = e => e.content_loaded ? Promise.resolve() : |
50 | 53 |
new Promise(c => e.addEventListener("DOMContentLoaded", c, {once: true})); |
... | ... | |
203 | 206 |
*/ |
204 | 207 |
if (some_attr_blocked) { |
205 | 208 |
const replacement_elem = document.createElement("a"); |
209 |
|
|
210 |
/* Prevent this node from being processed by our observer. */ |
|
211 |
replacement_elem.haketilo_trusted_node = true; |
|
212 |
|
|
206 | 213 |
element.replaceWith(replacement_elem); |
207 | 214 |
replacement_elem.replaceWith(element); |
208 | 215 |
} |
... | ... | |
221 | 228 |
element.haketilo_sanitized_onevent = true; |
222 | 229 |
|
223 | 230 |
for (const attribute_node of [...(element.attributes || [])]) { |
224 |
const attr = attribute_node.localName, attr_lo = attr.toLowerCase();;
|
|
225 |
if (!/^on/.test(attr_lo) || !(attr_lo in element.wrappedJSObject))
|
|
231 |
const attr = attribute_node.localName, attr_lo = attr.toLowerCase(); |
|
232 |
if (!/^on/.test(attr_lo) || !(attr_lo in element)) |
|
226 | 233 |
continue; |
227 | 234 |
|
228 | 235 |
/* |
... | ... | |
246 | 253 |
} |
247 | 254 |
#ENDIF |
248 | 255 |
|
249 |
function start_mo_sanitizing(doc) { |
|
250 |
if (!doc.content_loaded) { |
|
251 |
function mutation_handler(mutation) { |
|
252 |
mutation.addedNodes.forEach(sanitize_element_urls); |
|
256 |
/* |
|
257 |
* Sanitize elements on-the-fly as they appear using MutationObserver. |
|
258 |
* |
|
259 |
* Under Abrowser 97 it was observed that MutationObserver does not always work |
|
260 |
* as is should. When trying to observe nodes of an XMLDocument the behavior was |
|
261 |
* as if the "subtree" option to MutationObserver.observe() was ignored. To work |
|
262 |
* around this we avoid using the "subtree" option altogether and have the same |
|
263 |
* code work in all scenarios. |
|
264 |
*/ |
|
265 |
function MOSanitizer(root) { |
|
266 |
this.root = root; |
|
267 |
|
|
268 |
this.recursively_sanitize(root); |
|
269 |
|
|
270 |
this.mo = new MutationObserver(ms => this.handle_mutations(ms)); |
|
271 |
} |
|
272 |
|
|
273 |
MOSanitizer.prototype.observe = function() { |
|
274 |
let elem = this.root; |
|
275 |
while (elem && !elem.haketilo_trusted_node) { |
|
276 |
this.mo.observe(elem, {childList: true}); |
|
277 |
elem = elem.lastElementChild; |
|
278 |
} |
|
279 |
} |
|
280 |
|
|
281 |
MOSanitizer.prototype.handle_mutations = function(mutations) { |
|
282 |
for (const mut of mutations) { |
|
283 |
for (const new_node of mut.addedNodes) |
|
284 |
this.recursively_sanitize(new_node); |
|
285 |
} |
|
286 |
|
|
287 |
this.mo.disconnect(); |
|
288 |
this.observe(); |
|
289 |
} |
|
290 |
|
|
291 |
MOSanitizer.prototype.recursively_sanitize = function(elem) { |
|
292 |
const to_process = [elem]; |
|
293 |
|
|
294 |
while (to_process.length > 0) { |
|
295 |
const current_elem = to_process.pop(); |
|
296 |
|
|
297 |
if (current_elem.haketilo_trusted_node || |
|
298 |
current_elem.nodeType !== this.root.ELEMENT_NODE) |
|
299 |
continue; |
|
300 |
|
|
301 |
to_process.push(...current_elem.children); |
|
302 |
|
|
303 |
sanitize_element_urls(current_elem); |
|
253 | 304 |
#IF MOZILLA |
254 |
mutation.addedNodes.forEach(sanitize_element_onevent);
|
|
305 |
sanitize_element_onevent(current_elem);
|
|
255 | 306 |
#ENDIF |
256 |
} |
|
257 |
const mo = new MutationObserver(ms => ms.forEach(mutation_handler)); |
|
258 |
mo.observe(doc, {childList: true, subtree: true}); |
|
259 |
wait_loaded(doc).then(() => mo.disconnect()); |
|
260 | 307 |
} |
261 | 308 |
} |
262 | 309 |
|
310 |
MOSanitizer.prototype.start = function() { |
|
311 |
this.recursively_sanitize(this.root); |
|
312 |
this.observe(); |
|
313 |
} |
|
314 |
|
|
315 |
MOSanitizer.prototype.stop = function() { |
|
316 |
this.mo.disconnect(); |
|
317 |
} |
|
318 |
|
|
263 | 319 |
#IF MOZILLA |
264 | 320 |
/* |
265 | 321 |
* Normally, we block scripts with CSP. However, Mozilla does optimizations that |
... | ... | |
270 | 326 |
* applying this CSP to non-inline `<scripts>' in certain scenarios. |
271 | 327 |
*/ |
272 | 328 |
function prevent_script_execution(event) { |
273 |
if (!event.target.haketilo_payload) |
|
274 |
event.preventDefault(); |
|
329 |
event.preventDefault(); |
|
275 | 330 |
} |
276 | 331 |
#ENDIF |
277 | 332 |
|
... | ... | |
285 | 340 |
* javascript execution. |
286 | 341 |
*/ |
287 | 342 |
async function sanitize_document(doc, policy) { |
343 |
const root = doc.documentElement; |
|
344 |
const substitute_doc = |
|
345 |
new DOMParser().parseFromString("<!DOCTYPE html>", "text/html"); |
|
346 |
|
|
288 | 347 |
#IF MOZILLA |
289 | 348 |
/* |
290 | 349 |
* Blocking of scripts that are in the DOM from the beginning. Needed for |
291 | 350 |
* Mozilla. |
292 | 351 |
*/ |
293 | 352 |
const listener_args = ["beforescriptexecute", prevent_script_execution]; |
353 |
|
|
294 | 354 |
doc.addEventListener(...listener_args); |
355 |
substitute_doc.addEventListener(...listener_args); |
|
356 |
|
|
295 | 357 |
wait_loaded(doc).then(() => doc.removeEventListener(...listener_args)); |
296 | 358 |
|
297 | 359 |
sanitize_tree_urls(doc.documentElement); |
298 | 360 |
sanitize_tree_onevent(doc.documentElement); |
299 | 361 |
#ENDIF |
300 | 362 |
|
363 |
if (!doc.content_loaded) { |
|
364 |
const sanitizer = new MOSanitizer(doc.documentElement); |
|
365 |
sanitizer.start(); |
|
366 |
wait_loaded(doc).then(() => sanitizer.stop()); |
|
367 |
} |
|
368 |
|
|
301 | 369 |
/* |
302 | 370 |
* Ensure our CSP rules are employed from the beginning. This CSP injection |
303 | 371 |
* method is, when possible, going to be applied together with CSP rules |
... | ... | |
322 | 390 |
* Root node gets hijacked now, to be re-attached after <head> is loaded |
323 | 391 |
* and sanitized. |
324 | 392 |
*/ |
325 |
const root = doc.documentElement; |
|
326 | 393 |
root.replaceWith(temporary_html); |
394 |
#IF MOZILLA |
|
395 |
/* |
|
396 |
* To be able to handle the onbeforescriptexecute event for scripts that |
|
397 |
* appear under detached document. |
|
398 |
*/ |
|
399 |
substitute_doc.documentElement.replaceWith(root); |
|
400 |
#ENDIF |
|
327 | 401 |
|
328 | 402 |
/* |
329 | 403 |
* When we don't inject payload, we neither block document's CSP `<meta>' |
... | ... | |
336 | 410 |
.forEach(m => sanitize_meta(m, policy)); |
337 | 411 |
} |
338 | 412 |
|
339 |
sanitize_tree_urls(root); |
|
340 |
root.querySelectorAll("script").forEach(s => sanitize_script(s, policy)); |
|
413 |
const scripts = [...root.getElementsByTagNameNS(html_ns, "script"), |
|
414 |
...root.getElementsByTagNameNS(svg_ns, "svg")]; |
|
415 |
scripts.forEach(s => sanitize_script(s, policy)); |
|
341 | 416 |
temporary_html.replaceWith(root); |
342 |
root.querySelectorAll("script").forEach(s => desanitize_script(s, policy)); |
|
343 |
#IF MOZILLA |
|
344 |
sanitize_tree_onevent(root); |
|
345 |
#ENDIF |
|
346 |
|
|
347 |
start_mo_sanitizing(doc); |
|
417 |
scripts.forEach(s => desanitize_script(s, policy)); |
|
348 | 418 |
} |
349 | 419 |
|
350 | 420 |
async function _disable_service_workers() { |
Also available in: Unified diff
improve script blocking in non-HTML documents (XML)