Last active
February 15, 2021 00:53
-
-
Save eligrey/5f25971ce540a2dee233c8174dbfaafc to your computer and use it in GitHub Desktop.
Secure user-initiated click isTrusted attestation challenge — https://go.eligrey.com/t/event-isTrusted-challenge
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @ts-nocheck | |
// | |
// User-initiated click isTrusted attestation challenge: | |
// | |
// Secure this 'click' event listener from synthetic clicks | |
// while working in a prototype pollution superfund site. | |
// | |
// addEventListener() has been been backdoored. | |
// | |
// Good luck! | |
// | |
// Secure references for backdoor | |
const apply = Function.prototype.call.bind(Function.prototype.apply); | |
const create = Object.create.bind(Object); | |
const { addEventListener } = EventTarget.prototype; | |
const { MouseEvent } = self; | |
// Backdoor addEventListener | |
EventTarget.prototype.addEventListener = function () { | |
const [type, listener] = arguments; | |
// Hijack click listeners | |
if (type === 'click') { | |
const props = {}; | |
const click = new MouseEvent('click'); | |
// Clone and flatten all enumerable properties in prototype chain | |
for (const prop in click) { | |
props[prop] = { value: click[prop] }; | |
} | |
// Spoof event.isTrusted | |
props.isTrusted = { value: true }; | |
// Create fake user-initiated click with MouseEvent prototype chain | |
const fakeClick = create(click, props); | |
listener(fakeClick); | |
} | |
// Pass-through to real addEventListener | |
return apply(addEventListener, this, arguments); | |
}; | |
// Backdoor `instanceof MouseEvent` | |
const MouseEventHasInstance = Object.getOwnPropertyDescriptor(MouseEvent, Symbol.hasInstance); | |
if (!MouseEventHasInstance || MouseEventHasInstance.configurable) { | |
Object.defineProperty(MouseEvent, Symbol.hasInstance, { | |
value: () => true | |
}); | |
} | |
// Backdoor `event.type` getter | |
const getType = Object.getOwnPropertyDescriptor(Event.prototype, 'type').get; | |
Object.defineProperty(Event.prototype, 'type', { | |
get() { | |
return Object.getOwnPropertyDescriptor(this, 'type')?.value || getType.call(this); | |
} | |
}); | |
// SOLUTION | |
// Fix this function without changing code outside of the solution block | |
const isUserInitiatedClick = (event) => { | |
try { | |
return ( | |
event instanceof MouseEvent && | |
event.type === 'click' && | |
event.isTrusted | |
); | |
} catch (ex) { | |
return false; | |
} | |
}; | |
// END SOLUTION | |
const secret = '[secret value]'; | |
const reciever = (data) => { | |
console.log('Secret recieved:', data); | |
}; | |
const container = document.querySelector('ul.nav') || document.body; | |
const button = container.querySelector('button.challenge-button') || container.appendChild(document.createElement('button')); | |
button.classList.add('challenge-button'); | |
if (!button.textContent) { | |
button.append('Share secret (real clicks only!)'); | |
} | |
// Clear any event listeners on button | |
const clone = button.cloneNode(true); | |
button.replaceWith(clone); | |
// Listen for button clicks | |
clone.addEventListener('click', (event) => { | |
if (isUserInitiatedClick(event)) { | |
console.log('Click trusted:', event); | |
reciever(secret); | |
} else { | |
console.error('Detected fake click:', event); | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment