mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(core): prevent TransferState prototype pollution
Store transfer state and serialization callbacks in null-prototype dictionaries, and only read values that belong to the store. This keeps special state keys from changing lookup behavior or exposing inherited cache entries.
Fixes #70265
(cherry picked from commit 168a324cce)
This commit is contained in:
@@ -48,6 +48,11 @@ export function makeStateKey<T = void>(key: string): StateKey<T> {
|
||||
return key as StateKey<T>;
|
||||
}
|
||||
|
||||
function createDictionary<T>(): Record<string, T> {
|
||||
// StateKey can be any string, including names of special Object prototype properties.
|
||||
return Object.create(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* A key value store that is transferred from the application on the server side to the application
|
||||
* on the client side.
|
||||
@@ -79,15 +84,20 @@ export class TransferState {
|
||||
});
|
||||
|
||||
/** @internal */
|
||||
store: Record<string, unknown | undefined> = {};
|
||||
store: Record<string, unknown | undefined> = createDictionary();
|
||||
|
||||
private onSerializeCallbacks: {[k: string]: () => unknown | undefined} = {};
|
||||
private onSerializeCallbacks: {[k: string]: () => unknown | undefined} = createDictionary();
|
||||
|
||||
/**
|
||||
* Get the value corresponding to a key. Return `defaultValue` if key is not found.
|
||||
*/
|
||||
get<T>(key: StateKey<T>, defaultValue: T): T {
|
||||
return this.store[key] !== undefined ? (this.store[key] as T) : defaultValue;
|
||||
if (!Object.hasOwn(this.store, key)) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const value = this.store[key];
|
||||
return value !== undefined ? (value as T) : defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -159,11 +169,14 @@ export function retrieveTransferredState(
|
||||
// Avoid using any here as it triggers lint errors in google3 (any is not allowed).
|
||||
// Decoding of `<` is done of the box by browsers and node.js, same behaviour as G3
|
||||
// script_builders.
|
||||
return JSON.parse(script.textContent) as {};
|
||||
return Object.assign(
|
||||
createDictionary<unknown | undefined>(),
|
||||
JSON.parse(script.textContent) as {},
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn('Exception while restoring TransferState for app ' + appId, e);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
return createDictionary();
|
||||
}
|
||||
|
||||
@@ -435,6 +435,7 @@
|
||||
"createContainerAnchorImpl",
|
||||
"createDeferBlockInjector",
|
||||
"createDehydratedBlockRegistry",
|
||||
"createDictionary",
|
||||
"createDirectivesInstances",
|
||||
"createElementNode",
|
||||
"createElementRef",
|
||||
|
||||
@@ -91,6 +91,31 @@ describe('TransferState', () => {
|
||||
expect(transferState.hasKey(TEST_KEY)).toBe(true);
|
||||
});
|
||||
|
||||
it('does not read inherited properties', () => {
|
||||
const transferState = TestBed.inject(TransferState);
|
||||
const constructorKey = makeStateKey<unknown>('constructor');
|
||||
const store = (transferState as unknown as {store: Record<string, unknown>}).store;
|
||||
Object.setPrototypeOf(store, {constructor: 'inherited'});
|
||||
|
||||
expect(transferState.get(constructorKey, 'default')).toBe('default');
|
||||
expect(transferState.hasKey(constructorKey)).toBeFalse();
|
||||
});
|
||||
|
||||
it('supports keys that name object prototype properties', () => {
|
||||
const transferState = TestBed.inject(TransferState);
|
||||
const prototypeKey = makeStateKey<{cached: boolean}>('__proto__');
|
||||
const value = {cached: true};
|
||||
|
||||
transferState.set(prototypeKey, value);
|
||||
|
||||
expect(transferState.get(prototypeKey, {cached: false})).toBe(value);
|
||||
expect(transferState.hasKey(prototypeKey)).toBeTrue();
|
||||
|
||||
transferState.remove(prototypeKey);
|
||||
|
||||
expect(transferState.get(prototypeKey, null!)).toBeNull();
|
||||
});
|
||||
|
||||
it("supports setting and accessing value '0' via get", () => {
|
||||
const transferState: TransferState = TestBed.inject(TransferState);
|
||||
transferState.set(TEST_KEY, 0);
|
||||
@@ -137,6 +162,16 @@ describe('TransferState', () => {
|
||||
expect(transferState.toJson()).toBe('{"test":20,"delayed":"changed"}');
|
||||
});
|
||||
|
||||
it('calls onSerialize callbacks whose keys name object prototype properties', () => {
|
||||
const transferState = TestBed.inject(TransferState);
|
||||
const prototypeKey = makeStateKey<string>('__proto__');
|
||||
|
||||
transferState.onSerialize(prototypeKey, () => 'serialized');
|
||||
|
||||
expect(transferState.toJson()).toBe('{"__proto__":"serialized"}');
|
||||
expect(transferState.get(prototypeKey, null!)).toBe('serialized');
|
||||
});
|
||||
|
||||
it('should provide an ability to detect whether the state is empty', () => {
|
||||
const transferState = TestBed.inject(TransferState);
|
||||
|
||||
@@ -190,4 +225,14 @@ describe('TransferState', () => {
|
||||
expect(transferState.get(DELAYED_KEY, null)).toBe(relativeLink);
|
||||
expect(transferState.toJson()).toBe(encodedState);
|
||||
});
|
||||
|
||||
it('restores keys that name object prototype properties into a dictionary', () => {
|
||||
addScriptTag(doc, APP_ID, '{"__proto__":{"cached":true},"constructor":"state"}');
|
||||
const transferState = TestBed.inject(TransferState);
|
||||
|
||||
expect(transferState.get(makeStateKey<{cached: boolean}>('__proto__'), null)).toEqual({
|
||||
cached: true,
|
||||
});
|
||||
expect(transferState.get(makeStateKey<string>('constructor'), null)).toBe('state');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user