Skip to content

Commit

Permalink
Improve decodeBase64() to throw on invalid input rather than silently…
Browse files Browse the repository at this point in the history
… accept it (#7019)
  • Loading branch information
dconeybe authored Feb 11, 2023
1 parent f741ac1 commit c59f537
Show file tree
Hide file tree
Showing 8 changed files with 90 additions and 12 deletions.
7 changes: 7 additions & 0 deletions .changeset/popular-apples-peel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@firebase/firestore': patch
'@firebase/util': patch
'firebase': patch
---

Modify base64 decoding logic to throw on invalid input, rather than silently truncating it.
8 changes: 8 additions & 0 deletions common/api-review/util.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,14 @@ export function createSubscribe<T>(executor: Executor<T>, onNoObservers?: Execut
// @public
export const decode: (token: string) => DecodedToken;

// Warning: (ae-missing-release-tag) "DecodeBase64StringError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export class DecodeBase64StringError extends Error {
// (undocumented)
readonly name = "DecodeBase64StringError";
}

// Warning: (ae-missing-release-tag) "deepCopy" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
Expand Down
15 changes: 14 additions & 1 deletion packages/firestore/src/platform/base64.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,26 @@
* limitations under the License.
*/

import { Base64DecodeError } from '../util/base64_decode_error';

// This file is only used under ts-node.
// eslint-disable-next-line @typescript-eslint/no-require-imports
const platform = require(`./${process.env.TEST_PLATFORM ?? 'node'}/base64`);

/** Converts a Base64 encoded string to a binary string. */
export function decodeBase64(encoded: string): string {
return platform.decodeBase64(encoded);
const decoded = platform.decodeBase64(encoded);

// A quick correctness check that the input string was valid base64.
// See https://stackoverflow.com/questions/13378815/base64-length-calculation.
// This is done because node and rn will not always throw an error if the
// input is an invalid base64 string (e.g. "A===").
const expectedEncodedLength = 4 * Math.ceil(decoded.length / 3);
if (encoded.length !== expectedEncodedLength) {
throw new Base64DecodeError('Invalid base64 string');
}

return decoded;
}

/** Converts a binary string to a Base64 encoded string. */
Expand Down
12 changes: 11 additions & 1 deletion packages/firestore/src/platform/browser/base64.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,19 @@
* limitations under the License.
*/

import { Base64DecodeError } from '../../util/base64_decode_error';

/** Converts a Base64 encoded string to a binary string. */
export function decodeBase64(encoded: string): string {
return atob(encoded);
try {
return atob(encoded);
} catch (e) {
if (e instanceof DOMException) {
throw new Base64DecodeError('Invalid base64 string: ' + e);
} else {
throw e;
}
}
}

/** Converts a binary string to a Base64 encoded string. */
Expand Down
26 changes: 18 additions & 8 deletions packages/firestore/src/platform/rn/base64.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,31 @@
* limitations under the License.
*/

import { base64 } from '@firebase/util';
import { base64, DecodeBase64StringError } from '@firebase/util';

import { Base64DecodeError } from '../../util/base64_decode_error';

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

/** Converts a Base64 encoded string to a binary string. */
export function decodeBase64(encoded: string): string {
return String.fromCharCode.apply(
null,
// We use `decodeStringToByteArray()` instead of `decodeString()` since
// `decodeString()` returns Unicode strings, which doesn't match the values
// returned by `atob()`'s Latin1 representation.
base64.decodeStringToByteArray(encoded, WEB_SAFE)
);
try {
return String.fromCharCode.apply(
null,
// We use `decodeStringToByteArray()` instead of `decodeString()` since
// `decodeString()` returns Unicode strings, which doesn't match the values
// returned by `atob()`'s Latin1 representation.
base64.decodeStringToByteArray(encoded, WEB_SAFE)
);
} catch (e) {
if (e instanceof DecodeBase64StringError) {
throw new Base64DecodeError('Invalid base64 string: ' + e);
} else {
throw e;
}
}
}

/** Converts a binary string to a Base64 encoded string. */
Expand Down
23 changes: 23 additions & 0 deletions packages/firestore/src/util/base64_decode_error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* @license
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* An error encountered while decoding base64 string.
*/
export class Base64DecodeError extends Error {
readonly name = 'Base64DecodeError';
}
2 changes: 1 addition & 1 deletion packages/firestore/test/lite/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -857,7 +857,7 @@ describe('DocumentSnapshot', () => {

it('returns Bytes', () => {
return withTestDocAndInitialData(
{ bytes: Bytes.fromBase64String('aa') },
{ bytes: Bytes.fromBase64String('aa==') },
async docRef => {
const docSnap = await getDoc(docRef);
const bytes = docSnap.get('bytes');
Expand Down
9 changes: 8 additions & 1 deletion packages/util/src/crypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ export const base64: Base64 = {
++i;

if (byte1 == null || byte2 == null || byte3 == null || byte4 == null) {
throw Error();
throw new DecodeBase64StringError();
}

const outByte1 = (byte1 << 2) | (byte2 >> 4);
Expand Down Expand Up @@ -333,6 +333,13 @@ export const base64: Base64 = {
}
};

/**
* An error encountered while decoding base64 string.
*/
export class DecodeBase64StringError extends Error {
readonly name = 'DecodeBase64StringError';
}

/**
* URL-safe base64 encoding
*/
Expand Down

0 comments on commit c59f537

Please sign in to comment.