Skip to content

fix(security): close bridge auth bypass, verify real SEP-10 signers, reserve RBAC keys - #223

Merged
N-i-xx merged 1 commit into
mainfrom
fix/critical-auth-and-reserved-keys
Aug 24, 2026
Merged

fix(security): close bridge auth bypass, verify real SEP-10 signers, reserve RBAC keys#223
N-i-xx merged 1 commit into
mainfrom
fix/critical-auth-and-reserved-keys

Conversation

@N-thnI

@N-thnI N-thnI commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Three critical findings from an org-wide source review. All verified by test.

1. The shipped bridge ran the WebSocket feed with authentication disabled

engine-bridge/src/main.ts constructed new ZkStateSyncer(propagator, { port }) — no auth, no serverSigningKey. Both are optional, so:

  • verifyClient is spread in conditionally (...(options.auth && { verifyClient })) and was omitted entirely — the server accepted any connection on port 8080 and RelayerAuth never ran.
  • The broadcast guard if (this.options.serverSigningKey && !state.authenticatedAs) continue short-circuited on the missing key, so every connected socket received all ZK state commitments.

The whole RelayerAuth/SEP-10 layer was unreachable in the only production entrypoint. Worth noting this also means #202 (timing-safe comparison inside RelayerAuth) describes code that never executed.

Fix: main.ts wires auth from the environment, and ZkStateSyncer now fails closed — it throws unless auth, serverSigningKey, or an explicit allowUnauthenticated: true is supplied, warning loudly in that last case.

2. SEP-10 verification ignored the account's real signer configuration

wallet-connector.ts called verifyChallengeTxThreshold with a fabricated signer set — [{ key: clientAccountID, weight: 1 }] and threshold: 1 — where clientAccountID was read out of the submitted XDR itself. The account was never fetched.

So a master key revoked to weight 0 (the standard move after a key compromise) still authenticated, and any single signer satisfied a multi-sig account regardless of its real med_threshold. These are the treasury and governance accounts — precisely the ones expected to be multi-sig.

Fix: verifyResponse is now async and takes an AccountLoader, using the account's actual signers and med_threshold. It fails closed when the account can't be loaded rather than falling back to the master-key assumption. horizonAccountLoader() provides the real implementation; the interface keeps it mockable.

3. update_param could permanently brick RBAC

RESERVED_KEYS omitted the storage slots owned by core::access. Writing C_ADMIN replaces an Address with a u64; every later require_role then fails host conversion, and access::initialize refuses to re-run because C_INIT is still set — no recovery path. Added C_INIT, C_ADMIN, C_OPERS, C_AUDIT, and the proxy GAP slot.

Tests

engine-bridge: 93 passed. engine-core: 95 passed. New regression tests cover the startup guard, revoked-weight rejection, below-threshold multi-sig rejection, loader failure, and each reserved RBAC key.

Two existing tests asserted the old insecure defaults and were updated to opt in explicitly — worth a look during review, since that's a behaviour change contributors may hit.

New environment variables

RELAYER_API_KEYS, RELAYER_JWT_SECRET, SERVER_SIGNING_KEY, HORIZON_URL, NETWORK_PASSPHRASE, AUTH_DOMAIN, and ALLOW_UNAUTHENTICATED_SYNCER. A deployment supplying none of these will now fail to start rather than running open — that's intentional, but it does mean the bridge needs configuring before the next deploy.

…reserve RBAC keys

Three critical findings from an org-wide source review.

1. The shipped bridge ran the WebSocket feed with all authentication disabled.
   main.ts constructed `new ZkStateSyncer(propagator, { port })` — no `auth`,
   no `serverSigningKey`. Both are optional, so `verifyClient` was spread in
   conditionally and omitted entirely (any connection accepted on :8080), and
   the broadcast guard `if (serverSigningKey && !authenticatedAs) continue`
   short-circuited, sending every ZK state commitment to every socket. The whole
   RelayerAuth/SEP-10 layer was unreachable in the only production entrypoint.

   main.ts now wires auth from the environment, and ZkStateSyncer fails closed:
   it throws unless `auth`, `serverSigningKey`, or an explicit
   `allowUnauthenticated: true` is supplied, warning loudly in the last case.

2. SEP-10 verification ignored the client account's real signer configuration.
   verifyChallengeTxThreshold was called with a fabricated set —
   `[{ key: clientAccountID, weight: 1 }]`, threshold 1 — where the account ID
   came from the submitted XDR itself. The account was never fetched, so a
   master key revoked to weight 0 still authenticated, and any single signer
   satisfied a multi-sig account regardless of its med_threshold. These are the
   treasury and governance accounts, i.e. the ones expected to be multi-sig.

   verifyResponse is now async and takes an AccountLoader, using the account's
   real signers and med_threshold. It fails closed if the account can't be
   loaded rather than falling back to the master-key assumption.

3. RESERVED_KEYS omitted the RBAC slots owned by core::access, so update_param
   could overwrite C_ADMIN (an Address) with a u64. Every later require_role
   then fails host conversion, and access::initialize refuses to re-run because
   C_INIT is still set — no recovery path. Added C_INIT, C_ADMIN, C_OPERS,
   C_AUDIT and the proxy GAP slot.

Tests: engine-bridge 93 passed; engine-core 95 passed. Adds regression tests for
each — the startup guard, revoked-weight and below-threshold multi-sig
rejection, loader failure, and reserved RBAC keys.

@N-i-xx N-i-xx left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed: bridge auth wiring, SEP-10 signer verification, and RBAC key reservation. All checks green.

@N-i-xx
N-i-xx merged commit e761d3b into main Aug 24, 2026
4 checks passed
@N-i-xx
N-i-xx deleted the fix/critical-auth-and-reserved-keys branch August 24, 2026 17:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants