-
Notifications
You must be signed in to change notification settings - Fork 1.8k
test(server): structural assertion that dispatch path holds no Map<RequestId,> state #2075
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: fweinberger/r3-handlehttp
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import { readFileSync } from 'node:fs'; | ||
| import { dirname, join } from 'node:path'; | ||
| import { fileURLToPath } from 'node:url'; | ||
|
|
||
| import { describe, expect, test } from 'vitest'; | ||
|
|
||
| /** | ||
| * Structural guard for GHSA-345p (cross-tenant routing via id-keyed shared state). | ||
| * | ||
| * The original vulnerability shape was a `Map<RequestId, ...>` (or equivalent) on a | ||
| * long-lived object that routes per-request data. The `dispatch(req, env)` path yields | ||
| * its outputs into a per-call iterator with no id-keyed routing map; this test asserts | ||
| * that property structurally so a future change cannot quietly reintroduce the shape. | ||
| * | ||
| * Intentionally NOT covered: `StreamDriver._responseHandlers` (outbound correlation for | ||
| * requests this side initiates over a persistent channel) is a `Map<RequestId, ...>` but | ||
| * is not in the inbound dispatch path and is not the GHSA shape. `shttpHandler` keys its | ||
| * abort map on `(sessionId, requestId)`, not bare `RequestId`. | ||
| */ | ||
| describe('GHSA-345p structural guard: no id-keyed maps in inbound dispatch path', () => { | ||
| const here = dirname(fileURLToPath(import.meta.url)); | ||
| const files = [ | ||
| join(here, '../../core/src/shared/dispatcher.ts'), | ||
| join(here, '../src/server/mcp.ts'), | ||
| join(here, '../src/server/shttpHandler.ts') | ||
| ]; | ||
|
|
||
| /** | ||
| * Matches the field-declaration shapes that produced GHSA-345p: | ||
| * `: Map<RequestId, ...>` | ||
| * `new Map<RequestId, ...>` | ||
| * `: Map<string | number, ...>` (RequestId's underlying union) | ||
| * `Record<RequestId, ...>` | ||
| */ | ||
| const idKeyedMapPattern = /(?::\s*|new\s+)Map<\s*(?:RequestId|string\s*\|\s*number)\b|Record<\s*RequestId\b/; | ||
|
|
||
| for (const file of files) { | ||
| test(`${file.split('/packages/')[1]} has no Map<RequestId, ...> field declarations`, () => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 nit: Extended reasoning...What happens. Line 38 derives the test title with Step-by-step on Windows:
Why nothing else prevents it. Impact. Cosmetic only. Every workflow in Fix. Easiest is to keep the relative strings as the source of truth: const files = [
'core/src/shared/dispatcher.ts',
'server/src/server/mcp.ts',
'server/src/server/shttpHandler.ts'
];
for (const rel of files) {
test(`${rel} has no Map<RequestId, ...> field declarations`, () => {
const src = readFileSync(join(here, '../..', rel), 'utf8');
...
});
}Alternatively split on a separator-agnostic regex ( |
||
| const src = readFileSync(file, 'utf8'); | ||
| const matches: string[] = []; | ||
| for (const [i, line] of src.split('\n').entries()) { | ||
| if (idKeyedMapPattern.test(line)) { | ||
| matches.push(` ${i + 1}: ${line.trim()}`); | ||
| } | ||
| } | ||
| expect(matches, `id-keyed map declarations found:\n${matches.join('\n')}`).toEqual([]); | ||
| }); | ||
| } | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 The "Intentionally NOT covered" comment is factually wrong against current
streamDriver.ts:_responseHandlersisMap<number, ...>(wouldn't match this regex anyway), while the field that would match —_requestHandlerAbortControllers: Map<RequestId, AbortController>— is populated in_onrequest(), so "not in the inbound dispatch path" doesn't hold. The actual reason StreamDriver is safe to exclude is that it's instantiated per-connection, so its id-keyed maps can't cross tenants — worth stating that instead, since for a GHSA regression guard the exclusion rationale is what future maintainers will lean on.Extended reasoning...
What's wrong. The doc-comment block at lines 15–18 explains why
StreamDriveris excluded from the structural scan, but both the field it names and the rationale it gives don't matchpackages/core/src/shared/streamDriver.ts:StreamDriver._responseHandlers"is aMap<RequestId, ...>". It isn't — line 85 declares it asMap<number, (response: ...) => void>. The test regex/(?::\s*|new\s+)Map<\s*(?:RequestId|string\s*\|\s*number)\b/only matchesRequestIdor the literal unionstring | number, so a bareMap<number,would never trip the assertion even ifstreamDriver.tswere added to thefilesarray. Citing_responseHandlersas the reason for exclusion is moot._requestHandlerAbortControllers: Map<RequestId, AbortController>(line 88), and that one is populated inside_onrequest()(line 259) and consumed by inboundnotifications/cancelledhandling (line 297) — i.e. it is squarely in the inbound request-handling path.Step-by-step proof. Suppose a maintainer reads the comment, then greps StreamDriver to verify the exclusion is still valid:
_responseHandlers→ findsMap<number, ...>at L85. Type doesn't match what the comment claims; regex wouldn't catch it regardless. Comment claim (1) is false.Map<RequestIdin streamDriver.ts → finds_requestHandlerAbortControllersat L88. This does match/(?::\s*|new\s+)Map<\s*RequestId\b/._onrequest(request)at L259 doesthis._requestHandlerAbortControllers.set(request.id, abort)._onrequestis the inbound request handler. Comment claim (2) ("not in the inbound dispatch path") is false for the field that actually matches.So the comment names the wrong field, states the wrong type, and gives a rationale that contradicts the one matching declaration.
Why nothing else fixes it. The test body never reads
streamDriver.ts, so the assertions are unaffected — this is purely a prose error in the explanatory header. But for a GHSA regression-guard test, the exclusion rationale is the load-bearing documentation: it's what someone will read before deciding whether it's safe to add or drop a file from the scan. As written it will send them on a goose chase and force them to re-derive the real argument.Correct rationale. StreamDriver is safe not because its id-keyed map is outbound-only (it isn't), but because a
StreamDriveris instantiated per connection/transport — its maps are scoped to a single peer, so id collisions cannot cross tenants. That's a different (and stronger) argument than the one stated, and it's also the argument the PR description itself makes ("Per-connection correlation maps belong inStreamDriver(one instance per connection)").Suggested fix. Replace lines 15–18 with something like:
Severity. Nit — comment-only, no runtime or test-assertion impact.