diff --git a/src/bases/base.ts b/src/bases/base.ts index 54cea234..53bb8bda 100644 --- a/src/bases/base.ts +++ b/src/bases/base.ts @@ -218,6 +218,14 @@ function createAlphabetIdx (alphabet: string, caseInsensitive: boolean): Record< // Build the character lookup table: const alphabetIdx: Record = {} for (let i = 0; i < alphabet.length; ++i) { + // The trailing '=' of a padded alphabet is a padding marker, not a data + // symbol. Skip it so an interior '=' has no index and is rejected as a + // non-base character in decode, instead of being folded into the output as + // bogus data. Trailing padding is stripped by decode before the lookup, so + // valid input round-trips unchanged. + if (alphabet[i] === '=') { + continue + } alphabetIdx[alphabet[i]] = i // For case-insensitive codecs, map the opposite case to the same index so // differently cased input decodes without errors (multibase spec). diff --git a/test/test-multibase.spec.ts b/test/test-multibase.spec.ts index de1bc596..5931c1a2 100644 --- a/test/test-multibase.spec.ts +++ b/test/test-multibase.spec.ts @@ -199,6 +199,20 @@ describe('multibase', () => { assert.throws(() => base64.decode(b64.substring(0, b64.length - 1)), 'Unexpected end of data') }) + it('rejects interior padding (RFC 4648 section 3.2)', () => { + const padded = { ...b32, ...b64 } + for (const base of [padded.base64pad, padded.base64urlpad, padded.base32pad, padded.base32hexpad]) { + const value = Uint8Array.from([0x41, 0x42]) + // A valid encoding, with legitimate trailing padding, still round-trips. + const encoded = base.encode(value) + assert.deepStrictEqual(base.decode(encoded), value) + // A '=' spliced into the data run (padding may only be a trailing run) is + // invalid and must be rejected, not silently decoded to bogus bytes. + const tampered = encoded.slice(0, 2) + '=' + encoded.slice(2) + assert.throws(() => base.decode(tampered), `Non-${base.name} character`) + } + }) + it('infers prefix and name correctly', () => { const name = base32.name