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)