Skip to content

Commit 94eef6f

Browse files
committed
fix(plugin-node-server): harden query/params against prototype pollution (CodeQL)
CodeQL flagged remote property injection: query-param keys come straight from the request URL, so building the map on a plain object literal let '?__proto__=…' write through the prototype chain. Use null-prototype objects for query and params and drop the dangerous keys (__proto__/constructor/prototype) outright; regression test pins that '?__proto__=polluted' is dropped and Object.prototype stays clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1BtxNeUaGmvUYr8FwtUNu
1 parent d839b25 commit 94eef6f

2 files changed

Lines changed: 18 additions & 2 deletions

File tree

packages/plugins/plugin-node-server/src/adapter.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,15 @@ describe('NodeHttpServer (live socket)', () => {
140140
expect(await res.text()).toBe('');
141141
});
142142

143+
it('drops prototype-polluting query keys (CodeQL: remote property injection)', async () => {
144+
const res = await fetch(`${base}/echo/1?__proto__=polluted&constructor=x&a=ok`);
145+
expect(res.status).toBe(200);
146+
const body = await res.json();
147+
expect(body.q).toEqual({ a: 'ok' });
148+
// The global Object prototype must be untouched.
149+
expect(({} as any).polluted).toBeUndefined();
150+
});
151+
143152
it('exposes the Host header natively (no backfill needed)', async () => {
144153
server.get('/host', (req, res) => { res.json({ host: req.headers.host }); });
145154
const res = await fetch(`${base}/host`);

packages/plugins/plugin-node-server/src/adapter.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,9 @@ export class NodeHttpServer implements IHttpServer {
160160
if (route.method !== effective) continue;
161161
const m = route.regex.exec(normalized);
162162
if (!m) continue;
163-
const params: Record<string, string> = {};
163+
// Null-prototype for symmetry with `query` — keys come from the
164+
// registered pattern (developer-controlled), values from the URL.
165+
const params: Record<string, string> = Object.create(null);
164166
route.keys.forEach((k, i) => { params[k] = decodeURIComponent(m[i + 1]); });
165167
return { route, params };
166168
}
@@ -198,8 +200,13 @@ export class NodeHttpServer implements IHttpServer {
198200
}
199201

200202
// ── Query params (multi-value aware) ────────────────────────────────
201-
const query: Record<string, string | string[]> = {};
203+
// Null-prototype object + dangerous-key filter: the property names
204+
// come straight from the request, so a plain `{}` would be open to
205+
// prototype pollution via `?__proto__=…` (CodeQL: remote property
206+
// injection).
207+
const query: Record<string, string | string[]> = Object.create(null);
202208
for (const key of url.searchParams.keys()) {
209+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
203210
if (key in query) continue;
204211
const all = url.searchParams.getAll(key);
205212
query[key] = all.length > 1 ? all : all[0];

0 commit comments

Comments
 (0)