fix(policy): don't expose signer or key material through enforced handles - #75
Merged
Merged
Conversation
AlonzoRicardo
force-pushed
the
fix/policy-proxy-internal-reference-exposure
branch
from
July 8, 2026 15:47
e11471a to
0329f6e
Compare
jonathunne
requested changes
Jul 29, 2026
jonathunne
left a comment
Contributor
There was a problem hiding this comment.
This closes direct reads, but still exposes the raw protected references.
i.e. account.signTransaction(...) is blocked by policy and account._signer is undefined, but Object.getOwnPropertyDescriptor(account, '_signer')?.value returns the raw signer and can still sign. The same applies to protocol proxies: protocol.swap(...) is blocked, but
Object.getOwnPropertyDescriptor(protocol, '_account')?.value.sendTransaction(...) still succeeds.
in and Reflect.ownKeys() also continue to reveal the protected members. Let's add protection for getOwnPropertyDescriptor, has, and ownKeys on both the account and protocol proxies.
…dles The policy-enforced account and protocol proxies substituted only the named write methods; every other property access fell through to the raw account. That left `keyPair` (the private key), `_signer`, `_provider`, and a protocol's `_account` back-reference reachable through the guarded handle, so a caller holding an enforced account could sign outside policy evaluation (e.g. `account.keyPair.privateKey`, `account._signer.signTransaction(...)`, `account.getSwapProtocol(x)._account.sendTransaction(...)`). Both get-traps now hide key material (`keyPair`) and internal references (underscore-prefixed by WDK convention) via a shared `isProtectedMember` check. Enforced methods, protocol getters, `simulate`, `registerProtocol`, reads, and identity accessors (`path`/`index`) are unchanged; the prototype-climb variant is neutralized because every signing path funnels through the now-hidden `_signer`. Adds regression coverage for the account and protocol proxies.
The `get` trap alone guards one of several reflective paths into a governed handle. `Object.getOwnPropertyDescriptor` does not route through `get`, so it still returned the raw member behind every intercepted name: - `...(account, '_signer').value` — the raw signer - `...(account, '_account').value.privateKeyBuffer` — key material - `...(account, 'getSwapProtocol').value(label)` — the *unenforced* protocol getter `_registerProtocols` installs as an own property, whose write methods skip policy evaluation entirely - `...(protocol, '_account').value.sendTransaction(...)` `has` and `ownKeys` also kept reporting protected members. Both proxies were near-duplicates, so fold the four interception categories (operations, protocol getters, `simulate`, `registerProtocol`) into one substitutions map and route every trap through it via a shared `createGuardedProxy`: - `get` / `getOwnPropertyDescriptor` serve the enforced substitute, never the raw member. Descriptors are reported only for members the subject owns, so prototype methods still read as inherited. - `get` / `getOwnPropertyDescriptor` / `has` / `ownKeys` treat protected members as absent. - `preventExtensions` is refused: hiding own members is only a legal Proxy result while the target is extensible, so a caller must not be able to freeze it and turn later descriptor reads into TypeErrors.
AlonzoRicardo
force-pushed
the
fix/policy-proxy-internal-reference-exposure
branch
from
July 29, 2026 11:25
0329f6e to
dbc5450
Compare
jonathunne
approved these changes
Jul 29, 2026
Merged
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
The policy engine's enforced account (
wdk.getAccount()) and its protocol proxies only substituted the named members (theOPERATIONSwrite methods, protocol verbs,simulate,registerProtocol). Every other property read fell through to the raw account viaReflect.get, so the guarded handle still surfaced:keyPair→ the private key_signer→ the raw signer (account._signer.signTransaction(...))_provider/_config_accountback-reference (account.getSwapProtocol(x)._account.sendTransaction(...))Any of these lets a caller holding an "enforced" account sign or move funds without policy evaluation, defeating the guardrail the proxy exists to provide.
Fix
Both
gettraps now hide key material and internal references through one shared rule:keyPairis blocked by name — it's the one public getter that returns key material._signer,_provider,_config, the ERC-4337_ownerAccount, and a protocol's_accountacross every chain, rather than a per-name denylist that would miss chain-specific fields.Object.getPrototypeOf(account).sendTransaction.call(account, …)) is neutralized by the same rule: every signing path funnels throughthis._signer, nowundefined.Unchanged: enforced write methods, protocol getters,
simulate,registerProtocol, all reads (getBalance, quotes,verify, …), and identity accessors (path,index).instanceofstill holds (nogetPrototypeOftrap). Ungoverned accounts (no policy registered) are untouched.The wallet classes' public API is deliberately not changed — the boundary is enforced only at the proxy, so direct (non-governed) users keep
keyPair/_signer(no#private fields introduced).Scope / trust boundary
This contains a caller holding an enforced account. It is not a defense against code that also holds the
WalletManager/WDKinstance or constructs an account directly — that remains the existing, documented trust boundary.Tests
New
internal-reference containmentblock covering:keyPairhidden,_signer/_provider/_confighidden, reads + enforced ops still work, and the protocol_accountreach-through blocked while protocol enforcement still holds. Full suite 135 passing,standardclean.