Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Delta Between Two Patch Sets: include.preload.js

Issue 29347034: Issue 1727 - Prevent circumvention via WebSocket (Closed)
Left Patch Set: Use Proxy, intercept events Created July 12, 2016, 11:19 a.m.
Right Patch Set: Don't hardcode connection state values Created Aug. 10, 2016, 4:25 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
Left: Side by side diff | Download
Right: Side by side diff | Download
« no previous file with change/comment | « no previous file | lib/requestBlocker.js » ('j') | no next file with change/comment »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
LEFTRIGHT
1 /* 1 /*
2 * This file is part of Adblock Plus <https://adblockplus.org/>, 2 * This file is part of Adblock Plus <https://adblockplus.org/>,
3 * Copyright (C) 2006-2016 Eyeo GmbH 3 * Copyright (C) 2006-2016 Eyeo GmbH
4 * 4 *
5 * Adblock Plus is free software: you can redistribute it and/or modify 5 * Adblock Plus is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License version 3 as 6 * it under the terms of the GNU General Public License version 3 as
7 * published by the Free Software Foundation. 7 * published by the Free Software Foundation.
8 * 8 *
9 * Adblock Plus is distributed in the hope that it will be useful, 9 * Adblock Plus is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
(...skipping 332 matching lines...) Expand 10 before | Expand all | Expand 10 after
343 var observer = new MutationObserver(function() 343 var observer = new MutationObserver(function()
344 { 344 {
345 if (style.parentNode != parentNode) 345 if (style.parentNode != parentNode)
346 parentNode.appendChild(style); 346 parentNode.appendChild(style);
347 }); 347 });
348 348
349 observer.observe(parentNode, {childList: true}); 349 observer.observe(parentNode, {childList: true});
350 return observer; 350 return observer;
351 } 351 }
352 352
353 function injectJS(f) 353 function runInPage(fn, arg)
354 { 354 {
355 var args = JSON.stringify(Array.prototype.slice.call(arguments, 1));
356 args = args.substring(1, args.length - 1);
357 var codeString = "(" + f.toString() + ")(" + args + ");";
358
359 var script = document.createElement("script"); 355 var script = document.createElement("script");
356 script.type = "application/javascript";
360 script.async = false; 357 script.async = false;
361 script.textContent = codeString; 358 script.textContent = "(" + fn + ")(" + JSON.stringify(arg) + ");";
362 document.documentElement.appendChild(script); 359 document.documentElement.appendChild(script);
363 document.documentElement.removeChild(script); 360 document.documentElement.removeChild(script);
364 } 361 }
365 362
366 function protectStyleSheet(document, style) 363 function protectStyleSheet(document, style)
367 { 364 {
368 style.id = id; 365 style.id = id;
369 366
370 var protector = function(id) 367 runInPage(function(id)
371 { 368 {
372 var style = document.getElementById(id) || 369 var style = document.getElementById(id) ||
373 document.documentElement.shadowRoot.getElementById(id); 370 document.documentElement.shadowRoot.getElementById(id);
374 style.removeAttribute("id"); 371 style.removeAttribute("id");
375 372
376 var i;
377 var disableables = [style, style.sheet]; 373 var disableables = [style, style.sheet];
378 for (i = 0; i < disableables.length; i += 1) 374 for (var i = 0; i < disableables.length; i++)
379 Object.defineProperty(disableables[i], "disabled", 375 Object.defineProperty(disableables[i], "disabled",
380 {value: false, enumerable: true}); 376 {value: false, enumerable: true});
381 377
382 var methods = ["deleteRule", "removeRule"]; 378 ["deleteRule", "removeRule"].forEach(function(method)
383 for (i = 0; i < methods.length; i += 1) 379 {
384 { 380 var original = CSSStyleSheet.prototype[method];
385 if (methods[i] in CSSStyleSheet.prototype) 381 CSSStyleSheet.prototype[method] = function(index)
386 { 382 {
387 (function(method) 383 if (this != style.sheet)
388 { 384 original.call(this, index);
389 var original = CSSStyleSheet.prototype[method]; 385 };
390 CSSStyleSheet.prototype[method] = function(index) 386 });
391 { 387 }, id);
392 if (this != style.sheet)
393 original.call(this, index);
394 };
395 }(methods[i]));
396 }
397 }
398 };
399
400 injectJS(protector, id);
401 } 388 }
402 389
403 // Neither Chrome[1] nor Safari allow us to intercept WebSockets, and therefore 390 // Neither Chrome[1] nor Safari allow us to intercept WebSockets, and therefore
404 // some ad networks are misusing them as a way to serve adverts and circumvent 391 // some ad networks are misusing them as a way to serve adverts and circumvent
405 // us. As a workaround we wrap WebSocket, preventing blocked WebSocket 392 // us. As a workaround we wrap WebSocket, preventing blocked WebSocket
406 // connections from being opened. We go to some lengths to avoid breaking code 393 // connections from being opened.
407 // using WebSockets, circumvention and as far as possible detection.
408 // [1] - https://bugs.chromium.org/p/chromium/issues/detail?id=129353 394 // [1] - https://bugs.chromium.org/p/chromium/issues/detail?id=129353
409 function wrapWebSocket() 395 function wrapWebSocket()
410 { 396 {
411 if (typeof WebSocket == "undefined" || 397 if (typeof WebSocket == "undefined")
412 typeof WeakMap == "undefined" ||
413 typeof Proxy == "undefined")
414 return; 398 return;
415 399
416 var eventName = "abpws-" + id; 400 var eventName = "abpws-" + id;
417 401
418 document.addEventListener(eventName, function(event) 402 document.addEventListener(eventName, function(event)
419 { 403 {
420 ext.backgroundPage.sendMessage({ 404 ext.backgroundPage.sendMessage({
421 type: "websocket-request", 405 type: "websocket-request",
422 url: event.detail.url 406 url: event.detail.url
423 }, function (block) 407 }, function (block)
424 { 408 {
425 document.dispatchEvent( 409 document.dispatchEvent(
426 new CustomEvent(eventName + "-" + event.detail.url, {detail: block}) 410 new CustomEvent(eventName + "-" + event.detail.url, {detail: block})
427 ); 411 );
428 }); 412 });
429 }); 413 });
430 414
431 function wrapper(eventName) 415 runInPage(function(eventName)
432 { 416 {
417 // As far as possible we must track everything we use that could be
418 // sabotaged by the website later in order to circumvent us.
433 var RealWebSocket = WebSocket; 419 var RealWebSocket = WebSocket;
434 420 var closeWebSocket = Function.prototype.call.bind(RealWebSocket.prototype.cl ose);
435 function checkRequest(url, protocols, callback) 421 var addEventListener = document.addEventListener.bind(document);
422 var removeEventListener = document.removeEventListener.bind(document);
423 var dispatchEvent = document.dispatchEvent.bind(document);
424 var CustomEvent = window.CustomEvent;
425
426 function checkRequest(url, callback)
436 { 427 {
437 var incomingEventName = eventName + "-" + url; 428 var incomingEventName = eventName + "-" + url;
438 function listener(event) 429 function listener(event)
439 { 430 {
440 callback(event.detail); 431 callback(event.detail);
441 document.removeEventListener(incomingEventName, listener); 432 removeEventListener(incomingEventName, listener);
442 } 433 }
443 document.addEventListener(incomingEventName, listener); 434 addEventListener(incomingEventName, listener);
444 435
445 document.dispatchEvent(new CustomEvent(eventName, { 436 dispatchEvent(new CustomEvent(eventName, {
446 detail: {url: url, protocols: protocols} 437 detail: {url: url}
447 })); 438 }));
448 } 439 }
449 440
450 // We need to store state for our wrapped WebSocket instances, that webpages 441 WebSocket = function WrappedWebSocket(url, protocols)
451 // can't access. We use a WeakMap to avoid leaking memory in the case that 442 {
452 // all other references to a WebSocket instance have been deleted. 443 // Throw correct exceptions if the constructor is used improperly.
453 var instanceStorage = new WeakMap(); 444 if (!(this instanceof WrappedWebSocket)) return RealWebSocket();
454 445 if (arguments.length < 1) return new RealWebSocket();
455 var eventNames = ["close", "open", "message", "error"]; 446
456 var eventAttrNames = ["onclose", "onopen", "onmessage", "onerror"]; 447 var websocket = new RealWebSocket(url, protocols);
457 448
458 function addRemoveEventListener(storage, key, type, listener) 449 checkRequest(websocket.url, function(blocked)
kzar 2016/07/12 11:34:32 Should we care about the useCapture parameter for
459 {
460 if (typeof listener == "object")
461 listener = listener.handleEvent;
462
463 if (!(eventNames.indexOf(type) > -1 && typeof listener == "function"))
464 return;
465
466 var listeners = storage.listeners[type];
467 var listenerIndex = listeners.indexOf(listener);
468
469 if (key == "addEventListener")
470 {
471 if (listenerIndex == -1)
472 listeners.push(listener);
473 }
474 else if (listenerIndex > -1)
475 listeners.splice(listenerIndex, 1);
476 }
477
478 // We check if a WebSocket should be blocked before actually creating it. As
479 // this is done asynchonously we must queue up any actions (method calls and
480 // assignments) that happen in the mean time.
481 // Once we have a result, we create the WebSocket (if allowed) and perform
482 // the queued actions.
483 function processQueue(storage)
kzar 2016/07/12 11:34:33 Queuing up assignments and method calls seems rath
484 {
485 for (var i = 0; i < storage.queue.length; i += 1)
486 {
487 var action = storage.queue[i][0];
488 var key = storage.queue[i][1];
489 var value = storage.queue[i][2];
490
491 if (action == "set")
492 storage.websocket[key] = value;
493 else if (action == "call")
494 storage.websocket[key].apply(storage.websocket, value);
495 }
496 }
497
498 var defaults = {
499 readyState: RealWebSocket.CONNECTING,
500 bufferedAmount: 0,
501 extensions: "",
502 binaryType: "blob"
503 };
504
505 // We cannot dispatch WebSocket events directly to their listeners since
506 // event.target would give a way back to the original WebSocket constructor.
507 // Instead we must listen for events ourselves and pass them on, taking care
508 // to spoof the event target and isTrusted flag.
509 function wrappedEventListener(name, me)
510 {
511 var storage = instanceStorage.get(me);
512 return function(event)
513 {
514 var eventProxy = new Proxy(event, {
515 get: function(target, key)
516 {
517 if (key == "isTrusted" && "isTrusted" in target)
518 return true;
519 if (key == "target" || key == "srcElement" || key == "currentTarget" )
520 return me;
521 return target[key];
522 }
523 });
524
525 var listeners = storage.listeners[name];
526 for (var i = 0; i < listeners.length; i += 1)
527 listeners[i].call(me, eventProxy);
528 var listener = storage.listeners["on" + name];
529 if (typeof listener == "function")
530 listener.call(me, eventProxy);
531 };
532 }
533
534 WebSocket = function(url, protocols)
kzar 2016/07/12 11:34:33 So far I don't intercept WebSocket.toString() so i
lainverse 2016/07/12 12:22:20 If we going all the way to avoid detection then we
535 {
536 var me = this;
537 var storage = {
538 url: url,
539 protocol: protocols || "",
540 queue: [],
541 websocket: null,
542 blocked: false,
543 listeners: {
544 onclose: null,
545 onopen: null,
546 onmessage: null,
547 onerror: null,
548 close: [],
549 open: [],
550 message: [],
551 error: []
552 }
553 };
554 instanceStorage.set(me, storage);
555
556 checkRequest(url, protocols, function(blocked)
557 { 450 {
558 if (blocked) 451 if (blocked)
559 { 452 closeWebSocket(websocket);
560 storage.blocked = true;
561 wrappedEventListener("error", me)(new Error("error"));
562 }
563 else
564 {
565 storage.websocket = new RealWebSocket(url, protocols);
566 for (var i = 0; i < eventNames.length; i += 1)
kzar 2016/07/12 11:34:32 If a website creates a WebSocket (which manages to
567 storage.websocket.addEventListener(
568 eventNames[i],
569 wrappedEventListener(eventNames[i], me)
570 );
571 processQueue(storage);
572 }
573 delete storage.queue;
574 }); 453 });
575 }; 454
455 return websocket;
456 }.bind();
457
576 Object.defineProperties(WebSocket, { 458 Object.defineProperties(WebSocket, {
577 CONNECTING: {value: 0, enumerable: true}, 459 CONNECTING: {value: RealWebSocket.CONNECTING, enumerable: true},
578 OPEN: {value: 1, enumerable: true}, 460 OPEN: {value: RealWebSocket.OPEN, enumerable: true},
579 CLOSING: {value: 2, enumerable: true}, 461 CLOSING: {value: RealWebSocket.CLOSING, enumerable: true},
580 CLOSED: {value: 3, enumerable: true} 462 CLOSED: {value: RealWebSocket.CLOSED, enumerable: true},
463 prototype: {value: RealWebSocket.prototype}
581 }); 464 });
582 WebSocket.prototype = new Proxy(RealWebSocket.prototype,{ 465
583 set: function(target, key, value, me) 466 RealWebSocket.prototype.constructor = WebSocket;
584 { 467 }, eventName);
585 var storage = instanceStorage.get(me);
586
587 if (!storage)
588 target[key] = value;
589 else if (eventAttrNames.indexOf(key) > -1)
590 storage.listeners[key] = value;
591 else if (storage.websocket)
592 storage.websocket[key] = value;
593 else if (!storage.blocked)
594 storage.queue.push(["set", key, value]);
595
596 return true;
597 },
598 get: function(target, key, me)
599 {
600 if (key == "__proto__" && me != WebSocket.prototype)
601 return WebSocket.prototype;
602
603 if (key == "constructor")
604 return WebSocket;
605
606 var storage = instanceStorage.get(me);
607 if (!storage)
608 return target[key];
609
610 if (key == "addEventListener" || key == "removeEventListener")
611 return function()
612 {
613 if (arguments.length > 1)
614 addRemoveEventListener(storage, key, arguments[0], arguments[1]);
615 };
616
617 if (eventAttrNames.indexOf(key) > -1)
618 return storage.listeners[key];
619
620 var desc = Object.getOwnPropertyDescriptor(target, key);
621 if (desc && typeof desc.value == "function")
622 return function()
623 {
624 if (storage.websocket)
625 storage.websocket[key].apply(storage.websocket, arguments);
626 else if (!storage.blocked)
627 storage.queue.push(["call", key, arguments]);
628 };
629
630 if (storage.websocket)
631 return storage.websocket[key];
632 if (key == "url" || key == "protocol")
633 return storage[key];
634 if (storage.blocked && key == "readyState")
635 return WebSocket.CLOSED;
636 if (key in defaults)
637 return defaults[key];
638 return undefined;
639 }
640 });
641 }
642
643 injectJS(wrapper, eventName);
644 } 468 }
645 469
646 function init(document) 470 function init(document)
647 { 471 {
648 var shadow = null; 472 var shadow = null;
649 var style = null; 473 var style = null;
650 var observer = null; 474 var observer = null;
651 var tracer = null; 475 var tracer = null;
652 476
653 wrapWebSocket(); 477 wrapWebSocket();
(...skipping 161 matching lines...) Expand 10 before | Expand all | Expand 10 after
815 }, true); 639 }, true);
816 640
817 return updateStylesheet; 641 return updateStylesheet;
818 } 642 }
819 643
820 if (document instanceof HTMLDocument) 644 if (document instanceof HTMLDocument)
821 { 645 {
822 checkSitekey(); 646 checkSitekey();
823 window.updateStylesheet = init(document); 647 window.updateStylesheet = init(document);
824 } 648 }
LEFTRIGHT

Powered by Google App Engine
This is Rietveld