no-predictable-session-id
Disallow an MCP sessionIdGenerator that returns a counter, a timestamp, Math.random() or a constant.
CWE: CWE-330 OWASP: A02:2021 – Cryptographic Failures
Detects a Streamable HTTP transport whose sessionIdGenerator builds the session ID only from guessable sources, or returns the same value every time. This rule is part of eslint-plugin-mcp-sdk-security.
💼 This rule is set to error in the strict config.
Quick Summary
| Aspect | Details |
|---|---|
| CWE Reference | CWE-330 (Insufficiently Random Values) |
| Severity | High (CVSS 7.5) |
| Auto-Fix | ❌ No auto-fix available |
| Category | Security |
| messageId | What it reports |
|---|---|
predictableSessionId | IDs built only from timestamps, counters, Math.random(), literals |
constantSessionId | The same ID for every session, so every client shares one session |
Why this matters
In stateful mode the Mcp-Session-Id header is the only thing that ties a
request to a session: the server routes the request to whichever transport
owns that ID. Anyone who can produce a valid ID can send requests into
another client's session, read its notifications, and call tools under its
identity. A counter or a timestamp can be guessed from one observed ID, and
Math.random() is a PRNG whose state can be recovered from its outputs. A
constant puts every client in one session.
❌ Incorrect
// ❌ a counter
let n = 0;
new StreamableHTTPServerTransport({ sessionIdGenerator: () => String(n++) });
// ❌ a timestamp, or Math.random()
new StreamableHTTPServerTransport({
sessionIdGenerator: () => Date.now().toString(36),
});
new StreamableHTTPServerTransport({
sessionIdGenerator: () => Math.random().toString(36).slice(2),
});
// ❌ constant: every client shares one session
new StreamableHTTPServerTransport({ sessionIdGenerator: () => 'session' });
// ❌ random, but computed once at module level: still the same for every session
const ID = randomUUID();
new StreamableHTTPServerTransport({ sessionIdGenerator: () => ID });How it recognises things
- Transports are resolved to the SDK:
StreamableHTTPServerTransport(v1),NodeStreamableHTTPServerTransport(@modelcontextprotocol/node) andWebStandardStreamableHTTPServerTransport(@modelcontextprotocol/server), with their options written inline. - The generator is an inline function, a method shorthand, or a same-file
function passed by name. Every value it returns is classified, and so is the
return value of a same-file helper it calls.
sessionIdGenerator: Date.nowpassed by reference is reported too. - Predictable sources are
Date.now(),new Date(),performance.now(),process.hrtime()/process.hrtime.bigint(),Math.random(), an update expression (n++,++n), and literals. Each global counts only when no local binding shadows it. Templates,+,String(),Math.floor/round/truncand string, number or date methods (toString,toFixed,toISOString,getTime,valueOf,slice,substring,substr,padStart,padEnd,replace,replaceAll,toUpperCase,toLowerCase,trim,join) keep a value predictable. Aconstdeclared inside a function is followed to its initializer. A module-levelconstis evaluated once, so it is constant. - CSPRNG sources are
randomUUID,randomBytesandgetRandomValuesfromcrypto, the globalcrypto.randomUUID()andcrypto.getRandomValues(),v4andv7fromuuid, andnanoidfromnanoid. A returned value that contains one anywhere is not reported.
✅ Correct
import { randomUUID } from 'node:crypto';
new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() });
new StreamableHTTPServerTransport({ sessionIdGenerator: randomUUID });
new StreamableHTTPServerTransport({
sessionIdGenerator: () => crypto.randomUUID(),
});What this rule deliberately does not report
- A generator it cannot see into. An imported function, a method on an
object (
ids.next()), aletread without an update, an environment variable, or a conditional. The rule abstains rather than guess. - Options it cannot read: options in a variable, with a spread, or with a computed key.
- A per-request value closed over by the generator:
const id = randomUUID()inside the route handler is new for each transport. SSEServerTransport, which generates its own random session ID.- Non-MCP classes of the same name, and a file that imports no MCP server package.
Overlap with node-security / secure-coding
node-security/no-math-random-crypto reports Math.random() where a nearby
identifier names a security value, and decides by that name. secure-coding
has no randomness rule. This rule owns the MCP-specific question, whether the
value this transport hands out as a session ID can be guessed or is shared,
and decides it from the SDK option rather than a name. It also reports
counters, timestamps and constants, which no generic rule can tie to a session
ID. On a Math.random() generator both plugins may report.
When Not To Use It
There is no configuration in which a guessable or shared session ID is correct, so this rule has no options.
Further Reading
- CWE-330: Use of Insufficiently Random Values
- MCP: Security best practices, session hijacking
- Node.js:
crypto.randomUUID
Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.