js: make event.isTrusted false for page-constructed events

isTrusted was hardcoded to true for every Event, so new Event('x').isTrusted
returned true. Real browsers return false for script-constructed events and
true only for user-agent dispatched ones, so this was a trivial bot-detection
tell (issue #303).

Track trusted events in a closure-private WeakSet that page JS cannot read or
forge; default isTrusted to false. The CDP input pipeline marks its synthesized
mouse/key events trusted via a non-enumerable __obscura_markTrusted helper, so
automation clicks stay trusted like real Chrome over CDP while page-built
events report false.
This commit is contained in:
SGavrl committed 2026-06-17 15:12:04 +00:00
1 parent af2f89a03e
commit 6264e6bfad
2 files changed
+22 -13

No files matched your search

+11 -11
View File
@@ -23,9 +23,9 @@ pub async fn handle(
var target = (document.elementFromPoint && document.elementFromPoint({x},{y})) || globalThis.__obscura_click_target || document.activeElement || document.body;\
if (!target) return;\
globalThis.__obscura_click_target = target;\
var evt = new MouseEvent('mousedown', {{bubbles:true,cancelable:true,clientX:{x},clientY:{y},button:0}});\
var evt = globalThis.__obscura_markTrusted(new MouseEvent('mousedown', {{bubbles:true,cancelable:true,clientX:{x},clientY:{y},button:0}}));\
target.dispatchEvent(evt);\
var click = new MouseEvent('click', {{bubbles:true,cancelable:true,clientX:{x},clientY:{y},button:0}});\
var click = globalThis.__obscura_markTrusted(new MouseEvent('click', {{bubbles:true,cancelable:true,clientX:{x},clientY:{y},button:0}}));\
var cancelled = !target.dispatchEvent(click);\
if (!cancelled) {{\
var link = target.closest ? target.closest('a[href]') : null;\
@@ -46,7 +46,7 @@ pub async fn handle(
if (form2 && typeof form2.submit === 'function') {{ try {{ form2.submit(target); }} catch(e) {{}} }}\
}} else if (tag === 'INPUT' && (type === 'checkbox' || type === 'radio')) {{\
target.checked = !target.checked;\
try {{ target.dispatchEvent(new Event('change', {{bubbles:true}})); }} catch(e) {{}}\
try {{ target.dispatchEvent(globalThis.__obscura_markTrusted(new Event('change', {{bubbles:true}}))); }} catch(e) {{}}\
}}\
}}\
}}\
@@ -62,7 +62,7 @@ pub async fn handle(
"(function() {{\
var target = (document.elementFromPoint && document.elementFromPoint({x},{y})) || globalThis.__obscura_click_target || document.activeElement || document.body;\
if (!target) return;\
var evt = new MouseEvent('mouseup', {{bubbles:true,cancelable:true,clientX:{x},clientY:{y},button:0}});\
var evt = globalThis.__obscura_markTrusted(new MouseEvent('mouseup', {{bubbles:true,cancelable:true,clientX:{x},clientY:{y},button:0}}));\
target.dispatchEvent(evt);\
}})()",
x = x, y = y,
@@ -85,7 +85,7 @@ pub async fn handle(
let js = format!(
"(function() {{\
var target = document.activeElement || document.body;\
var evt = new KeyboardEvent('keydown', {{bubbles:true,cancelable:true,key:'{key}',code:'{code}'}});\
var evt = globalThis.__obscura_markTrusted(new KeyboardEvent('keydown', {{bubbles:true,cancelable:true,key:'{key}',code:'{code}'}}));\
target.dispatchEvent(evt);\
}})()",
key = key.replace('\'', "\\'"),
@@ -102,7 +102,7 @@ pub async fn handle(
var target = document.activeElement;\
if (target && (target.localName === 'input' || target.localName === 'textarea')) {{\
target.value = (target.value || '') + '{text}';\
target.dispatchEvent(new Event('input', {{bubbles:true}}));\
target.dispatchEvent(globalThis.__obscura_markTrusted(new Event('input', {{bubbles:true}})));\
}}\
}})()",
text = escaped_text,
@@ -118,10 +118,10 @@ pub async fn handle(
let js = "(function() {\
var target = document.activeElement;\
if (!target) return;\
target.dispatchEvent(new KeyboardEvent('keypress', {bubbles:true,key:'Enter',code:'Enter'}));\
target.dispatchEvent(globalThis.__obscura_markTrusted(new KeyboardEvent('keypress', {bubbles:true,key:'Enter',code:'Enter'})));\
if (target.localName === 'textarea') {\
target.value = (target.value || '') + '\\n';\
target.dispatchEvent(new Event('input', {bubbles:true}));\
target.dispatchEvent(globalThis.__obscura_markTrusted(new Event('input', {bubbles:true})));\
} else {\
var form = target.form || (target.closest && target.closest('form'));\
if (form && typeof form.submit === 'function') form.submit();\
@@ -135,7 +135,7 @@ pub async fn handle(
var target = document.activeElement;\
if (target && (target.localName === 'input' || target.localName === 'textarea')) {\
target.value = target.value.slice(0, -1);\
target.dispatchEvent(new Event('input', {bubbles:true}));\
target.dispatchEvent(globalThis.__obscura_markTrusted(new Event('input', {bubbles:true})));\
}\
})()";
page.evaluate(js);
@@ -145,7 +145,7 @@ pub async fn handle(
let js = format!(
"(function() {{\
var target = document.activeElement || document.body;\
var evt = new KeyboardEvent('keyup', {{bubbles:true,key:'{key}',code:'{code}'}});\
var evt = globalThis.__obscura_markTrusted(new KeyboardEvent('keyup', {{bubbles:true,key:'{key}',code:'{code}'}}));\
target.dispatchEvent(evt);\
}})()",
key = key.replace('\'', "\\'"),
@@ -160,7 +160,7 @@ pub async fn handle(
var target = document.activeElement;\
if (target && (target.localName === 'input' || target.localName === 'textarea')) {{\
target.value = (target.value || '') + '{text}';\
target.dispatchEvent(new Event('input', {{bubbles:true}}));\
target.dispatchEvent(globalThis.__obscura_markTrusted(new Event('input', {{bubbles:true}})));\
}}\
}})()",
text = text.replace('\'', "\\'").replace('\\', "\\\\"),
+11 -2
View File
@@ -14,7 +14,7 @@
'__obscura_errors', '__obscura_init', '__obscura_hide_list',
'__obscura_objects', '__obscura_oid', '__obscura_ua',
'__obscura_platform', '__obscura_ua_platform', '__obscura_ua_platform_version',
'__obscura_stealth',
'__obscura_stealth', '__obscura_markTrusted',
'__documentReadyState__', '__currentUrl',
// internal helpers (var-declared throughout the file)
'__processDynScriptQueue', '_markNative', '_fpRand', '_fpNoise',
@@ -4002,9 +4002,18 @@ globalThis.DOMException = (function () {
}
return DOMException;
})();
// Per the UI Events spec, only events the user agent dispatches (real or
// automation-synthesized input) are trusted; events page script builds with
// `new Event(...)` must report isTrusted === false (issue #303). Returning true
// for everything is a trivial bot-detection tell. Trusted events are tracked in
// a closure-private WeakSet so page JS can neither read nor forge the flag.
// obscura's CDP input pipeline marks its synthetic events via the
// non-enumerable __obscura_markTrusted helper.
const _trustedEvents = new WeakSet();
globalThis.__obscura_markTrusted = function(ev) { try { if (ev) _trustedEvents.add(ev); } catch (_e) {} return ev; };
globalThis.Event = class Event {
constructor(t,o={}) { this.type=t;this.bubbles=!!o.bubbles;this.cancelable=!!o.cancelable;this.composed=!!o.composed;this.defaultPrevented=false;this.target=null;this.currentTarget=null;this.eventPhase=0;this.timeStamp=Date.now();this._propagationStopped=false;this._immediatePropagationStopped=false; }
get isTrusted() { return true; }
get isTrusted() { return _trustedEvents.has(this); }
preventDefault() { if (this.cancelable) this.defaultPrevented=true; } stopPropagation(){ this._propagationStopped=true; } stopImmediatePropagation(){ this._propagationStopped=true; this._immediatePropagationStopped=true; }
initEvent(type,bubbles,cancelable) { if (arguments.length < 1) throw new TypeError("Failed to execute 'initEvent' on 'Event': 1 argument required, but only 0 present."); this.type=String(type);this.bubbles=!!bubbles;this.cancelable=!!cancelable;this.defaultPrevented=false;this._propagationStopped=false;this._immediatePropagationStopped=false; }
composedPath() {