Skip to content

[ANCHOR-1268]: SEP-10 authentication bypass and owner lockout via sign-extended threshold/weight parsing on the RPC ledger backend - #1987

Open
amandagonsalves wants to merge 2 commits into
developfrom
fix/anchor-1268
Open

[ANCHOR-1268]: SEP-10 authentication bypass and owner lockout via sign-extended threshold/weight parsing on the RPC ledger backend#1987
amandagonsalves wants to merge 2 commits into
developfrom
fix/anchor-1268

Conversation

@amandagonsalves

@amandagonsalves amandagonsalves commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Description

StellarRpc.getAccount() reads an account's med_threshold and master-key weight from the on-chain XDR Thresholds field, which the Java SDK models as a signed byte[] even though the wire type is uint8. The code widened each byte with a plain (int)/(long) cast, which in Java sign-extends: any byte in [128,255] becomes negative (200-56, 255-1). Sep10Service then compares that value against the summed SEP-10 challenge signer weights via the SDK's Sep10Challenge.verifyChallengeTransactionThreshold, which does a signed if_icmpge. A negative threshold makes that comparison unconditionally true, so on accounts with med_threshold >= 128 (institutional custody, issuers, or any account using per-signer weights like 50/100/255 instead of 1/2/3), a single listed signer of any weight — including a master key the owner deliberately demoted to weight 0 after a compromise — is enough to obtain a valid SEP-10 JWT for that account. The same cast applied to the master weight produces the mirror-image bug: any single-signer account whose owner raised their master weight to >= 128 is permanently locked out, with the anchor returning a negative weight in the rejection message.

This only affects the RPC ledger backend (stellar_network.type: rpc); Horizon is unaffected because AccountResponse.Thresholds is already deserialized from JSON into correctly-ranged ints. RPC is not the schema default, but it is what SDF's own shipped reference profile sets and is required for Soroban/SEP-45 support, so this isn't an edge-case configuration.

The fix is a single masking helper (b & 0xFF), applied at all four points the raw bytes are widened. No other change is needed: once the parsed value is guaranteed non-negative by construction, both the bypass and the lockout resolve as a direct consequence, and no defense-in-depth range check or signer-list change is required.

Changes

  • StellarRpc.getAccount: added unsignedByte(byte) (b & 0xFF) and used it for the three threshold bytes and the master-key signer weight, replacing the sign-extending (int)/(long) casts.
  • Sep10RpcThresholdSignExtensionTest.kt (new): wires the real StellarRpc + Sep10Service + pinned Stellar SDK together (only the Soroban-RPC network call itself is stubbed, same boundary StellarRpcTest already stubs) and runs real, freshly-signed SEP-10 challenges through them, so the SDK's actual threshold-comparison bytecode decides pass/fail. Covers: the parsing defect in isolation, a below-threshold co-signer bypass, a revoked weight-0 master-key bypass, the owner-lockout case, a control case proving the check itself works, and a 7-value parameterized sweep (0, 1, 127, 128, 200, 254, 255) asserting no uint8 byte value ever parses as negative.

Acceptance Criteria

  • On the RPC ledger backend, an account with med_threshold = 200 rejects a SEP-10 challenge signed only by a single weight-100 co-signer.
  • The same account rejects a challenge signed only by a revoked weight-0 master key.
  • A plain single-signer account with master weight 200 and med_threshold = 100 authenticates successfully with its own master key (no lockout).
  • StellarRpc.getAccount() returns non-negative low/medium/high thresholds and a non-negative master-signer weight for every possible on-chain byte value (0255).
  • Existing correctly-rejected cases (weight below threshold, both bytes <128) are unaffected.

Context

HackerOne #3893785

Testing

  • Unit: ./gradlew :core:test --tests "org.stellar.anchor.ledger.Sep10RpcThresholdSignExtensionTest"
  • Unit: ./gradlew :core:test --tests "org.stellar.anchor.ledger.StellarRpcTest" (no regressions in existing coverage)
  • Integration: ./gradlew :core:test :platform:test :core:spotlessCheck (full suite, confirms no regressions elsewhere)

Documentation

N/A

Known limitations

N/A

* fix sign-extension issue when parsing on-chain uint8 threshold and master-weight bytes

* update `stellarrpc.java` to properly interpret unsigned byte values as positive integers

* add new `sep10rpcthresholdsignextensiontest.kt` to validate the fix
Copilot AI review requested due to automatic review settings July 31, 2026 14:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes RPC ledger parsing of unsigned threshold and master-weight bytes, preventing SEP-10 authentication bypasses and owner lockouts.

Changes:

  • Converts XDR threshold bytes to unsigned integers before use.
  • Adds regression coverage for bypass, lockout, boundary, and control cases.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
core/src/main/java/org/stellar/anchor/ledger/StellarRpc.java Correctly parses unsigned XDR threshold bytes.
core/src/test/kotlin/org/stellar/anchor/ledger/Sep10RpcThresholdSignExtensionTest.kt Tests parsing and SEP-10 authentication behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@amandagonsalves amandagonsalves self-assigned this Jul 31, 2026
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.

3 participants