Skip to content

Commit 3a41fc7

Browse files
committed
Fix prototype pollution
1 parent dfdb1d9 commit 3a41fc7

2 files changed

Lines changed: 65 additions & 2 deletions

File tree

src/services/wasm/wasm.spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,54 @@ describe("WasmScopeAbi", () => {
128128
expect(rootScope.filters.status).toBeUndefined();
129129
});
130130

131+
it("rejects prototype-polluting write paths", () => {
132+
const scope = abi.createScope(rootScope, { name: "todoList:main" });
133+
const value = guest.writeJson("polluted");
134+
const poisoned = [
135+
"__proto__.wasmPolluted",
136+
"safe.constructor.prototype.wasmPolluted",
137+
"safe.prototype.wasmPolluted",
138+
];
139+
140+
for (const pathValue of poisoned) {
141+
const path = guest.write(pathValue);
142+
143+
expect(
144+
imports.scope_set(
145+
scope.handle,
146+
path.ptr,
147+
path.len,
148+
value.ptr,
149+
value.len,
150+
),
151+
).toBe(0);
152+
}
153+
154+
expect(rootScope.safe).toBeUndefined();
155+
expect(({} as Record<string, unknown>).wasmPolluted).toBeUndefined();
156+
});
157+
158+
it("rejects prototype-polluting read and delete paths", () => {
159+
const scope = abi.createScope(rootScope, { name: "todoList:main" });
160+
const path = guest.write("__proto__.wasmPolluted");
161+
162+
(Object.prototype as Record<string, unknown>).wasmPolluted = "inherited";
163+
164+
try {
165+
const result = imports.scope_get(scope.handle, path.ptr, path.len);
166+
167+
expect(
168+
guest.readJson(imports.buffer_ptr(result), imports.buffer_len(result)),
169+
).toBeNull();
170+
expect(imports.scope_delete(scope.handle, path.ptr, path.len)).toBe(0);
171+
expect(({} as Record<string, unknown>).wasmPolluted).toBe("inherited");
172+
173+
imports.buffer_free(result);
174+
} finally {
175+
delete (Object.prototype as Record<string, unknown>).wasmPolluted;
176+
}
177+
});
178+
131179
it("runs bridge sync callbacks asynchronously", async () => {
132180
const scope = abi.createScope(rootScope, { name: "todoList:main" });
133181
const name = guest.write("todoList:main");

src/services/wasm/wasm.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ const textEncoder = new TextEncoder();
66

77
const textDecoder = new TextDecoder();
88

9+
const UNSAFE_SCOPE_PATH_KEYS = new Set([
10+
"__proto__",
11+
"constructor",
12+
"prototype",
13+
]);
14+
915
/** Options for loading a WebAssembly module through `$wasm`. */
1016
export interface WasmOptions {
1117
/**
@@ -895,6 +901,11 @@ function readScopePath(scope: ng.Scope, path: string): unknown {
895901
}
896902

897903
const keys = scopePathKeys(path);
904+
905+
if (!isSafeScopePath(keys)) {
906+
return undefined;
907+
}
908+
898909
let current: unknown = scope;
899910

900911
for (let i = 0, l = keys.length; i < l; i++) {
@@ -915,7 +926,7 @@ function writeScopePath(
915926
): boolean {
916927
const keys = scopePathKeys(path);
917928

918-
if (keys.length === 0) {
929+
if (keys.length === 0 || !isSafeScopePath(keys)) {
919930
return false;
920931
}
921932

@@ -944,7 +955,7 @@ function writeScopePath(
944955
function deleteScopePath(scope: ng.Scope, path: string): boolean {
945956
const keys = scopePathKeys(path);
946957

947-
if (keys.length === 0) {
958+
if (keys.length === 0 || !isSafeScopePath(keys)) {
948959
return false;
949960
}
950961

@@ -966,3 +977,7 @@ function deleteScopePath(scope: ng.Scope, path: string): boolean {
966977
function scopePathKeys(path: string): string[] {
967978
return path.split(".").filter(Boolean);
968979
}
980+
981+
function isSafeScopePath(keys: string[]): boolean {
982+
return keys.every((key) => !UNSAFE_SCOPE_PATH_KEYS.has(key));
983+
}

0 commit comments

Comments
 (0)