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)