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.
This commit is contained in:
Diego Cavalcante committed 2026-08-02 01:26:42 -03:00
1 parent eab2a5eea1
commit c7e7c703b7
1 file changed
+12
+12
View File
@@ -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++) {