Skip to content

fix: stale issuer cache, XDR size limits, and missing Soroban param validation - #882

Merged
Just-Bamford merged 3 commits into
Sorokit:mainfrom
davedumto:fix/core-assigned-batch-dumto
Oct 21, 2026
Merged

Just-Bamford merged 3 commits into
Sorokit:mainfrom
davedumto:fix/core-assigned-batch-dumto

Conversation

@davedumto

Copy link
Copy Markdown
Contributor

Summary

This batch fixes three distinct validation/security gaps: a stellar.toml cache that trusted stale issuer data indefinitely, an XDR validator with no size or complexity bound, and a Soroban invocation builder that skipped required-parameter validation when no metadata was explicitly supplied.

closes #837
closes #838
closes #843

Changes

  • IssuerDiscovery caches incorrect asset issuers from stale or compromised RPC responses — no cache invalidation on error #837 - IssuerDiscovery caches incorrect asset issuers from stale or compromised RPC responses: fetchStellarToml (src/integration/sep1Toml.ts) and getAnchorInfo (src/integration/anchors.ts) both returned a domain-keyed cache hit immediately within its TTL, with no check that the cached document still agreed with what the domain currently serves. Added detectStellarTomlConflict(), which compares two stellar.toml documents on the two fields that actually control payment/auth routing: SIGNING_KEY (used for SEP-10 trust verification) and CURRENCIES issuer addresses. Both cache readers now re-verify a cache hit against a fresh fetch/resolve by default (new verifyOnCacheHit option, defaults to true, opt-out available for latency-sensitive callers). On a detected mismatch, the stale cache entry is invalidated, a warning is logged with the conflict detail via an optional logger, and the fresh (not the stale) document is returned. If the re-verification fetch itself fails (e.g. a transient network blip), the unverified cached value is served rather than failing the whole call. Found and fixed the same vulnerability in two places since getAnchorInfo reads the same cache through a separate code path that bypassed fetchStellarToml entirely.
  • validateXDR does not check for malicious or oversized XDR envelopes — accepts invalid XDR silently #838 - validateXDR does not check for malicious or oversized XDR envelopes: validateTransactionXdr (src/transaction/validateTransactionXdr.ts) called TransactionBuilder.fromXDR on an unbounded input string with no size or structural limit, making XDR decoding a CPU/memory DOS vector. Added a size check (maxXdrLength, default 100,000 base64 chars) that rejects oversized input before attempting to parse it at all, and an operation-count check (maxOperationCount, default 100, matching the protocol's own per-transaction cap) that rejects a successfully-parsed transaction with too many operations before the per-operation validation loop runs. Both limits are configurable via ValidationRules.
  • buildSorobanInvocation does not validate that all required contract parameters are provided — missing params are silently ignored #843 - buildSorobanInvocation does not validate that all required contract parameters are provided: prepareContractCall (src/soroban/prepareCall.ts) only validated argument count when the caller explicitly supplied cachedMetadata or contractAbi - both are optional fields, and when neither was passed, zero parameter validation occurred, letting missing required params through to simulation and on-chain submission. When neither is supplied, the function now auto-discovers the contract's methods from its deployed Wasm via the existing getContractMethods() (already cached and deduplicated against concurrent in-flight requests), and validates against that instead. Also improved the mismatch error message in contractMetadata.ts's validateCachedMetadata to name the specific missing parameter(s) by their declared name (e.g. "missing required parameter(s): to, amount") rather than only reporting a count mismatch, satisfying the issue's "clear error messages indicate which parameters are missing" criterion.

#840 (ContractInvocationCache does not handle Soroban contract upgrades) was found already fully implemented on main at src/soroban/upgradeDetection.ts's detectContractUpgrade(): it hashes the deployed Wasm, compares against a cached hash, invalidates every related cache (schema/read/state/metadata) on a detected upgrade, and is already wired into createSorokitClient.ts with its own passing test suite. No change needed; left out of this batch.

Test plan

  • src/integration/sep1Toml.test.ts and src/integration/anchors.test.ts: updated the pre-existing cache-hit tests to opt into verifyOnCacheHit: false where they're specifically testing basic TTL caching (that behavior is unchanged when opted out), added new tests for the new default (re-verifies on every hit), a conflict scenario (changed SIGNING_KEY triggers invalidation, warning, and returns the fresh data), and a re-verification-fetch-failure fallback.
  • src/tests/validate-transaction-xdr-limits.test.ts (new): covers acceptance/rejection at and around the size boundary, the operation-count boundary, and confirms the size check runs before parsing (an oversized garbage string is reported as too-large, not as a parse error).
  • src/tests/soroban.test.ts: added a new describe block covering the zero-cachedMetadata/zero-contractAbi auto-discovery path (single and multiple missing parameters, naming each one), the success path once all required params are supplied, and confirmed cachedMetadata still short-circuits auto-discovery when explicitly provided. Updated two pre-existing tests whose assertions on the exact error message text needed to change to match the new, more specific wording (the underlying count-mismatch behavior is unchanged, only the message is more informative).
  • Ran the full suite: npm run typecheck, npm run lint, and npm run test all pass (191 test files, 3849 tests passed, 0 failed, 80 pre-existing skips).

The SEP-1/anchor cache-verification fix changes a previously-undocumented performance characteristic (a cache hit no longer skips the network call by default) in exchange for closing the actual security gap the issue describes; verifyOnCacheHit: false is available for callers who need the old latency profile and already verify issuer identity another way.

…cts (Sorokit#837)

fetchStellarToml and getAnchorInfo both trusted a domain-keyed cache hit
indefinitely within its TTL, with no check that the cached document still
agrees with what the domain currently serves. A compromised or stale RPC
response for a domain would be returned forever within the TTL window
even if the real document had since changed its SIGNING_KEY or CURRENCIES
issuer addresses, both of which control where payments get routed.

Add detectStellarTomlConflict() to compare two stellar.toml documents on
these security-critical fields. Both cache readers now re-verify a cache
hit against a fresh fetch/resolve by default (verifyOnCacheHit, opt-out):
on a mismatch, the cache is invalidated, a warning is logged with the
conflict detail, and the fresh (not the stale) document is returned. If
the re-verification fetch itself fails, the unverified cached value is
served rather than failing the call outright.
…actionXdr (Sorokit#838)

validateTransactionXdr parsed any input string with TransactionBuilder.fromXDR
with no upper bound on length and no cap on the resulting operation count,
making XDR decoding a CPU/memory DOS vector for an oversized or deeply
structured payload.

Reject XDR strings longer than maxXdrLength (default: 100_000 chars) before
attempting to parse them at all, and reject a successfully-parsed
transaction whose operation count exceeds maxOperationCount (default: 100,
the protocol's own cap) before the per-operation validation loop runs.
Both limits are configurable via ValidationRules for callers with
different needs.
…invocation (Sorokit#843)

prepareContractCall only validated argument count against cachedMetadata
or contractAbi when one of them was explicitly supplied by the caller.
Without either, a missing required parameter was silently passed through
to simulation and submission, failing on-chain with an unhelpful error
after fees were already paid.

When neither is supplied, auto-discover the contract's methods from its
deployed Wasm via the existing getContractMethods() (already
cached/deduplicated), and validate against that instead. Also improve the
mismatch error message to name the specific missing parameter(s) by
declared name, not just report a count mismatch.
@drips-wave

drips-wave Bot commented Oct 21, 2026

Copy link
Copy Markdown

@davedumto Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Just-Bamford
Just-Bamford merged commit d612f37 into Sorokit:main Oct 21, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment