From c7e7c703b755a202e483fb718c12bcc244a971e5 Mon Sep 17 00:00:00 2001 From: Diego Cavalcante Date: Sun, 2 Aug 2026 01:26:42 -0300 Subject: [PATCH] fix(js): expose WebIDL interfaces as non-enumerable, like a real browser `bootstrap.js` publishes its DOM interfaces with `globalThis.Node = Node`, which defaults to `enumerable: true`. A real browser exposes every WebIDL interface on the global as `enumerable: false`, so the difference is a one-liner for any script that cares: Object.getOwnPropertyDescriptor(window, 'Node').enumerable // real Chrome: false obscura: true It also means `Object.keys(window)` and `for...in` over the global enumerate 22 names a browser would never list. The file already has the mechanism for this -- `_preHideInternals()` pre-declares internal globals with `enumerable: false`, and its own comment notes that later `globalThis.x = value` assignments only update the value and leave the descriptor intact. This just adds the WebIDL names to that list, so no assignment site changes. Before: 9 of 10 sampled interfaces enumerable. After: 0 of 10, matching Chrome. Regression: interfaces still callable (`typeof Node === 'function'`), `instanceof Node` / `instanceof Element` hold, `querySelector` works, Hacker News still yields 30 results under `--stealth`, snapshot builds clean. Composes with #537 -- verified with both applied: `var Node` pages run, the JSF/RichFaces case loads (jQuery/A4J/RichFaces all live), and enumerability stays at 0/10. --- crates/obscura-js/js/bootstrap.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/obscura-js/js/bootstrap.js b/crates/obscura-js/js/bootstrap.js index 3215d79..c9c41e9 100644 --- a/crates/obscura-js/js/bootstrap.js +++ b/crates/obscura-js/js/bootstrap.js @@ -36,6 +36,18 @@ '_rngAncestors', '_rngOrder', '_rngCmp', '_rngCheckOffset', '_idbRequest', '_idbObjectStore', '_idbTransaction', '_idbDatabase', '_makeListenerBox', + // WebIDL interfaces. A real browser exposes these on the global as + // enumerable:false; here they were assigned with `globalThis.X = X`, which + // defaults to enumerable:true and is detectable in one line: + // Object.getOwnPropertyDescriptor(window, 'Node').enumerable + // Pre-declaring them non-enumerable here is enough -- per the note above, + // the later `globalThis.X = X` assignments only update the value. + 'Node', 'Element', 'Document', 'DocumentFragment', 'DocumentType', + 'Text', 'Comment', 'CDATASection', 'ProcessingInstruction', 'CharacterData', + 'CSSStyleDeclaration', 'DOMTokenList', 'Screen', 'NetworkInformation', + 'MessageChannel', 'MessagePort', 'CustomElementRegistry', + 'XMLHttpRequestEventTarget', 'HTMLMediaElement', 'HTMLVideoElement', + 'HTMLAudioElement', 'WebGL2RenderingContext', ]; var _desc = { value: undefined, writable: true, enumerable: false, configurable: true }; for (var _i = 0; _i < _names.length; _i++) {