Skip to content

Commit

Permalink
🔒 Fixes prototype exposure (#22)
Browse files Browse the repository at this point in the history
* 🔒 Fixes prototype pollution issue

* 🔒 Tests for protoype and constructor

* 🐛 Updates pnpm action
  • Loading branch information
ekwoka authored Jul 29, 2024
1 parent c98cb93 commit 5163b1b
Show file tree
Hide file tree
Showing 5 changed files with 2,952 additions and 2,484 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/test-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
with:
node-version: 20

- uses: pnpm/action-setup@v2.4.0
- uses: pnpm/action-setup@v4
name: Install pnpm
id: pnpm-install
with:
Expand Down Expand Up @@ -72,7 +72,7 @@ jobs:
with:
node-version: 20

- uses: pnpm/action-setup@v2.4.0
- uses: pnpm/action-setup@v4
name: Install pnpm
id: pnpm-install
with:
Expand Down Expand Up @@ -119,7 +119,7 @@ jobs:
with:
node-version: 20

- uses: pnpm/action-setup@v2.4.0
- uses: pnpm/action-setup@v4
name: Install pnpm
id: pnpm-install
with:
Expand Down Expand Up @@ -163,7 +163,7 @@ jobs:
with:
node-version: 20

- uses: pnpm/action-setup@v2.4.0
- uses: pnpm/action-setup@v4
name: Install pnpm
id: pnpm-install
with:
Expand Down
20 changes: 13 additions & 7 deletions packages/params/src/pathresolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ export const objectAtPath = (
) => {
while (keys.length) {
const key = keys.shift()!;
if (isForbidden(key)) return null;

// This is where we fill in empty arrays/objects allong the way to the assigment...
if (data[key] === undefined)
data[key] = isNaN(Number(keys[0] ?? final)) ? {} : [];
data[key] = isNaN(Number(keys[0] ?? final)) ? Object.create({}) : [];
data = data[key] as Record<string, unknown>;
// Keep deferring assignment until the full key is built up...
}
Expand All @@ -22,8 +23,10 @@ export const insertDotNotatedValueIntoData = (
) => {
const keys = path.split('.');
const final = keys.pop()!;
data = objectAtPath(keys, data, final);
data[final] = value;
const interimdata = objectAtPath(keys, data, final);
return !interimdata || isForbidden(final)
? undefined
: (interimdata[final] = value);
};

export const retrieveDotNotatedValueFromData = (
Expand All @@ -32,8 +35,8 @@ export const retrieveDotNotatedValueFromData = (
) => {
const keys = path.split('.');
const final = keys.pop()!;
data = objectAtPath(keys, data, final);
return data[final];
const interimdata = objectAtPath(keys, data, final);
return !interimdata || isForbidden(final) ? undefined : interimdata[final];
};

export const deleteDotNotatedValueFromData = (
Expand All @@ -42,8 +45,8 @@ export const deleteDotNotatedValueFromData = (
) => {
const keys = path.split('.');
const final = keys.pop()!;
data = objectAtPath(keys, data, final);
delete data[final];
const interimdata = objectAtPath(keys, data, final);
if (interimdata && !isForbidden(final)) delete data[final];
};

if (import.meta.vitest) {
Expand Down Expand Up @@ -74,3 +77,6 @@ if (import.meta.vitest) {
});
});
}

export const isForbidden = (key: string) => forbiddenKeys.includes(key);
const forbiddenKeys = ['__proto__', 'constructor', 'prototype'];
30 changes: 28 additions & 2 deletions packages/params/src/querystring.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { insertDotNotatedValueIntoData } from './pathresolve';
import { insertDotNotatedValueIntoData, isForbidden } from './pathresolve';

/**
* Converts Objects to bracketed query strings
Expand Down Expand Up @@ -36,12 +36,13 @@ export const buildQueryStringEntries = (
};

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

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

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

Expand Down Expand Up @@ -81,5 +82,30 @@ if (import.meta.vitest) {
expect(fromQueryString(str)).toEqual(obj);
},
);
it.each(['__proto__', 'constructor', 'prototype'])(
'doesnt parse %s',
(key) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const thing: any = fromQueryString(`hello=world&${key}[fizz]=bar`);
expect(thing).toEqual({ hello: `world` });
expect(thing[key]?.fizz).toBeUndefined();
expect(fromQueryString(`foo[${key}]=bar&hello=world`)).toEqual({
foo: {},
hello: `world`,
});
expect(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(fromQueryString(`foo[${key}]=bar&hello=world`) as any).foo?.[key],
).not.toEqual(`bar`);
expect(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(fromQueryString(`foo[0]=test&foo[${key}]=bar&hello=world`) as any)
.foo?.[key],
).not.toEqual(`bar`);
expect(
fromQueryString(`foo[0]=test&foo[${key}]=bar&hello=world`),
).toEqual({ foo: [`test`], hello: `world` });
},
);
});
}
Loading

0 comments on commit 5163b1b

Please sign in to comment.