MCP Server Security
Regular expression security in MCP servers: ReDoS and safe patterns
A single regex applied to attacker-controlled input can hang your MCP server's event loop for seconds or minutes — blocking all concurrent tool calls and creating a denial-of-service condition. ReDoS is subtle because the vulnerable regex looks completely normal; the exploit is the input, not the code.
What makes a regex vulnerable to ReDoS
ReDoS (Regular Expression Denial of Service) exploits catastrophic backtracking: a regex engine tries exponentially many ways to match a carefully crafted string before failing. The classic trigger is a pattern with two overlapping repetition quantifiers applied to a common character class, combined with input that almost-but-not-quite matches:
// Vulnerable patterns — catastrophic backtracking
const EMAIL_PATTERN = /^([a-zA-Z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})$/
// Input: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@"
// The regex engine tries every partition of the 'a' sequence across the groups
// before deciding it can't match — O(2^n) attempts for n 'a' characters
const HTML_COMMENT = //s
// With nested loops: /^(a+)+$/ is the canonical catastrophic example
const NESTED_QUANTIFIER = /^(a+)+$/ // purely educational — never use this
// Input: "aaaaaaaaaaaaaaaaaaaaaaab" — exponential backtracking
The danger class is: nested quantifiers ((a+)+, (a*)*), alternation with overlap ((a|aa)+), and possessive or greedy quantifiers on a broad character class when the match can fail.
Why MCP servers are especially vulnerable
MCP tools often validate string inputs with regex — email addresses, URLs, search queries, identifiers. The LLM intermediary means an attacker can craft a tool argument that is syntactically valid (passes schema parsing) but designed to trigger catastrophic backtracking. Unlike a human-facing form where the payload needs to survive URL encoding, tool arguments arrive as raw JSON strings — the attacker has full control over the character composition.
Node.js runs regex in the same event loop as your MCP handlers. A 5-second regex hang blocks all concurrent sessions for 5 seconds. A 30-second hang looks like a server crash to the client.
Detecting vulnerable regex at development time
Use safe-regex or recheck to lint your regex patterns during CI:
npm install --save-dev safe-regex recheck
import safeRegex from 'safe-regex'
import { check } from 'recheck'
const patterns = [
/^([a-zA-Z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})$/,
/^https?:\/\/([\w-]+\.)+[\w-]+(\/[\w./?%&=-]*)?$/,
]
for (const pattern of patterns) {
const isSafe = safeRegex(pattern)
const recheckResult = check(pattern.source, pattern.flags)
if (!isSafe || recheckResult.status === 'vulnerable') {
console.error(`UNSAFE REGEX: ${pattern} — ${recheckResult.attack?.pattern}`)
process.exit(1)
}
}
Run this check in CI as part of your test suite. recheck is more precise than safe-regex — it uses static analysis to find the actual attack string, not just a heuristic classification.
Timeout-based regex execution
For patterns you can't rewrite (legacy validation logic, third-party patterns), wrap execution in a Worker thread with a timeout:
import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads'
function testRegexWithTimeout(
pattern: string,
flags: string,
input: string,
timeoutMs = 100
): Promise<boolean> {
return new Promise((resolve, reject) => {
const code = `
const { pattern, flags, input } = workerData
const re = new RegExp(pattern, flags)
parentPort.postMessage(re.test(input))
`
const worker = new Worker(code, {
eval: true,
workerData: { pattern, flags, input }
})
const timer = setTimeout(() => {
worker.terminate()
reject(new Error('Regex timeout — potential ReDoS input'))
}, timeoutMs)
worker.on('message', (result) => {
clearTimeout(timer)
resolve(result as boolean)
})
worker.on('error', (err) => {
clearTimeout(timer)
reject(err)
})
})
}
The worker thread runs on a separate OS thread — if it hangs, the main event loop is unaffected, and the timeout terminates it cleanly. The 100ms default is generous; most legitimate inputs match in under 1ms.
Linear-time alternatives for common validation patterns
Many common MCP input validation patterns can be rewritten without catastrophic-backtracking risk:
Email validation — avoid regex entirely
// Bad: complex regex with nested quantifiers
const EMAIL_RE = /^([a-zA-Z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})$/
// Good: minimal structural check — let the email server reject truly invalid addresses
function isPlausibleEmail(s: string): boolean {
if (s.length > 254) return false
const atIdx = s.lastIndexOf('@')
if (atIdx < 1 || atIdx === s.length - 1) return false
const local = s.slice(0, atIdx)
const domain = s.slice(atIdx + 1)
return local.length <= 64 && domain.includes('.') && /^[\w.!#$%&'*+/=?^`{|}~-]+$/.test(local)
}
URL validation — use the URL constructor
// Bad: regex that tries to parse URL structure
const URL_RE = /^https?:\/\/([\w-]+\.)+[\w-]+(\/[\w./?%&=-]*)?$/
// Good: the URL constructor does exact parsing, O(n)
function isValidHttpUrl(s: string): boolean {
try {
const url = new URL(s)
return url.protocol === 'https:' || url.protocol === 'http:'
} catch {
return false
}
}
Identifier validation — simple character class only
// Bad: (word)+ patterns are vulnerable to long strings of word chars followed by non-word
const ID_RE = /^([a-z0-9]+[-_]?)+[a-z0-9]+$/
// Good: single quantifier, anchored, length-bounded
function isValidIdentifier(s: string): boolean {
return s.length <= 64 && /^[a-z0-9][a-z0-9_-]*[a-z0-9]$/.test(s)
}
Input length bounding as a defence-in-depth control
All string inputs that pass through regex validation should be length-bounded before the regex runs. Most catastrophic backtracking attacks require inputs of 50+ characters to achieve multi-second hangs:
function validateEmail(raw: unknown): string {
if (typeof raw !== 'string') throw new Error('Expected string')
if (raw.length > 254) throw new Error('Email too long') // ← bound BEFORE regex
if (!isPlausibleEmail(raw)) throw new Error('Invalid email format')
return raw
}
SkillAudit grading for regex security
| Finding | Severity | Grade impact |
|---|---|---|
| Catastrophic backtracking pattern on user input (confirmed by recheck) | High | −12 |
| Complex validation regex with no input length bound | Medium | −5 |
| Email/URL parsed by hand-written regex (vs URL constructor or structural check) | Low | −3 |
| safe-regex/recheck linting present in CI | — | +4 |
| All string inputs length-bounded before validation | — | +3 |
Scan your MCP server for ReDoS risks
SkillAudit's static scanner identifies regex patterns that safe-regex classifies as unsafe and correlates them with tool handler inputs. Run a free audit to see which patterns in your tool handlers are ReDoS candidates.