fix(ext/node): brand-check SecureContext._external accessor#33569
Merged
bartlomieju merged 1 commit intodenoland:mainfrom Apr 28, 2026
Merged
fix(ext/node): brand-check SecureContext._external accessor#33569bartlomieju merged 1 commit intodenoland:mainfrom
bartlomieju merged 1 commit intodenoland:mainfrom
Conversation
Node's `_external` accessor on a TLS SecureContext returns the C++ external pointer; reading it on a non-context receiver hits a slot check and throws. Deno had no such accessor at all, so accessing `secureContext.context._external` returned `undefined` regardless of the receiver — silently passing prototype-tampering tests instead of matching Node's behaviour. Define `_external` as an own getter on the context that brand-checks the receiver via a `WeakSet` and throws `TypeError` when it's not the same context object the SecureContext constructed. Enables `parallel/test-tls-external-accessor.js`.
nathanwhitbot
commented
Apr 27, 2026
Contributor
Author
nathanwhitbot
left a comment
There was a problem hiding this comment.
Targeted fix for the exact symptom — prototype-tampering reads of
_external on a non-pctx receiver now throw TypeError instead of
silently returning undefined.
The brand check via a module-scope WeakSet is the right shape:
- Constructor adds
this.contextto the set and installs the getter. - Getter compares
this(the property-access receiver) against the set —
inherited objects,Reflect.getwith a different receiver, etc. all fail
the check. - Returning
thisfrom the live path is fine because deno has no real
external pointer and the upstream test only asserts "no throw" for that
branch.
Worth verifying once at the implementation level:
- The descriptor uses
configurable: true, which is more permissive than
Node's C++ accessor — it could be deleted or redefined by user code.
Probably fine since the test doesn't exercise it, but the safer match is
configurable: false. __proto__: nullon the descriptor is a nice touch — it dodges the
Object.prototypepollution path that the recent child_process PR was
fixing in the same neighborhood.- No
setis defined, so writes will throw in strict mode and silently
no-op otherwise. Matches Node's accessor (no setter).
The WeakSet is module-scope and weakly held, so contexts won't leak after
their owning SecureContext is GC'd.
LGTM.
bartlomieju
approved these changes
Apr 28, 2026
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Node's
tls.createSecureContext().context._externalis a C++ accessor that returns the underlying external pointer; reading it on a non-context receiver hits an internal-slot check and throwsTypeError. Deno had no_externalaccessor at all, so:This patch defines
_externalas a non-enumerable own getter on the context object, brand-checked against a privateWeakSetof contexts theSecureContextconstructor created. Reading it on the original context returns the context (we have no real external pointer); reading it via prototype-tampering throwsTypeError("Illegal invocation"), matching Node's behavior.Enables
parallel/test-tls-external-accessor.jsin the Node compat suite.Test plan
cargo test --test node_compat -- test-tls-external-accessor(was failing on main, passes here)cargo build --bin denocleantools/format.jsandtools/lint.js --jscleantest-tls-basic-validations,test-tls-alert,test-tls-cert-regression,test-tls-honorcipherorder,test-tls-startcom-wosign-whitelist,test-tls-check-server-identity,test-tls-connect-simple.