SlideShare a Scribd company logo
@vazac
How to identify bad third-
parties on your page
Charles Vazac
@vazac
@vazac
@vazac
@vazac
@vazac
https://www.w3.org/webperf/
@vazac
● 36f11e49.akstat.io
● 4448044.fls.doubleclick.net
● 642-skn-449.mktoresp.com
● 79792546.va.cobrowse.liveperson.net
● accdn.lpsnmedia.net
● akamai.com
● analytics.twitter.com
● api.company-target.com
● bat.bing.com
● bizographics.com
● c.go-mpulse.net
● cdn.dashjs.org
● cdnssl.clicktale.net
● cm.g.doubleclick.net
● d.company-target.com
● d26x5ounzdjojj.cloudfront.net
● dc.ads.linkedin.com
● drvizd1lyevz4.cloudfront.net
● ds-aksb-a.akamaihd.net
● e02.optimix.asia
● google-analytics.com
● google.com
● googleads.g.doubleclick.net
● googleadservices.com
● googletagmanager.com
● graph.facebook.com
● insight.adsrvr.org
● j02.optimix.asia
● j02.optimix.asia
● linkedin.com
● lpcdn.lpsnmedia.net
● lptag.liveperson.net
● m.addthis.com
● m.addthisedge.com
● match.adsrvr.org
● match.prod.bidr.io
● munchkin.marketo.net
● pixel.quantserve.com
● pubads.g.doubleclick.net
● px.ads.linkedin.com
● s.ml-attr.com
● s7.addthis.com
● scripts.demandbase.com
● secure.adnxs.com
● secure.quantserve.com
● sjs.bizographics.com
● snap.licdn.com
● sp.adbrn.com
● static.ads-twitter.com
● stats.g.doubleclick.net
● sync.adap.tv
● sync.adaptv.advertising.com
● t.co
● tags.w55c.net
● unpkg.com
● us-east-1.dc.ads.linkedin.com
@vazac
https://twitter.com/zachleat/status/363053721258700800
@vazac http://www.tooled-up.com/
@vazac
https://twitter.com/Tobi042/status/648946436445446145
@vazac
Browser native overrides
● forge
● guerrilla patch
● hijack
● hook
● instrument
● intercept
● mock
● monkey-patch
● override
● overwrite
● polyfill, prollyfill, notifill, ponyfill
● proxy
● shadow
● sham
● shim
● shiv
● spoof
● stub
● swizzle
● trap
● wrap
@vazac
https://twitter.com/aweary/status/913519003426988032
@vazac
What is a third-party
“Code in your site that is managed by someone else” - Guy Podjarny (@guypod)
@vazac
Why do we sometimes need third-parties?
● JS / CSS Frameworks
● Analytics (yay! boomerang!)
● Ads :(
● Customer engagement (intercom.io)
● Comments widgets
● Social Media
● A/B Testing
● Web Fonts
● Accessibility Tools
● .... and yes, jQuery! (30KB minified and gzipped)
@vazac
What can go wrong?
● Page breakage
○ Script errors
○ Bad polyfills
○ Namespace collisions
○ SPOF
https://twitter.com/patmeenan/status/938071367525781506
@vazac
What can go wrong?
● Page breakage
● Performance Issues
○ Slow page load
○ Janky user experience
○ Redirect chains
○ Battery drain
○ Memory Leaks
@vazac
What can go wrong?
● Page breakage
● Performance Issues
● Privacy & security
@vazac
https://twitter.com/calrgn/status/913525736836931585
@vazac
https://twitter.com/nytimes/status/3958547840
@vazac
What can go wrong?
● Page breakage
● Performance Issues
● Privacy & security
● Makes debugging *your* stuff really hard!
https://twitter.com/slicknet/status/997248009778876416
@vazac
https://www.tumblr.com/search/i've%20made%20a%20huge%20mistake%20gif
@vazac
Example #1
“Lazy loader” library
// lazy loading package
window.requestAnimationFrame = window.requestAnimationFrame || setTimeout
@vazac
Example #1
“Lazy loader” library
// lazy loading package
window.requestAnimationFrame = window.requestAnimationFrame || setTimeout
// in-page feature detection
if (typeof requestAnimationFrame === 'function') {
requestAnimationFrame(function() {
/* fancy animation codez */
})
}
@vazac
Example #1
“Lazy loader” library
window.requestAnimationFrame = window.requestAnimationFrame || setTimeout
// in-page feature detection
if (typeof requestAnimationFrame === 'function') {
var requestId = requestAnimationFrame(function() {
/* fancy animation codez */
})
// sometime later
cancelAnimationFrame(requestId)
}
@vazac
Example #1
“Lazy loader” library
window.requestAnimationFrame = window.requestAnimationFrame || setTimeout
// in-page feature detection
if (typeof requestAnimationFrame === 'function') {
var requestId = requestAnimationFrame(function() {
/* fancy animation codez */
})
// sometime later
cancelAnimationFrame(requestId)
}
Uncaught ReferenceError: cancelAnimationFrame is not defined
@vazac
Example #2
“Accelerated JavaScript animation” framework
@vazac
Example #2
var performance = (function() {
var perf = window.performance || {};
if (!Object.prototype.hasOwnProperty.call(perf, 'now')) {
var nowOffset = perf.timing && perf.timing.domComplete ? perf.timing.domComplete : (new
Date()).getTime();
perf.now = function() {
return (new Date()).getTime() - nowOffset;
};
}
return perf;
})();
@vazac
Example #2
var performance = (function() {
var perf = window.performance || {};
if (!Object.prototype.hasOwnProperty.call(perf, 'now')) {
var nowOffset = perf.timing && perf.timing.domComplete ? perf.timing.domComplete : (new
Date()).getTime();
perf.now = function() {
return (new Date()).getTime() - nowOffset;
};
}
return perf;
})();
Should be perf.timing.navigationStart
@vazac
Example #3
“Front End Optimization”
@vazac
Example #3
try {
Object.defineProperty(document, 'readyState', {
get: function() {
return somethingElse
}
})
} catch (e) {}
@vazac
Example #3
window.addEventListener = (function(addEventListener) {
return function() {
if (['load', 'readyStateChanged'].indexOf(arguments[0]) !== -1) {
// collect these and execute them later
return
}
return addEventListener.apply(this, arguments)
}
})(window.addEventListener)
@vazac
Example #3
window.onload = function() {
// but they left this one alone
}
@vazac
document.hasOwnProperty('readyState')
Example #3 - detection
@vazac
document.hasOwnProperty('readyState')
// false
Example #3 - detection
@vazac
document.hasOwnProperty('readyState')
// false
Example #3 - detection
Object.defineProperty(document, 'readyState', {
/* ... */
})
@vazac
Example #3 - detection
document.hasOwnProperty('readyState')
// false
document.hasOwnProperty('readyState')
Object.defineProperty(document, 'readyState', {
/* ... */
})
@vazac
Example #3 - detection
document.hasOwnProperty('readyState')
// false
document.hasOwnProperty('readyState')
// true
Object.defineProperty(document, 'readyState', {
/* ... */
})
@vazac
Example #4
“RUM analytics” script (**cough cough** boomerang)
@vazac
Example #4
var addEventListener = EventTarget.prototype.addEventListener
EventTarget.prototype.addEventListener = function(type, callback) {
addEventListener(type, function() {
/* intervene here */
callback()
})
}
@vazac
Example #4
var addEventListener = EventTarget.prototype.addEventListener
EventTarget.prototype.addEventListener = function(type, callback) {
addEventListener(type, function() {
/* intervene here */
callback()
})
}
document.body.addEventListener('click', handler)
@vazac
Example #4
var addEventListener = EventTarget.prototype.addEventListener
EventTarget.prototype.addEventListener = function(type, callback) {
addEventListener(type, function() {
/* intervene here */
callback()
})
}
document.body.addEventListener('click', handler)
document.body.removeEventListener('click', handler)
@vazac
Example #4
document.body.addEventListener('click', handler)
document.body.removeEventListener('click', handler)
// ^^^ does NOT work because `handler` was never attached!
var addEventListener = EventTarget.prototype.addEventListener
EventTarget.prototype.addEventListener = function(type, callback) {
addEventListener(type, function() {
/* intervene here */
callback()
})
}
@vazac
Example #4
document.body.addEventListener('click', handler)
document.body.removeEventListener('click', handler)
// ^^^ does NOT work because `handler` was never attached!
var addEventListener = EventTarget.prototype.addEventListener
EventTarget.prototype.addEventListener = function(type, callback) {
addEventListener(type, function() {
/* intervene here */
callback()
})
}
@vazac
https://tenor.com/view/bananastand-burn-it-down-banana-gif-8351164
@vazac
Example #1
https://twitter.com/kinsi55/status/888439631318061057
@vazac
@vazac
Example #2
setInterval(function() {
debugger
}, 10)
https://stackoverflow.com/questions/7798748/find-out-whether-chrome-console-is-open/30638226#30638226
@vazac
Circumvention
Local Overrides (Chrome 65+)
@vazac
@vazac
Example #3
“Too many secrets”
● Ads
● Ad blockers
● Ad blocker blockers
● Ad infinitum
@vazac
DevTools detection #1
setInterval(function () {
const threshold = 160
const {outerWidth, innerWidth, outerHeight, innerHeight} = window
window.devToolsOpen = outerWidth - innerWidth > threshold ||
outerHeight - innerHeight > threshold
}, 500);
@vazac
function isDevToolsOpen() {
const element = new Image()
let open = false
Object.defineProperty(element, 'id', {
get: function () {
open = true // this is the "trap"
}
})
console.log(element)
return open
}
DevTools detection #2
https://stackoverflow.com/questions/7798748/find-out-whether-chrome-console-is-open/30638226#30638226
@vazac
Circumvention
@vazac
Circumvention
// using tampermonkey
window.open()
@vazac
Circumvention
// from devtools of the blank page
var iframes = opener.document.getElementsByTagName('iframe')
// using tampermonkey
window.open()
@vazac
Example #4
“Super phishing”
@vazac
@vazac
http://78.media.tumblr.com/tumblr_maeb36MSBm1qgb321o1_500.gif
@vazac
Unbound listeners
var car = new Car()
car.start()
@vazac
Unbound listeners
var car = new Car()
car.start()
function Car() { /* ... */}
Car.prototype.start = function() {
// what is `this`?
}
@vazac
Unbound listeners
var car = new Car()
car.start()
function Car() { /* ... */}
Car.prototype.start = function() {
// `this` will be `car`
}
@vazac
Unbound listeners
var car = new Car()
var start = car.start
start()
@vazac
Unbound listeners
var car = new Car()
var start = car.start
start()
function Car() { /* ... */}
Car.prototype.start = function() {
// what is `this`?
}
@vazac
Unbound listeners
var car = new Car()
var start = car.start
start()
function Car() { /* ... */}
Car.prototype.start = function() {
// `this` will be `window`
}
@vazac
Unbound listeners
window.addEventListener('load', function loadHandlerBound(e) {
// what is `this`?
})
@vazac
Unbound listeners
window.addEventListener('load', function loadHandlerBound(e) {
// `this` is `window`
})
@vazac
Unbound listeners
window.addEventListener('load', function loadHandlerBound(e) {
// `this` is `window`
})
var method = window.addEventListener
method('load', function loadHandlerUnbound(e) {
// what is `this`?
})
@vazac
Unbound listeners
window.addEventListener('load', function loadHandlerBound(e) {
// `this` is `window`
})
var method = window.addEventListener
method('load', function loadHandlerUnbound(e) {
// `this` is `window`
})
@vazac
Unbound listeners
var _addEventListener = top.EventTarget.prototype.addEventListener
top.EventTarget.prototype.addEventListener = function intervene() {
// intervene here
return _addEventListener.apply(this, arguments)
}
@vazac
Unbound listeners
window.addEventListener('load', function loadHandlerBound(e) {
// what is `this`?
})
@vazac
Unbound listeners
window.addEventListener('load', function loadHandlerBound(e) {
// `this` is `window`
})
@vazac
Unbound listeners
window.addEventListener('load', function loadHandlerBound(e) {
// `this` is `window`
})
var method = window.addEventListener
method('load', function loadHandlerUnbound(e) {
// what is `this`?
})
@vazac
Unbound listeners
var _addEventListener = top.EventTarget.prototype.addEventListener
top.EventTarget.prototype.addEventListener = function intervene() {
// intervene here
return _addEventListener.apply(this, arguments)
}
@vazac
Unbound listeners
var _addEventListener = top.EventTarget.prototype.addEventListener
top.EventTarget.prototype.addEventListener = function intervene() {
// intervene here
return _addEventListener.apply(this, arguments)
}
var method = window.addEventListener
method('load', function loadHandlerUnbound(e) {
// what is `this`?
})
@vazac
Unbound listeners
var _addEventListener = top.EventTarget.prototype.addEventListener
top.EventTarget.prototype.addEventListener = function intervene() {
// intervene here
return _addEventListener.apply(this, arguments)
}
var method = window.addEventListener
method('load', function loadHandlerUnbound(e) {
// what is `this`?
})
@vazac
Unbound listeners
var _addEventListener = top.EventTarget.prototype.addEventListener
top.EventTarget.prototype.addEventListener = function intervene() {
// intervene here
return _addEventListener.apply(this, arguments)
}
var method = window.addEventListener
method('load', function loadHandlerUnbound(e) {
// what is `this`?
})
@vazac
Unbound listeners
var _addEventListener = top.EventTarget.prototype.addEventListener
top.EventTarget.prototype.addEventListener = function intervene() {
// intervene here
return _addEventListener.apply(this, arguments)
}
var method = window.addEventListener
method('load', function loadHandlerUnbound(e) {
// `this` is the IFRAME
})
@vazac
Unbound listeners
var method = window.addEventListener
window.addEventListener =
function unboundAddEventListener(eventName, handler) {
method(eventName, handler)
}
@vazac
https://daicynotdaisy.wordpress.com/2010/10/page/2/
@vazac
Table stakes
● HTTPS only
● Don’t produce scripts errors
● No sync XHR
● Don’t pollute the global namespace
● Don’t ship down ALL of jQuery
● Handle “both sides”
● Test on old browsers
● Don’t slow down unload
● Don’t attach too many handlers
● Polyfills
○ Ensure spec compliance
○ Don’t let them rot!
@vazac
Be a good citizen
raven.js (sentry.io)
@vazac
@vazac
EventTarget.prototype.addEventListener = (function(addEventListener) {
return function(type, callback) {
addEventListener(type, function() {
/* ... */
callback()
/* ... */
})
}
})(EventTarget.prototype.addEventListener)
@vazac
EventTarget.prototype.addEventListener = (function(addEventListener) {
return function(type, callback) {
addEventListener(type, function() {
/* ... */
callback()
/* ... */
})
}
})(EventTarget.prototype.addEventListener)
document.body.addEventListener('click', function(e) {
console.info('CLICKED!') // CLICKED!
})
@vazac
EventTarget.prototype.addEventListener = (function(addEventListener) {
return function(type, callback) {
addEventListener(type, function() {
/* ... */
callback()
/* ... */
})
}
})(EventTarget.prototype.addEventListener)
document.body.addEventListener('click', function(e) {
console.info('CLICKED!') // CLICKED!
})
@vazac
EventTarget.prototype.addEventListener = (function(addEventListener) {
return function() {
var args = Array.prototype.slice.call(arguments)
var callback = args[1]
args[1] = function() {
/* ... */
callback()
/* ... */
}
addEventListener.apply(undefined, args)
}
})(EventTarget.prototype.addEventListener)
@vazac
EventTarget.prototype.addEventListener = (function(addEventListener) {
return function() {
var args = Array.prototype.slice.call(arguments)
var callback = args[1]
args[1] = function() {
/* ... */
callback()
/* ... */
}
addEventListener.apply(undefined, args)
}
})(EventTarget.prototype.addEventListener)
document.body.addEventListener('click', function(e) {
console.info('CLICKED!') // CLICKED!
console.info(e.timeStamp)
})
@vazac
EventTarget.prototype.addEventListener = (function(addEventListener) {
return function() {
var args = Array.prototype.slice.call(arguments)
var callback = args[1]
args[1] = function() {
/* ... */
callback()
/* ... */
}
addEventListener.apply(undefined, args)
}
})(EventTarget.prototype.addEventListener)
document.body.addEventListener('click', function(e) {
console.info('CLICKED!') // CLICKED!
console.info(e.timeStamp)
})
Uncaught TypeError: Cannot read property 'timeStamp' of undefined
@vazac
EventTarget.prototype.addEventListener = (function(addEventListener) {
return function() {
var args = Array.prototype.slice.call(arguments)
var callback = args[1]
args[1] = function() {
/* ... */
callback.apply(undefined, arguments)
/* ... */
}
addEventListener.apply(undefined, args)
}
})(EventTarget.prototype.addEventListener)
@vazac
EventTarget.prototype.addEventListener = (function(addEventListener) {
return function() {
var args = Array.prototype.slice.call(arguments)
var callback = args[1]
args[1] = function() {
/* ... */
callback.apply(undefined, arguments)
/* ... */
}
addEventListener.apply(undefined, args)
}
})(EventTarget.prototype.addEventListener)
document.body.addEventListener('click', function(e) {
console.info('CLICKED!') // CLICKED!
console.info(e.timeStamp)
console.info(this.tagName)
})
@vazac
EventTarget.prototype.addEventListener = (function(addEventListener) {
return function() {
var args = Array.prototype.slice.call(arguments)
var callback = args[1]
args[1] = function() {
/* ... */
callback.apply(undefined, arguments)
/* ... */
}
addEventListener.apply(undefined, args)
}
})(EventTarget.prototype.addEventListener)
document.body.addEventListener('click', function(e) {
console.info('CLICKED!') // CLICKED!
console.info(e.timeStamp) // 949.3050000000001
console.info(this.tagName)
})
@vazac
EventTarget.prototype.addEventListener = (function(addEventListener) {
return function() {
var args = Array.prototype.slice.call(arguments)
var callback = args[1]
args[1] = function() {
/* ... */
callback.apply(undefined, arguments)
/* ... */
}
addEventListener.apply(undefined, args)
}
})(EventTarget.prototype.addEventListener)
document.body.addEventListener('click', function(e) {
console.info('CLICKED!') // CLICKED!
console.info(e.timeStamp) // 949.3050000000001
console.info(this.tagName) // undefined :(
})
@vazac
EventTarget.prototype.addEventListener = (function(addEventListener) {
return function() {
var args = Array.prototype.slice.call(arguments)
var callback = args[1]
args[1] = function() {
/* ... */
callback.apply(this, arguments)
/* ... */
}
addEventListener.apply(this, args)
}
})(EventTarget.prototype.addEventListener)
@vazac
EventTarget.prototype.addEventListener = (function(addEventListener) {
return function() {
var args = Array.prototype.slice.call(arguments)
var callback = args[1]
args[1] = function() {
/* ... */
callback.apply(this, arguments)
/* ... */
}
addEventListener.apply(this, args)
}
})(EventTarget.prototype.addEventListener)
document.body.addEventListener('click', function(e) {
console.info('CLICKED!') // CLICKED!
console.info(e.timeStamp) // 949.3050000000001
console.info(this.tagName)
})
@vazac
EventTarget.prototype.addEventListener = (function(addEventListener) {
return function() {
var args = Array.prototype.slice.call(arguments)
var callback = args[1]
args[1] = function() {
/* ... */
callback.apply(this, arguments)
/* ... */
}
addEventListener.apply(this, args)
}
})(EventTarget.prototype.addEventListener)
document.body.addEventListener('click', function(e) {
console.info('CLICKED!') // CLICKED!
console.info(e.timeStamp) // 949.3050000000001
console.info(this.tagName) // BODY
})
@vazac
EventTarget.prototype.addEventListener = (function(addEventListener) {
return function() {
var args = Array.prototype.slice.call(arguments)
var callback = args[1]
args[1] = function() {
/* ... */
callback.apply(this, arguments)
/* ... */
}
addEventListener.apply(this, args)
}
})(EventTarget.prototype.addEventListener)
document.body.addEventListener('click', function(e) {
console.info('CLICKED!') // CLICKED!
console.info(e.timeStamp) // 949.3050000000001
console.info(this.tagName) // BODY
foo.bar()
})
@vazac
EventTarget.prototype.addEventListener = (function(addEventListener) {
return function() {
var args = Array.prototype.slice.call(arguments)
var callback = args[1]
args[1] = function() {
/* ... */
callback.apply(this, arguments)
/* ... */
}
addEventListener.apply(this, args)
}
})(EventTarget.prototype.addEventListener)
document.body.addEventListener('click', function(e) {
console.info('CLICKED!') // CLICKED!
console.info(e.timeStamp) // 949.3050000000001
console.info(this.tagName) // BODY
foo.bar()
})
@vazac
EventTarget.prototype.addEventListener = (function(addEventListener) {
return function() {
var args = Array.prototype.slice.call(arguments)
var callback = args[1]
args[1] = function() {
/* ... */
var e
try {
callback.apply(this, arguments)
} catch(_e) {
e = _e
}
/* ... */
if (e) throw e
}
addEventListener.apply(this, args)
}
})(EventTarget.prototype.addEventListener)
@vazac
EventTarget.prototype.addEventListener = (function(addEventListener) {
return function() {
var args = Array.prototype.slice.call(arguments)
var callback = args[1]
args[1] = function() {
/* ... */
var e
try {
callback.apply(this, arguments)
} catch(_e) {
e = _e
}
/* ... */
if (e) throw e
}
addEventListener.apply(this, args)
}
})(EventTarget.prototype.addEventListener)
@vazac
EventTarget.prototype.addEventListener = (function(addEventListener) {
return function() {
var args = Array.prototype.slice.call(arguments)
var callback = args[1]
args[1] = function() {
/* ... */
var e
try {
callback.apply(this, arguments)
} catch(_e) {
e = _e
}
/* ... */
if (e) throw e
}
addEventListener.apply(this, args)
}
})(EventTarget.prototype.addEventListener)
@vazac
EventTarget.prototype.addEventListener = (function(addEventListener) {
return function() {
var args = Array.prototype.slice.call(arguments)
var callback = args[1]
args[1] = function() {
/* ... */
var e
try {
callback.apply(this, arguments)
} catch(_e) {
e = _e
}
/* ... */
if (e) throw e
}
addEventListener.apply(this, args)
}
})(EventTarget.prototype.addEventListener)
@vazac
Let’s talk about the `console`
● Don’t be chatty
● Don’t clear it
● Don’t block *my* messages
● Don’t emit warnings
@vazac
Let’s talk about directives
<link
href='http://tpc.googlesyndication.com/safeframe/1-0-9/html/container.html' />
<script
src='https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js?ver=4.8.2'>
</script>
@vazac
Let’s talk about stack traces
Uncaught ReferenceError: s is not defined
at a (https://js.example.com/bundles/1.2/metrics:1:22530)
at post (https://js.example.com/bundles/1.2/metrics:1:24939)
at TLT</</</n[i] (https://js.example.com/bundles/1.2/metrics:1:14165)
at TLT.ModuleContext</</f[o]</< (https://js.example.com/bundles/1.2/metrics:1:19916)
at e (https://js.example.com/bundles/1.2/metrics:1:44663)
at onevent (https://js.example.com/bundles/1.2/metrics:1:45023)
at _publishEvent (https://js.example.com/bundles/1.2/metrics:1:11026)
at v/< (https://js.example.com/bundles/1.2/metrics:1:32162)
at dispatch (https://js.example.com/bundles/1.6.6/vendor:1:47613)
at add/a.handle (https://js.example.com/bundles/1.6.6/vendor:1:44402)
at wrap/< (https://c.go-mpulse.net/boomerang/XXXXX-XXXXX-XXXXX-XXXXX-XXXXX:15:8516)
at e/f.submitLogin/< (https://js.example.com/bundles/17.6.21.37271/app:1:178948)
at nt/o.success/< (https://js.example.com/bundles/1.6.6/vendor:1:413549)
at nt (https://js.example.com/bundles/1.6.6/vendor:1:430936)
at h/< (https://js.example.com/bundles/1.6.6/vendor:1:431108)
at $eval (https://js.example.com/bundles/1.6.6/vendor:1:438663)
at $digest (https://js.example.com/bundles/1.6.6/vendor:1:437152)
at $apply (https://js.example.com/bundles/1.6.6/vendor:1:438946)
at ut (https://js.example.com/bundles/1.6.6/vendor:1:414086)
at it (https://js.example.com/bundles/1.6.6/vendor:1:415961)
at vp/</k.onload (https://js.example.com/bundles/1.6.6/vendor:1:416510)
@vazac
http://wegotthiscovered.com/tv/jokes-missed-arrested-development/5/
@vazac
Code defensively
navigator.sendBeacon &&
navigator.sendBeacon(...)
if (typeof navigator.sendBeacon !== 'undefined') {
navigator.sendBeacon(...)
}
if (typeof navigator.sendBeacon === 'function') {
navigator.sendBeacon(...)
} ☑
☒
☒navigator.sendBeacon = {}
@vazac
Code defensively
∞
const nodeList = document.getElementsByTagName('input')
while (nodeList.length) {
nodeList[0].parentNode.removeChild(nodeList[0])
}
const nodeList = document.getElementsByTagName('input')
let length = nodeList.length
while (length--) {
nodeList[0].parentNode &&
nodeList[0].parentNode.removeChild(nodeList[0])
}
☑
Element.prototype.removeChild = function() {}
@vazac
Content Security Policy
Content-Security-Policy: default-src 'self' scripts.example.com
Content-Security-Policy: default-src 'self'
Content-Security-Policy: default-src 'self' *.googleapis.com
Content-Security-Policy: script-src 'sha256-qznLcsROx4GACP2dm0UCKCzCG-
HiZ1guq6ZZDob_Tng='
@vazac
Cross-Origin IFRAMEs
// from https://www.example.com
<iframe src='https://third-party.example.com/social-media.js' />
// www.example.com !== third-party.example.com
@vazac
Sandboxing IFRAMEs
● allow-scripts
● allow-same-origin
● allow-forms
● allow-popups
● allow-top-navigation
● allow-top-navigation-by-user-activation
<iframe src='https://third-party.com/widget.html' sandbox='' />
@vazac
http://arresteddevelopment.wikia.com/
@vazac
Freeze
● Object.defineProperty
Object.defineProperty(console, 'log', {
writable: false, // shut off assignment
configurable: false, // shut off changes to the property descriptor
})
@vazac
Freeze
● Object.freeze
Object.freeze(console)
@vazac
Freeze
● freeze.js (https://github.com/cvazac/freeze.js)
const {freeze, thaw} = require('freeze.js')
freeze('console.clear')
freeze('document.readyState')
freeze('XMLHttpRequest')
freeze('XMLHttpRequest.prototype.open')
freeze('MyClass.prototype.method')
● nops calls to Object.defineProperty
● nops calls to Object.assign
● nops setter attempts
@vazac
http://www.fanpop.com/clubs/arrested-development/picks/show/702020/better-pi-gene-parmesan-ice
@vazac
isNativeFunction #1
function isNativeFunction(fn) {
return typeof fn === 'function' &&
/[native code]/.test(String(fn))
}
@vazac
isNativeFunction(XMLHttpRequest) // true
isNativeFunction #1
function isNativeFunction(fn) {
return typeof fn === 'function' &&
/[native code]/.test(String(fn))
}
@vazac
window.XMLHttpRequest = function() { /* ... */ }
isNativeFunction(XMLHttpRequest) // true
isNativeFunction #1
function isNativeFunction(fn) {
return typeof fn === 'function' &&
/[native code]/.test(String(fn))
}
@vazac
isNativeFunction(XMLHttpRequest) // true
isNativeFunction(XMLHttpRequest) // false
window.XMLHttpRequest = function() { /* ... */ }
isNativeFunction #1
function isNativeFunction(fn) {
return typeof fn === 'function' &&
/[native code]/.test(String(fn))
}
@vazac
isNativeFunction(XMLHttpRequest) // true
isNativeFunction(XMLHttpRequest) // false
isNativeFunction #1
function isNativeFunction(fn) {
return typeof fn === 'function' &&
/[native code]/.test(String(fn))
}
window.XMLHttpRequest = function() { /* ... */ }
window.XMLHttpRequest.toString = function() {
return '[native code]'
}
@vazac
isNativeFunction(XMLHttpRequest) // true
isNativeFunction(XMLHttpRequest) // false
isNativeFunction(XMLHttpRequest) // **true**
(false positive)
isNativeFunction #1
function isNativeFunction(fn) {
return typeof fn === 'function' &&
/[native code]/.test(String(fn))
}
window.XMLHttpRequest = function() { /* ... */ }
window.XMLHttpRequest.toString = function() {
return '[native code]'
}
@vazac
isNativeFunction #1
function isNativeFunction(fn) {
return typeof fn === 'function' &&
/[native code]/.test(Function.prototype.toString.call(fn))
}
@vazac
isNativeFunction(XMLHttpRequest) // true
isNativeFunction #1
function isNativeFunction(fn) {
return typeof fn === 'function' &&
/[native code]/.test(Function.prototype.toString.call(fn))
}
@vazac
isNativeFunction(XMLHttpRequest) // true
window.XMLHttpRequest = function() { /* ... */ }
isNativeFunction #1
function isNativeFunction(fn) {
return typeof fn === 'function' &&
/[native code]/.test(Function.prototype.toString.call(fn))
}
@vazac
window.XMLHttpRequest = function() { /* ... */ }
isNativeFunction(XMLHttpRequest) // true
isNativeFunction(XMLHttpRequest) // false
isNativeFunction #1
function isNativeFunction(fn) {
return typeof fn === 'function' &&
/[native code]/.test(Function.prototype.toString.call(fn))
}
@vazac
isNativeFunction(XMLHttpRequest) // true
isNativeFunction(XMLHttpRequest) // false
isNativeFunction #1
function isNativeFunction(fn) {
return typeof fn === 'function' &&
/[native code]/.test(Function.prototype.toString.call(fn))
}
window.XMLHttpRequest = function() { /* ... */ }
Function.prototype.toString = function() {
return '[native code]'
}
@vazac
isNativeFunction(XMLHttpRequest) // true
isNativeFunction(XMLHttpRequest) // false
isNativeFunction(XMLHttpRequest) // **true**
(false positive)
isNativeFunction #1
function isNativeFunction(fn) {
return typeof fn === 'function' &&
/[native code]/.test(Function.prototype.toString.call(fn))
}
window.XMLHttpRequest = function() { /* ... */ }
Function.prototype.toString = function() {
return '[native code]'
}
@vazac
Evil Function.prototype.toString
(function() {
const natives = {};
['open', 'send'].forEach(function(methodName) {
natives[methodName] = XMLHttpRequest.prototype[methodName]
XMLHttpRequest.prototype[methodName] = function() {
/* steal all the data here */
return natives[methodName].apply(this, arguments)
}
})
})()
@vazac
Evil Function.prototype.toString
Function.prototype.toString = (function(toString) {
return function () {
let method = this
if (method === XMLHttpRequest.prototype.send)
method = natives['send']
else if (method === XMLHttpRequest.prototype.open)
method = natives['open']
return toString.apply(method, arguments)
}
})(Function.prototype.toString)
@vazac
Detection tactic #1
new XMLHttpRequest()
XMLHttpRequest.prototype.send()
requestAnimationFrame(function() { /* ... */ })
@vazac
Detection tactic #1
debugger; new XMLHttpRequest()
debugger; XMLHttpRequest.prototype.send()
debugger; requestAnimationFrame(function() { /* ... */ })
@vazac
Detection tactic #2
@vazac
const isNativeFunction = (function() {
return function(fn) {
return typeof fn === 'function' &&
/[native code]/.test(getTrustWorthySerializer().call(fn))
}
})()
@vazac
const isNativeFunction = (function() {
return function(fn) {
return typeof fn === 'function' &&
/[native code]/.test(getTrustWorthySerializer().call(fn))
}
})()
@vazac
const isNativeFunction = (function() {
function getTrustWorthySerializer() {
const iframe = document.createElement('iframe')
iframe.src = 'javascript:false'
document.getElementsByTagName('script')[0].parentNode.appendChild(iframe)
const serializer = iframe.contentWindow.Function.prototype.toString
iframe.parentNode.removeChild(iframe)
return serializer
}
return function(fn) {
return typeof fn === 'function' &&
/[native code]/.test(getTrustWorthySerializer().call(fn))
}
})()
@vazac
Detection tactic #3
requestAnimationFrame(function(){
try {
foo.bar()
} catch(e) {
/* inspect e.stack */
}
})
@vazac
Detection tactic #3
// native
new Error ReferenceError: foo is not defined
at bundle.js:21:7
@vazac
Detection tactic #3
// wrapped
new Error ReferenceError: foo is not defined
at bundle.js:21:7
at window.requestAnimationFrame.args.(anonymous function) (hijacker.js:12:18)
// native
new Error ReferenceError: foo is not defined
at bundle.js:21:7
@vazac
Detection tactic #3
// wrapped
new Error ReferenceError: foo is not defined
at bundle.js:21:7
at window.requestAnimationFrame.args.(anonymous function) (hijacker.js:12:18)
// native
new Error ReferenceError: foo is not defined
at bundle.js:21:7
@vazac
Detection tactic #4
(function() {
const httpVerbTrap = {}
httpVerbTrap.toString = function() {
try {
foo.bar()
} catch (e) { /* inspect e.stack */ }
return 'GET'
}
const xhr = new XMLHttpRequest()
xhr.open(httpVerbTrap, '')
})()
@vazac
Detection tactic #4
(function() {
const httpVerbTrap = {}
httpVerbTrap.toString = function() {
try {
foo.bar()
} catch (e) { /* inspect e.stack */ }
return 'GET'
}
const xhr = new XMLHttpRequest()
xhr.open(httpVerbTrap, '')
})()
@vazac
Wrapping up
● Be careful when bringing in third parties
● Code defensively
● Sandbox third parties, if possible
● Freeze the objects that you need to remain native
● Create short lived browser contexts to grab clean natives
● Detect native overrides with traps
@vazac
Tools
● Request Map Generator by @simonhearne - http://requestmap.webperf.tools/
● Detect native overrides bookmarklet - https://github.com/cvazac/detect-native-overrides
● Freeze constructors, methods, and properties - https://github.com/cvazac/freeze.js
@vazac
Thanks!!
Charles Vazac
@vazac

More Related Content

How to identify bad third parties on your page