Project

General

Profile

« Previous | Next » 

Revision 96efcc33

Added by koszko over 1 year ago

improve script blocking in non-HTML documents (XML)

View differences:

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