Skip to content

Commit ef46dbe

Browse files
committed
fix: reject + and / in the no-native-fromBase64 decode fallback
When `Uint8Array.fromBase64` is unavailable, base64url `decode()` falls back to translating `-_` to `+/` and decoding via `atob`, which accepts the standard-Base64 characters `+` and `/`. The native path (`Uint8Array.fromBase64(input, { alphabet: 'base64url' })`) rejects them, so the same input is accepted on runtimes without the native method and rejected on runtimes with it. Reject `+` and `/` in the fallback before the `-_` -> `+/` translation, matching the native path. Only those two characters change behaviour; padding and whitespace handled by `atob` are untouched.
1 parent b299ce2 commit ef46dbe

2 files changed

Lines changed: 34 additions & 0 deletions

File tree

src/util/base64url.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ export function decode(input: Uint8Array | string): Uint8Array {
2121
if (encoded instanceof Uint8Array) {
2222
encoded = decoder.decode(encoded)
2323
}
24+
if (encoded.indexOf('+') !== -1 || encoded.indexOf('/') !== -1) {
25+
// + and / are not in the Base64URL alphabet; reject them here so this
26+
// path matches Uint8Array.fromBase64(..., { alphabet: 'base64url' }) above
27+
throw new TypeError('The input to be decoded is not correctly encoded.')
28+
}
2429
encoded = encoded.replace(/-/g, '+').replace(/_/g, '/')
2530
try {
2631
return decodeBase64(encoded)

test/unit/base64url.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import test from 'ava'
2+
3+
import { decode } from '../../src/util/base64url.js'
4+
5+
// `+` and `/` are valid Base64 but not Base64URL. Uint8Array.fromBase64 with
6+
// the base64url alphabet rejects them, so the no-native-fromBase64 fallback
7+
// (which translates `-_` to `+/` and decodes via atob) must reject them too
8+
// instead of accepting them as standard Base64.
9+
10+
test('decode rejects + and / (native path)', (t) => {
11+
t.throws(() => decode('+/+/'))
12+
t.throws(() => decode('AB+CD'))
13+
t.deepEqual(decode('-_-_'), new Uint8Array([0xfb, 0xff, 0xbf]))
14+
})
15+
16+
test.serial('decode rejects + and / (fallback path)', (t) => {
17+
// @ts-ignore
18+
const native = Uint8Array.fromBase64
19+
try {
20+
// @ts-ignore
21+
delete Uint8Array.fromBase64
22+
t.throws(() => decode('+/+/'), { instanceOf: TypeError })
23+
t.throws(() => decode('AB/CD'), { instanceOf: TypeError })
24+
t.deepEqual(decode('-_-_'), new Uint8Array([0xfb, 0xff, 0xbf]))
25+
} finally {
26+
// @ts-ignore
27+
Uint8Array.fromBase64 = native
28+
}
29+
})

0 commit comments

Comments
 (0)