Skip to content

Commit 5163b1b

Browse files
authored
🔒 Fixes prototype exposure (#22)
* 🔒 Fixes prototype pollution issue * 🔒 Tests for protoype and constructor * 🐛 Updates pnpm action
1 parent c98cb93 commit 5163b1b

File tree

5 files changed

+2952
-2484
lines changed

5 files changed

+2952
-2484
lines changed

.github/workflows/test-pr.yml

+4-4
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
with:
2929
node-version: 20
3030

31-
- uses: pnpm/action-setup@v2.4.0
31+
- uses: pnpm/action-setup@v4
3232
name: Install pnpm
3333
id: pnpm-install
3434
with:
@@ -72,7 +72,7 @@ jobs:
7272
with:
7373
node-version: 20
7474

75-
- uses: pnpm/action-setup@v2.4.0
75+
- uses: pnpm/action-setup@v4
7676
name: Install pnpm
7777
id: pnpm-install
7878
with:
@@ -119,7 +119,7 @@ jobs:
119119
with:
120120
node-version: 20
121121

122-
- uses: pnpm/action-setup@v2.4.0
122+
- uses: pnpm/action-setup@v4
123123
name: Install pnpm
124124
id: pnpm-install
125125
with:
@@ -163,7 +163,7 @@ jobs:
163163
with:
164164
node-version: 20
165165

166-
- uses: pnpm/action-setup@v2.4.0
166+
- uses: pnpm/action-setup@v4
167167
name: Install pnpm
168168
id: pnpm-install
169169
with:

packages/params/src/pathresolve.ts

+13-7
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ export const objectAtPath = (
55
) => {
66
while (keys.length) {
77
const key = keys.shift()!;
8+
if (isForbidden(key)) return null;
89

910
// This is where we fill in empty arrays/objects allong the way to the assigment...
1011
if (data[key] === undefined)
11-
data[key] = isNaN(Number(keys[0] ?? final)) ? {} : [];
12+
data[key] = isNaN(Number(keys[0] ?? final)) ? Object.create({}) : [];
1213
data = data[key] as Record<string, unknown>;
1314
// Keep deferring assignment until the full key is built up...
1415
}
@@ -22,8 +23,10 @@ export const insertDotNotatedValueIntoData = (
2223
) => {
2324
const keys = path.split('.');
2425
const final = keys.pop()!;
25-
data = objectAtPath(keys, data, final);
26-
data[final] = value;
26+
const interimdata = objectAtPath(keys, data, final);
27+
return !interimdata || isForbidden(final)
28+
? undefined
29+
: (interimdata[final] = value);
2730
};
2831

2932
export const retrieveDotNotatedValueFromData = (
@@ -32,8 +35,8 @@ export const retrieveDotNotatedValueFromData = (
3235
) => {
3336
const keys = path.split('.');
3437
const final = keys.pop()!;
35-
data = objectAtPath(keys, data, final);
36-
return data[final];
38+
const interimdata = objectAtPath(keys, data, final);
39+
return !interimdata || isForbidden(final) ? undefined : interimdata[final];
3740
};
3841

3942
export const deleteDotNotatedValueFromData = (
@@ -42,8 +45,8 @@ export const deleteDotNotatedValueFromData = (
4245
) => {
4346
const keys = path.split('.');
4447
const final = keys.pop()!;
45-
data = objectAtPath(keys, data, final);
46-
delete data[final];
48+
const interimdata = objectAtPath(keys, data, final);
49+
if (interimdata && !isForbidden(final)) delete data[final];
4750
};
4851

4952
if (import.meta.vitest) {
@@ -74,3 +77,6 @@ if (import.meta.vitest) {
7477
});
7578
});
7679
}
80+
81+
export const isForbidden = (key: string) => forbiddenKeys.includes(key);
82+
const forbiddenKeys = ['__proto__', 'constructor', 'prototype'];

packages/params/src/querystring.ts

+28-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { insertDotNotatedValueIntoData } from './pathresolve';
1+
import { insertDotNotatedValueIntoData, isForbidden } from './pathresolve';
22

33
/**
44
* Converts Objects to bracketed query strings
@@ -36,12 +36,13 @@ export const buildQueryStringEntries = (
3636
};
3737

3838
export const fromQueryString = (queryString: string) => {
39-
const data: Record<string, unknown> = {};
39+
const data: Record<string, unknown> = Object.create(null);
4040
if (queryString === '') return data;
4141

4242
const entries = new URLSearchParams(queryString).entries();
4343

4444
for (const [key, value] of entries) {
45+
if (isForbidden(key)) continue;
4546
// Query string params don't always have values... (`?foo=`)
4647
if (!value) continue;
4748

@@ -81,5 +82,30 @@ if (import.meta.vitest) {
8182
expect(fromQueryString(str)).toEqual(obj);
8283
},
8384
);
85+
it.each(['__proto__', 'constructor', 'prototype'])(
86+
'doesnt parse %s',
87+
(key) => {
88+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
89+
const thing: any = fromQueryString(`hello=world&${key}[fizz]=bar`);
90+
expect(thing).toEqual({ hello: `world` });
91+
expect(thing[key]?.fizz).toBeUndefined();
92+
expect(fromQueryString(`foo[${key}]=bar&hello=world`)).toEqual({
93+
foo: {},
94+
hello: `world`,
95+
});
96+
expect(
97+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
98+
(fromQueryString(`foo[${key}]=bar&hello=world`) as any).foo?.[key],
99+
).not.toEqual(`bar`);
100+
expect(
101+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
102+
(fromQueryString(`foo[0]=test&foo[${key}]=bar&hello=world`) as any)
103+
.foo?.[key],
104+
).not.toEqual(`bar`);
105+
expect(
106+
fromQueryString(`foo[0]=test&foo[${key}]=bar&hello=world`),
107+
).toEqual({ foo: [`test`], hello: `world` });
108+
},
109+
);
84110
});
85111
}

0 commit comments

Comments
 (0)