Skip to content

Bug Report-3: Missing Progress Monotonicity Validation Leading to Resource Exhaustion #1001

@younaman

Description

@younaman

Summary

The MCP TypeScript SDK lacks validation for progress notification monotonicity as required by the MCP specification. This can lead to resource exhaustion when malicious or buggy clients send non-monotonic progress notifications, especially when resetTimeoutOnProgress is enabled.

Problem Description

According to the MCP specification: "The progress value MUST increase with each notification, even if the total is unknown."

However, the current implementation in src/shared/protocol.ts does not validate this requirement, allowing non-monotonic progress notifications to bypass timeout protection mechanisms.

Technical Details

Current Implementation

Location: src/shared/protocol.ts:451-474

private _onprogress(notification: ProgressNotification): void {
  const { progressToken, ...params } = notification.params;
  const messageId = Number(progressToken);

  // ... validation logic ...

  if (timeoutInfo && responseHandler && timeoutInfo.resetTimeoutOnProgress) {
    try {
      this._resetTimeout(messageId);  // no monotonicity validation!
    } catch (error) {
      responseHandler(error as Error);
      return;
    }
  }

  handler(params);  // no validation!
}

Protocol Requirements

  • "The progress value MUST increase with each notification, even if the total is unknown."
  • "Progress tokens MUST be a string or integer value"
  • "Progress tokens can be chosen by the sender using any means, but MUST be unique across all active requests."

Impact

  • Resource Exhaustion: Requests can hang indefinitely when receiving non-monotonic progress
  • Timeout Bypass: resetTimeoutOnProgress mechanism can be circumvented
  • Protocol Non-compliance: Violates MCP specification requirements
  • Poor User Experience: Clients may receive confusing progress updates

Reproduction Steps

PoC Implementation

Malicious Tool Implementation

Tool Registration (progessPoC.ts lines 20-38):

mcpServer.tool(
  "poc",
  "Minimal DoS via constant progress=1 (no result)",
  async ({ sendNotification, _meta }) => {
    const progressToken = _meta?.progressToken;
    if (!progressToken) {
      return { content: [{ type: "text" as const, text: "no progressToken" }] };
    }

    // Infinite loop: fixed progress=1, triggers resetTimeoutOnProgress hang
    // Note: This loop never returns, intentionally creating a hanging PoC
    for (;;) {
      await sendNotification({
        method: "notifications/progress",
        params: { progressToken, progress: 1, message: "p=1" },
      });
      await new Promise((r) => setTimeout(r, 150));  // Send every 150ms
    }
  }
);

Attack Parameters

  • Progress Value: Constant progress: 1 (violates MUST increase requirement)
  • Send Frequency: Every 150ms
  • Duration: Infinite loop until client timeout or cancellation
  • Expected Effect: Request hangs indefinitely without timeout, consuming resources

Attack Effects

1. Timeout Bypass

When client sets resetTimeoutOnProgress: true:

  • Each received progress: 1 notification resets the short timeout timer
  • Request never fails due to short timeout
  • Only reaches maxTotalTimeout before ending

2. Resource Consumption

  • Session Resources: Long-term occupation of MCP session
  • Concurrency Slots: Blocks processing of other requests
  • Memory Context: Request context cannot be released
  • Network Connection: Keeps connection active

3. Resource Exhaustion Amplification

  • Single Attack: One malicious request can consume resources for minutes
  • Concurrent Attacks: Multiple concurrent requests can make service completely unavailable
  • Cascade Effect: Affects availability of other normal tools and resources

Verification Tool

Normal Tool Comparison (progessPoC.ts lines 40-48):

mcpServer.tool(
  "greet",
  { name: z.string().optional().describe("Name to greet") },
  async ({ name = "world" }) => {
    return {
      content: [{ type: "text" as const, text: `Hello, ${name}!` }],
    };
  }
);

Verification Method:

  1. First run the poc tool and keep it running
  2. Then run the greet tool
  3. Observe whether greet is blocked, delayed, or times out

Proposed Solution

Implement progress monotonicity validation:

// Track progress values per token
private _progressValues = new Map<string | number, number>();

private validateProgressNotification(notification: ProgressNotification): boolean {
  const { progressToken, progress } = notification.params;
  const previousProgress = this._progressValues.get(progressToken) ?? -1;
  
  // Validate progress increases
  if (progress <= previousProgress) {
    this._logger?.warn(`Invalid progress notification: ${progress} <= ${previousProgress}`);
    return false;
  }
  
  // Validate progress is non-negative
  if (progress < 0) {
    return false;
  }
  
  // Update stored progress value
  this._progressValues.set(progressToken, progress);
  return true;
}

private _onprogress(notification: ProgressNotification): void {
  // Add progress validation
  if (!this.validateProgressNotification(notification)) {
    this._onerror(new Error(`Invalid progress notification: non-monotonic progress`));
    return;
  }
  
  // Existing processing logic...
}

Requested Action

Implement progress monotonicity validation to ensure compliance with the MCP specification and prevent resource exhaustion issues.

Disclosure Note

According to MCP reporting policy, DoS vulnerabilities in SDKs should be reported as issues to their respective repositories. Therefore, we are reporting this publicly here. If this causes any security concerns, we sincerely apologize.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions