By Charles Vazac (Akamai)
MidwestJS 2018
Being a developer of a third-party script means not only that you battle browser discrepancies and bugs, but also non-conforming code that exists on the page. We have spent countless hours debugging issues, only to learn that the culprit was an overwritten method or property that behaves differently than its original native. I will share with you the tools and methods we use to identify and workaround these party crashers.
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
54. @vazac
Circumvention
// from devtools of the blank page
var iframes = opener.document.getElementsByTagName('iframe')
// using tampermonkey
window.open()
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`
}
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!
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)
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
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