How to identify bad third parties on your page
- 6. @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
- 10. @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
- 12. @vazac
What is a third-party
“Code in your site that is managed by someone else” - Guy Podjarny (@guypod)
- 13. @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)
- 14. @vazac
What can go wrong?
● Page breakage
○ Script errors
○ Bad polyfills
○ Namespace collisions
○ SPOF
https://twitter.com/patmeenan/status/938071367525781506
- 15. @vazac
What can go wrong?
● Page breakage
● Performance Issues
○ Slow page load
○ Janky user experience
○ Redirect chains
○ Battery drain
○ Memory Leaks
- 19. @vazac
What can go wrong?
● Page breakage
● Performance Issues
● Privacy & security
● Makes debugging *your* stuff really hard!
https://twitter.com/slicknet/status/997248009778876416
- 22. @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 */
})
}
- 23. @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)
}
- 24. @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
- 26. @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;
})();
- 27. @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
- 30. @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)
- 35. @vazac
Example #3 - detection
document.hasOwnProperty('readyState')
// false
document.hasOwnProperty('readyState')
Object.defineProperty(document, 'readyState', {
/* ... */
})
- 36. @vazac
Example #3 - detection
document.hasOwnProperty('readyState')
// false
document.hasOwnProperty('readyState')
// true
Object.defineProperty(document, 'readyState', {
/* ... */
})
- 38. @vazac
Example #4
var addEventListener = EventTarget.prototype.addEventListener
EventTarget.prototype.addEventListener = function(type, callback) {
addEventListener(type, function() {
/* intervene here */
callback()
})
}
- 39. @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)
- 40. @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)
- 50. @vazac
DevTools detection #1
setInterval(function () {
const threshold = 160
const {outerWidth, innerWidth, outerHeight, innerHeight} = window
window.devToolsOpen = outerWidth - innerWidth > threshold ||
outerHeight - innerHeight > threshold
}, 500);
- 51. @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
- 62. @vazac
Unbound listeners
var car = new Car()
var start = car.start
start()
function Car() { /* ... */}
Car.prototype.start = function() {
// what is `this`?
}
- 63. @vazac
Unbound listeners
var car = new Car()
var start = car.start
start()
function Car() { /* ... */}
Car.prototype.start = function() {
// `this` will be `window`
}
- 68. @vazac
Unbound listeners
var _addEventListener = top.EventTarget.prototype.addEventListener
top.EventTarget.prototype.addEventListener = function intervene() {
// intervene here
return _addEventListener.apply(this, arguments)
}
- 72. @vazac
Unbound listeners
var _addEventListener = top.EventTarget.prototype.addEventListener
top.EventTarget.prototype.addEventListener = function intervene() {
// intervene here
return _addEventListener.apply(this, arguments)
}
- 73. @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`?
})
- 74. @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`?
})
- 75. @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`?
})
- 76. @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
})
- 77. @vazac
Unbound listeners
var method = window.addEventListener
window.addEventListener =
function unboundAddEventListener(eventName, handler) {
method(eventName, handler)
}
- 79. @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!
- 86. @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)
})
- 87. @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
- 89. @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)
})
- 90. @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)
})
- 91. @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 :(
})
- 93. @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)
})
- 94. @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
})
- 95. @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()
})
- 96. @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()
})
- 97. @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)
- 98. @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)
- 99. @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)
- 100. @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)
- 101. @vazac
Let’s talk about the `console`
● Don’t be chatty
● Don’t clear it
● Don’t block *my* messages
● Don’t emit warnings
- 102. @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>
- 103. @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)
- 106. @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() {}
- 107. @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='
- 108. @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
- 109. @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='' />
- 113. @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
- 117. @vazac
window.XMLHttpRequest = function() { /* ... */ }
isNativeFunction(XMLHttpRequest) // true
isNativeFunction #1
function isNativeFunction(fn) {
return typeof fn === 'function' &&
/[native code]/.test(String(fn))
}
- 124. @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))
}
- 126. @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]'
}
- 127. @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)
}
})
})()
- 128. @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)
- 132. @vazac
const isNativeFunction = (function() {
return function(fn) {
return typeof fn === 'function' &&
/[native code]/.test(getTrustWorthySerializer().call(fn))
}
})()
- 133. @vazac
const isNativeFunction = (function() {
return function(fn) {
return typeof fn === 'function' &&
/[native code]/.test(getTrustWorthySerializer().call(fn))
}
})()
- 134. @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))
}
})()
- 137. @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
- 138. @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
- 139. @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, '')
})()
- 140. @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, '')
})()
- 141. @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
- 142. @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