Skip to content

Commit c59f537

Browse files
authored
Improve decodeBase64() to throw on invalid input rather than silently accept it (#7019)
1 parent f741ac1 commit c59f537

File tree

8 files changed

+90
-12
lines changed

8 files changed

+90
-12
lines changed

.changeset/popular-apples-peel.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@firebase/firestore': patch
3+
'@firebase/util': patch
4+
'firebase': patch
5+
---
6+
7+
Modify base64 decoding logic to throw on invalid input, rather than silently truncating it.

common/api-review/util.api.md

+8
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,14 @@ export function createSubscribe<T>(executor: Executor<T>, onNoObservers?: Execut
9393
// @public
9494
export const decode: (token: string) => DecodedToken;
9595

96+
// Warning: (ae-missing-release-tag) "DecodeBase64StringError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
97+
//
98+
// @public
99+
export class DecodeBase64StringError extends Error {
100+
// (undocumented)
101+
readonly name = "DecodeBase64StringError";
102+
}
103+
96104
// Warning: (ae-missing-release-tag) "deepCopy" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
97105
//
98106
// @public

packages/firestore/src/platform/base64.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,26 @@
1515
* limitations under the License.
1616
*/
1717

18+
import { Base64DecodeError } from '../util/base64_decode_error';
19+
1820
// This file is only used under ts-node.
1921
// eslint-disable-next-line @typescript-eslint/no-require-imports
2022
const platform = require(`./${process.env.TEST_PLATFORM ?? 'node'}/base64`);
2123

2224
/** Converts a Base64 encoded string to a binary string. */
2325
export function decodeBase64(encoded: string): string {
24-
return platform.decodeBase64(encoded);
26+
const decoded = platform.decodeBase64(encoded);
27+
28+
// A quick correctness check that the input string was valid base64.
29+
// See https://stackoverflow.com/questions/13378815/base64-length-calculation.
30+
// This is done because node and rn will not always throw an error if the
31+
// input is an invalid base64 string (e.g. "A===").
32+
const expectedEncodedLength = 4 * Math.ceil(decoded.length / 3);
33+
if (encoded.length !== expectedEncodedLength) {
34+
throw new Base64DecodeError('Invalid base64 string');
35+
}
36+
37+
return decoded;
2538
}
2639

2740
/** Converts a binary string to a Base64 encoded string. */

packages/firestore/src/platform/browser/base64.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,19 @@
1515
* limitations under the License.
1616
*/
1717

18+
import { Base64DecodeError } from '../../util/base64_decode_error';
19+
1820
/** Converts a Base64 encoded string to a binary string. */
1921
export function decodeBase64(encoded: string): string {
20-
return atob(encoded);
22+
try {
23+
return atob(encoded);
24+
} catch (e) {
25+
if (e instanceof DOMException) {
26+
throw new Base64DecodeError('Invalid base64 string: ' + e);
27+
} else {
28+
throw e;
29+
}
30+
}
2131
}
2232

2333
/** Converts a binary string to a Base64 encoded string. */

packages/firestore/src/platform/rn/base64.ts

+18-8
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,31 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { base64 } from '@firebase/util';
18+
import { base64, DecodeBase64StringError } from '@firebase/util';
19+
20+
import { Base64DecodeError } from '../../util/base64_decode_error';
1921

2022
// WebSafe uses a different URL-encoding safe alphabet that doesn't match
2123
// the encoding used on the backend.
2224
const WEB_SAFE = false;
2325

2426
/** Converts a Base64 encoded string to a binary string. */
2527
export function decodeBase64(encoded: string): string {
26-
return String.fromCharCode.apply(
27-
null,
28-
// We use `decodeStringToByteArray()` instead of `decodeString()` since
29-
// `decodeString()` returns Unicode strings, which doesn't match the values
30-
// returned by `atob()`'s Latin1 representation.
31-
base64.decodeStringToByteArray(encoded, WEB_SAFE)
32-
);
28+
try {
29+
return String.fromCharCode.apply(
30+
null,
31+
// We use `decodeStringToByteArray()` instead of `decodeString()` since
32+
// `decodeString()` returns Unicode strings, which doesn't match the values
33+
// returned by `atob()`'s Latin1 representation.
34+
base64.decodeStringToByteArray(encoded, WEB_SAFE)
35+
);
36+
} catch (e) {
37+
if (e instanceof DecodeBase64StringError) {
38+
throw new Base64DecodeError('Invalid base64 string: ' + e);
39+
} else {
40+
throw e;
41+
}
42+
}
3343
}
3444

3545
/** Converts a binary string to a Base64 encoded string. */
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* @license
3+
* Copyright 2023 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
/**
19+
* An error encountered while decoding base64 string.
20+
*/
21+
export class Base64DecodeError extends Error {
22+
readonly name = 'Base64DecodeError';
23+
}

packages/firestore/test/lite/integration.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -857,7 +857,7 @@ describe('DocumentSnapshot', () => {
857857

858858
it('returns Bytes', () => {
859859
return withTestDocAndInitialData(
860-
{ bytes: Bytes.fromBase64String('aa') },
860+
{ bytes: Bytes.fromBase64String('aa==') },
861861
async docRef => {
862862
const docSnap = await getDoc(docRef);
863863
const bytes = docSnap.get('bytes');

packages/util/src/crypt.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ export const base64: Base64 = {
284284
++i;
285285

286286
if (byte1 == null || byte2 == null || byte3 == null || byte4 == null) {
287-
throw Error();
287+
throw new DecodeBase64StringError();
288288
}
289289

290290
const outByte1 = (byte1 << 2) | (byte2 >> 4);
@@ -333,6 +333,13 @@ export const base64: Base64 = {
333333
}
334334
};
335335

336+
/**
337+
* An error encountered while decoding base64 string.
338+
*/
339+
export class DecodeBase64StringError extends Error {
340+
readonly name = 'DecodeBase64StringError';
341+
}
342+
336343
/**
337344
* URL-safe base64 encoding
338345
*/

0 commit comments

Comments
 (0)