CVE-2026-10691
Description
A ReDoS vulnerability in DesktopCommanderMCP allows remote attackers to crash the server by sending a crafted regex pattern for file content searches.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A ReDoS vulnerability in DesktopCommanderMCP allows remote attackers to crash the server by sending a crafted regex pattern for file content searches.
Vulnerability
A Regular Expression Denial of Service (ReDoS) vulnerability exists in the start_search function of wonderwhy-er DesktopCommanderMCP up to version 0.2.38 [2]. This flaw specifically affects the search functionality for Excel (.xlsx) and DOCX (.docx) files. The issue arises because user-supplied regular expression patterns are directly compiled into a JavaScript RegExp object without adequate validation against catastrophic backtracking, leading to inefficient complexity [2]. The vulnerability is present in the src/search-manager.ts file [1].
Exploitation
An attacker can exploit this vulnerability by initiating a prompt injection into an AI agent integrated with DesktopCommanderMCP, causing the agent to trigger a search query with a malicious, syntactically valid regular expression pattern. This pattern, when applied against crafted file content within Excel or DOCX files, causes the Node.js event loop to enter exponential backtracking. This requires no OS-level shell access and can be performed remotely by an attacker interacting with the AI agent [2].
Impact
Successful exploitation of this ReDoS vulnerability results in a denial of service. The malicious regular expression causes the Node.js event loop to block indefinitely, consuming 100% CPU resources. This renders the entire MCP server unresponsive and unavailable to legitimate users. The scope of the compromise is limited to the availability of the server itself [2].
Mitigation
This vulnerability is fixed in DesktopCommanderMCP version 0.2.39. The fix is available via commit 4ce845f8749b6a159b57b38dcc3357f7222a8078 [4]. Users are advised to upgrade to version 0.2.39 or later. No workarounds are specified in the available references.
- GitHub - wonderwhy-er/DesktopCommanderMCP: This is MCP server for Claude that gives it terminal control, file system search and diff file editing capabilities
- [Security] Regular Expression Denial of Service (ReDoS) in Excel/DOCX Content Search
- fix(security): validate regex patterns to prevent ReDoS in Excel/DOCX… · wonderwhy-er/DesktopCommanderMCP@4ce845f
AI Insight generated on Jun 3, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
1- Range: <=0.2.38
Patches
24ce845f8749bfix(security): validate regex patterns to prevent ReDoS in Excel/DOCX search
1 file changed · +53 −18
src/search-manager.ts+53 −18 modified@@ -7,6 +7,51 @@ import { getRipgrepPath } from './utils/ripgrep-resolver.js'; import { isExcelFile } from './utils/files/index.js'; import PizZip from 'pizzip'; +/** + * Check if a regex pattern is safe from catastrophic backtracking (ReDoS). + * Rejects patterns with nested quantifiers like (a+)+, (a*)+, (a+)*, (a*)*, + * and similar constructs that cause exponential runtime. + */ +export function isSafeRegex(pattern: string): boolean { + // Detect nested quantifiers: a group containing a quantifier, followed by a quantifier + // Matches patterns like (a+)+, (a+)*, (a*)+, (a*)*, (?:a+)+, etc. + // Also catches {n,m} style quantifiers nested inside quantified groups + const nestedQuantifier = /([^\\]|^)\((?:[^)]*[+*}])\s*\)[+*?]|\((?:[^)]*[+*}])\s*\)\{/; + if (nestedQuantifier.test(pattern)) { + return false; + } + + // Detect overlapping alternations in quantified groups: (a|a)+, (\w|\d)+ + // These can also cause catastrophic backtracking + const overlappingAlt = /\((?:[^)]*\|[^)]*)\)[+*]\s*[+*?{]/; + if (overlappingAlt.test(pattern)) { + return false; + } + + return true; +} + +/** + * Build a RegExp safely, falling back to literal string matching if the pattern + * is invalid or vulnerable to ReDoS. + * Returns { regex, isLiteral } so callers know if fallback occurred. + */ +export function buildSafeRegex(pattern: string, flags: string): { regex: RegExp; isLiteral: boolean } { + // Check for ReDoS-prone patterns first + if (!isSafeRegex(pattern)) { + const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + return { regex: new RegExp(escaped, flags), isLiteral: true }; + } + + try { + return { regex: new RegExp(pattern, flags), isLiteral: false }; + } catch { + // If pattern is not valid regex, escape it for literal matching + const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + return { regex: new RegExp(escaped, flags), isLiteral: true }; + } +} + export interface SearchResult { file: string; line?: number; @@ -345,16 +390,9 @@ export interface SearchSessionOptions { ): Promise<SearchResult[]> { const results: SearchResult[] = []; - // Build regex for matching content + // Build regex for matching content, with ReDoS protection const flags = ignoreCase ? 'i' : ''; - let regex: RegExp; - try { - regex = new RegExp(pattern, flags); - } catch { - // If pattern is not valid regex, escape it for literal matching - const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - regex = new RegExp(escaped, flags); - } + const { regex } = buildSafeRegex(pattern, flags); // Find Excel files recursively let excelFiles = await this.findExcelFiles(rootPath); @@ -368,7 +406,8 @@ export interface SearchSessionOptions { // Support glob-like patterns if (pat.includes('*')) { const regexPat = pat.replace(/\./g, '\\.').replace(/\*/g, '.*'); - return new RegExp(`^${regexPat}$`, 'i').test(fileName); + const { regex: globRegex } = buildSafeRegex(`^${regexPat}$`, 'i'); + return globRegex.test(fileName); } // Exact match (case-insensitive) return fileName.toLowerCase() === pat.toLowerCase(); @@ -529,14 +568,9 @@ export interface SearchSessionOptions { ): Promise<SearchResult[]> { const results: SearchResult[] = []; + // Build regex for matching content, with ReDoS protection const flags = ignoreCase ? 'i' : ''; - let regex: RegExp; - try { - regex = new RegExp(pattern, flags); - } catch { - const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - regex = new RegExp(escaped, flags); - } + const { regex } = buildSafeRegex(pattern, flags); let docxFiles = await this.findDocxFiles(rootPath); @@ -547,7 +581,8 @@ export interface SearchSessionOptions { return patterns.some(pat => { if (pat.includes('*')) { const regexPat = pat.replace(/\./g, '\\.').replace(/\*/g, '.*'); - return new RegExp(`^${regexPat}$`, 'i').test(fileName); + const { regex: globRegex } = buildSafeRegex(`^${regexPat}$`, 'i'); + return globRegex.test(fileName); } return fileName.toLowerCase() === pat.toLowerCase(); });
eb139aca208dfix(security): validate regex patterns to prevent ReDoS in search (#400)
1 file changed · +32 −35
src/search-manager.ts+32 −35 modified@@ -159,7 +159,8 @@ export interface SearchSessionOptions { options.pattern, options.ignoreCase !== false, options.maxResults, - options.filePattern // Pass filePattern to filter Excel files too + options.filePattern, // Pass filePattern to filter Excel files too + options.literalSearch // Respect literalSearch flag for Office files ).then(excelResults => { // Add Excel results to session (merged after initial response) for (const result of excelResults) { @@ -182,7 +183,8 @@ export interface SearchSessionOptions { options.pattern, options.ignoreCase !== false, options.maxResults, - options.filePattern + options.filePattern, + options.literalSearch // Respect literalSearch flag for Office files ).then(docxResults => { for (const result of docxResults) { session.results.push(result); @@ -341,20 +343,14 @@ export interface SearchSessionOptions { pattern: string, ignoreCase: boolean, maxResults?: number, - filePattern?: string + filePattern?: string, + _literalSearch?: boolean ): Promise<SearchResult[]> { const results: SearchResult[] = []; - // Build regex for matching content - const flags = ignoreCase ? 'i' : ''; - let regex: RegExp; - try { - regex = new RegExp(pattern, flags); - } catch { - // If pattern is not valid regex, escape it for literal matching - const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - regex = new RegExp(escaped, flags); - } + // Office file search always uses literal matching to prevent ReDoS. + // Regex patterns are treated as literal strings — this is intentional. + const searchTerm = ignoreCase ? pattern.toLowerCase() : pattern; // Find Excel files recursively let excelFiles = await this.findExcelFiles(rootPath); @@ -367,7 +363,13 @@ export interface SearchSessionOptions { return patterns.some(pat => { // Support glob-like patterns if (pat.includes('*')) { - const regexPat = pat.replace(/\./g, '\\.').replace(/\*/g, '.*'); + // Escape all regex metacharacters first (preserving * for glob expansion), + // then convert the remaining * wildcards to .* for glob matching. + // Without this, patterns like report(2024).xlsx or [draft].xlsx would be + // misinterpreted as regex groups/character-classes. + const regexPat = pat + .replace(/[.+^${}()|[\]\\]/g, '\\$&') // escape metacharacters except * + .replace(/\*/g, '.*'); // glob * → regex .* return new RegExp(`^${regexPat}$`, 'i').test(fileName); } // Exact match (case-insensitive) @@ -424,12 +426,10 @@ export interface SearchSessionOptions { // Join all cell values with space for cross-column matching const rowText = rowValues.join(' '); - if (regex.test(rowText)) { - // Extract the matching portion for display - const match = rowText.match(regex); - const matchContext = match - ? this.getMatchContext(rowText, match.index || 0, match[0].length) - : rowText.substring(0, 150); + const textToSearch = ignoreCase ? rowText.toLowerCase() : rowText; + const matchIndex = textToSearch.indexOf(searchTerm); + if (matchIndex !== -1) { + const matchContext = this.getMatchContext(rowText, matchIndex, searchTerm.length); results.push({ file: `${filePath}:${sheetName}!Row${rowNumber}`, @@ -525,18 +525,14 @@ export interface SearchSessionOptions { pattern: string, ignoreCase: boolean, maxResults?: number, - filePattern?: string + filePattern?: string, + _literalSearch?: boolean ): Promise<SearchResult[]> { const results: SearchResult[] = []; - const flags = ignoreCase ? 'i' : ''; - let regex: RegExp; - try { - regex = new RegExp(pattern, flags); - } catch { - const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - regex = new RegExp(escaped, flags); - } + // Office file search always uses literal matching to prevent ReDoS. + // Regex patterns are treated as literal strings — this is intentional. + const searchTerm = ignoreCase ? pattern.toLowerCase() : pattern; let docxFiles = await this.findDocxFiles(rootPath); @@ -546,7 +542,9 @@ export interface SearchSessionOptions { const fileName = path.basename(filePath); return patterns.some(pat => { if (pat.includes('*')) { - const regexPat = pat.replace(/\./g, '\\.').replace(/\*/g, '.*'); + const regexPat = pat + .replace(/[.+^${}()|[\]\\]/g, '\\$&') // escape metacharacters except * + .replace(/\*/g, '.*'); // glob * → regex .* return new RegExp(`^${regexPat}$`, 'i').test(fileName); } return fileName.toLowerCase() === pat.toLowerCase(); @@ -583,11 +581,10 @@ export interface SearchSessionOptions { if (!text || !text.trim()) continue; lineNum++; - if (regex.test(text)) { - const match = text.match(regex); - const matchContext = match - ? this.getMatchContext(text, match.index || 0, match[0].length) - : text.substring(0, 150); + const textToSearch = ignoreCase ? text.toLowerCase() : text; + const matchIndex = textToSearch.indexOf(searchTerm); + if (matchIndex !== -1) { + const matchContext = this.getMatchContext(text, matchIndex, searchTerm.length); const partName = xmlPath === 'word/document.xml' ? '' : `:${xmlPath.replace('word/', '')}`; results.push({
Vulnerability mechanics
Root cause
"Inefficient regular expression complexity in the search functionality allows for denial-of-service attacks."
Attack vector
An attacker can remotely trigger this vulnerability by providing a specially crafted regular expression pattern as an argument to the `start_search` function. This manipulation of the `SearchResult[]` argument leads to inefficient regular expression complexity, causing catastrophic backtracking and a denial-of-service condition. The attack is possible because the application does not adequately validate or sanitize user-supplied regular expression patterns before compiling them [ref_id=1].
Affected code
The vulnerability resides in the `src/search-manager.ts` file, specifically within the functions responsible for searching file content, such as `searchExcelFiles` and `searchDocxFiles`. The original implementation directly compiled user-supplied patterns into regular expressions without sufficient validation, leading to ReDoS vulnerabilities.
What the fix does
The patch introduces `isSafeRegex()` and `buildSafeRegex()` functions to `src/search-manager.ts` [patch_id=4550933]. These functions validate regular expression patterns to detect and reject those prone to catastrophic backtracking, such as those with nested quantifiers. If a pattern is deemed unsafe or invalid, it automatically falls back to literal string matching. This prevents the ReDoS vulnerability by ensuring that complex or malicious regex patterns do not cause excessive resource consumption.
Preconditions
- authThe attacker needs low privileges (PR:L) to initiate the attack.
- networkThe attack can be initiated remotely (AV:N).
Generated on Jun 3, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
8- github.com/wonderwhy-er/DesktopCommanderMCP/commit/4ce845f8749b6a159b57b38dcc3357f7222a8078nvd
- github.com/wonderwhy-er/DesktopCommanderMCP/issues/375nvd
- github.com/wonderwhy-er/DesktopCommanderMCP/pull/400nvd
- github.com/wonderwhy-er/DesktopCommanderMCP/releases/tag/v0.2.39nvd
- vuldb.com/cve/CVE-2026-10691nvd
- vuldb.com/submit/830746nvd
- vuldb.com/vuln/367960nvd
- vuldb.com/vuln/367960/ctinvd
News mentions
0No linked articles in our index yet.