Skip to main content
interlace
Plugin: mcp-sdk-securityRules

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

AspectDetails
CWE ReferenceCWE-330 (Insufficiently Random Values)
SeverityHigh (CVSS 7.5)
Auto-Fix❌ No auto-fix available
CategorySecurity
messageIdWhat it reports
predictableSessionIdIDs built only from timestamps, counters, Math.random(), literals
constantSessionIdThe 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) and WebStandardStreamableHTTPServerTransport (@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.now passed 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/trunc and 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. A const declared inside a function is followed to its initializer. A module-level const is evaluated once, so it is constant.
  • CSPRNG sources are randomUUID, randomBytes and getRandomValues from crypto, the global crypto.randomUUID() and crypto.getRandomValues(), v4 and v7 from uuid, and nanoid from nanoid. 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()), a let read 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

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.