VYPR
Medium severity4.3NVD Advisory· Published Jun 3, 2026

CVE-2026-10691

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.

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

Patches

2
4ce845f8749b

fix(security): validate regex patterns to prevent ReDoS in Excel/DOCX search

https://github.com/wonderwhy-er/desktopcommandermcpSaeed Al MansouriMar 26, 2026via nvd-ref
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();
             });
    
eb139aca208d

fix(security): validate regex patterns to prevent ReDoS in search (#400)

https://github.com/wonderwhy-er/desktopcommandermcpSaeed Al MansouriApr 9, 2026via nvd-ref
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

News mentions

0

No linked articles in our index yet.