fix(strkey): bound decodeCheck input length before base32 decode - #1583
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
Bounds StrKey inputs before Base32 decoding to prevent unnecessary work and malformed payloads.
Changes:
- Adds per-type encoded-length validation.
- Adds regression tests and updates error expectations.
- Updates changelog and generated API references.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/base/strkey.ts |
Adds encoded-length guards. |
test/unit/base/strkey.test.ts |
Tests guards and valid types. |
docs/reference/core-keys.md |
Updates generated source links. |
CHANGELOG.md |
Documents behavior change. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
quietbits
approved these changes
Aug 3, 2026
7 tasks
Ryang-21
added a commit
that referenced
this pull request
Aug 10, 2026
* Class XDR Implementation (#1422) * feat(xdr): codegen tool + schema source * feat(xdr): add class-based XDR runtime and sep51 JSON walker * refactor(numbers): drop LargeInt classes, delegate to new XDR layer * refactor(base): migrate src/base to new XDR layer; drop legacy xdr.ts + generated * refactor: migrate downstream consumers (bindings/contract/horizon/rpc/webauth) * feat(base/scval): add bool to ScValType * allow opaque xdr types to be initalized via string * refactor xdr strings to be represented soley via bytes with a dx friendly XdrString wrapper class * generate a value getter function for void union cases * add a is() function to the generated XDR union classes for instanceOf checks * feat(xdr): regenerate schemas against @stellar/js-xdr and add CAP-71 credentials * feat(xdr): wire the toJSON hook so JSON.stringify emits SEP-0051 * fix(bindings): emit Uint8Array for bytes/bytesN to match scValToNative * refactor(contract): rename fromJSON to fromJson with deprecated aliases * feat(xdr): accept ASCII asset codes with zero padding in constructors * Migrate public API from Buffer to Uint8Array (#1564) * feat(base): migrate crypto and strkey APIs to Uint8Array * feat(base)!: migrate value types to Uint8Array * feat(base)!: migrate transactions, operations, and auth to Uint8Array * feat!: migrate contract, rpc, and webauth layers to Uint8Array * build!: drop buffer polyfill and dependency * fix(horizon): type manage_data value as string to match runtime API * fix(xdr): emit SEP-51 key `type` instead of Rust-escaped `type_` (#1571) * build(xdr): regenerate xdr.json via docker from pinned stellar-xdr commit (#1575) * build(xdr): regenerate schema from stellar-xdr with CAP-83 and CAP-85 ungated (#1576) * build(xdr): regenerate schema from stellar-xdr with CAP-83 and CAP-85 ungated * fix(xdr): keep consumers compiling against the regenerated union arms * fix(vitest): isolate browser dep cache per transport * feat(xdr): support CAP-83 empty tx set values and CAP-85 external executables (#1577) * build(xdr): fail the schema download instead of masking it in a pipe * feat(xdr): support CAP-83 and CAP-85 protocol values * fix(xdr): bound decimal string length before BigInt parse in JSON decode (#1581) * fix(xdr): bound decimal string length before BigInt parse in json decode * refactor(xdr): name the digit-budget constants in bigint-parts * fix(xdr): restrict fromJson to SEP-0051 keys and reject unknown fields (#1582) * fix(xdr): restrict fromJson to SEP-51 keys and reject unknown fields * fix(test): correct horizon corpus fixture path so corpus tests run * fix(strkey): bound decodeCheck input length before base32 decode (#1583) * fix(xdr): reject AssetCode12 JSON codes shorter than 5 bytes (#1585) * fix(horizon)!: make TransactionFailedExtras result_codes.operations optional (#1586) * Fix: signed payload strkey framing (#1588) * fix(strkey): validate signed payload framing in decodeCheck * Fix: xdr json decode validation (#1592) * fix(xdr): throw on unknown union discriminant in fromXdrObject * fix(strkey): validate the claimable balance discriminant byte * fix(xdr): reject non-decimal integer strings in JSON decoding * V17.0.0 rc.1 (#1593) * chore(release): cut v17.0.0-rc.1 * fix(spec): restore instanceof Map check lost in the v17 merge * feat(xdr): add validateXdr static to every generated type (#1597) * feat(xdr): add validateXdr static to every generated type * fix(contract): declare error classes, make types self-contained (#1627) * perf(strkey): reject by length and prefix before decodeCheck throws (#1629) * fix: restore wide-int bounds statics, document Memo.text break (#1628) * fix(xdr): restore wide-int MIN_VALUE/MAX_VALUE statics --------- Co-authored-by: Iveta <quietbits@users.noreply.github.com>
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.
What
decodeChecknow validates the encoded string's length against the requested strkey type before it decodes anything. Each type has a legal encoded length — 56 characters forG/S/T/X/C/L, 58 forB, 69 forM, and a 56–165 range for the variable-lengthP— and an input outside that range throwsinvalid encoded string lengthimmediately. The unknown-version-byte-name check moved up alongside it, so a bad type name is also rejected before any decoding work. The per-type length table lives in a newencodedLengthRangehelper;isValidno longer duplicates it and relies ondecodeCheckthrowing instead.Why
The length check used to live only in
isValid, so the two ways of reachingdecodeCheckdirectly — calling the exported function, andSignerKey.decodeAddress— had no bound on their input at all. That had two consequences.First, an attacker-supplied string of arbitrary length drove a full base32 decode plus a canonical re-encode comparison before anything rejected it. A megabyte-long
G...string did a megabyte of work to produce an error that its second character already implied.Second, and worse, a strkey with a valid version byte and a correct CRC16 but a wrong-sized payload decoded successfully and returned a mis-sized buffer. A 37-byte payload encoded as an
ed25519PublicKeystrkey came back as 37 bytes, and the caller only failed later, downstream, if it happened to check —isValidwould have caught it, but nothing on the direct path did. That case now throws.One consumer-visible consequence: an input that was already invalid may now report a length error where it previously reported a checksum, version-byte, or base32-character error. The input is rejected either way, but anyone matching on the message text will see the new one. Two existing tests asserted the old base32 message for 58-character inputs and were updated.
encodedLengthRangehas nodefaultcase, and the repo builds withstrictandnoImplicitReturns, so adding a strkey type will fail the build until the table covers it.