Skip to content

Prevent forwardedHeaders from overriding MCP auth headers#2893

Merged
miles-kt-inkeep merged 5 commits intomainfrom
codex/fix-durable-execution-untrusted-header-vulnerability
Mar 30, 2026
Merged

Prevent forwardedHeaders from overriding MCP auth headers#2893
miles-kt-inkeep merged 5 commits intomainfrom
codex/fix-durable-execution-untrusted-header-vulnerability

Conversation

@robert-inkeep
Copy link
Copy Markdown
Collaborator

Motivation

  • The durable execution flow accepted caller-provided forwardedHeaders and merged them into MCP server requests after injecting trusted auth and tenant/project headers, allowing API-key holders to spoof or override sensitive headers.

Description

  • Add mergeHeadersWithoutOverrides(existingHeaders, forwardedHeaders) to merge forwarded headers only when the header name does not already exist (case-insensitive), preserving injected trusted headers.
  • Replace the previous blind spread merge in agents-api/src/domains/run/agents/services/AgentMcpManager.ts with the new merge helper to prevent overrides of Authorization, x-inkeep-tenant-id, x-inkeep-project-id, x-inkeep-tool-id, etc.
  • Add a regression test in agents-api/src/__tests__/run/agents/AgentMcpManager.test.ts that verifies trusted Slack MCP Authorization and tenant headers cannot be replaced by forwardedHeaders while allowing additive custom headers.
  • Update the test helper createManager() to accept forwardedHeaders to exercise the path under test.

Testing

  • Ran pnpm --filter agents-api exec biome check --write src/domains/run/agents/services/AgentMcpManager.ts src/__tests__/run/agents/AgentMcpManager.test.ts, which completed successfully.
  • Attempted to run the unit test with pnpm --filter agents-api exec vitest --run --config vitest.config.ts src/__tests__/run/agents/AgentMcpManager.test.ts, but the test run failed in this environment due to unresolved package entry for @inkeep/agents-core (monorepo build/setup required), so the regression could not be executed end-to-end here.

Codex Task

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 30, 2026

⚠️ No Changeset found

Latest commit: fa23e2c

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agents-api Ready Ready Preview, Comment Mar 30, 2026 6:02pm
agents-docs Ready Ready Preview, Comment Mar 30, 2026 6:02pm
agents-manage-ui Ready Ready Preview, Comment Mar 30, 2026 6:02pm

Request Review

@vercel vercel bot temporarily deployed to Preview – agents-docs March 30, 2026 14:32 Inactive
@pullfrog
Copy link
Copy Markdown
Contributor

pullfrog bot commented Mar 30, 2026

TL;DR — Fixes a header-injection vulnerability where caller-supplied forwardedHeaders could override trusted auth and tenant headers on outbound MCP and A2A requests. A new mergeHeadersWithoutOverrides utility performs case-insensitive deduplication so existing headers are never replaced — applied to both the MCP server path and the A2A client path.

Key changes

  • Extract mergeHeadersWithoutOverrides into merge-headers.ts — shared utility that builds a lowercase set of existing header names and only adds forwarded headers whose names don't already exist, preventing spoofing of Authorization, x-inkeep-tenant-id, x-inkeep-project-id, etc.
  • Apply safe merge in AgentMcpManager.ts — replaces the naive spread of forwardedHeaders with the new merge utility when constructing MCP server config headers.
  • Apply safe merge in executionHandler.ts — replaces the naive spread of forwardedHeaders on A2A client headers with the same utility, closing the same class of vulnerability on the inter-agent communication path.
  • Add unit tests for mergeHeadersWithoutOverrides — dedicated test suite covering exact-case collisions, case-insensitive collisions, passthrough of non-conflicting headers, and undefined/empty inputs.
  • Add integration-level header override tests — regression tests in both AgentMcpManager.test.ts (Slack and GitHub MCP paths) and executionHandler-run-as-user.test.ts (A2A client path) verifying trusted headers survive attacker-controlled payloads.

Summary | 6 files | 5 commits | base: maincodex/fix-durable-execution-untrusted-header-vulnerability


Case-insensitive header merge utility

Before: forwardedHeaders were spread after trusted auth/tenant headers in both MCP and A2A paths, allowing any caller to replace Authorization, x-inkeep-tenant-id, or x-inkeep-project-id.
After: mergeHeadersWithoutOverrides builds a lowercase Set of existing header names and skips any forwarded header that collides, preserving trusted values while still passing through genuinely new headers.

Why is this case-insensitive? HTTP header names are case-insensitive per RFC 9110. An attacker could bypass a naive key-equality check by sending authorization instead of Authorization. The helper normalises to lowercase before comparing.

merge-headers.ts · AgentMcpManager.ts · executionHandler.ts


A2A client header protection in executionHandler

Before: The A2AClient constructor received { ...trustedHeaders, ...(forwardedHeaders || {}) }, allowing overrides.
After: Trusted headers are defined first, then merged via mergeHeadersWithoutOverrides so forwarded headers can only add — never replace.

The test suite covers individual header overrides (Authorization, x-inkeep-tenant-id, x-inkeep-project-id, x-inkeep-agent-id, x-inkeep-sub-agent-id), case-variant attacks, and a combined scenario proving non-conflicting headers still pass through.

executionHandler.ts · executionHandler-run-as-user.test.ts

Pullfrog  | View workflow run | Triggered by Pullfrog | Using Claude Opus𝕏

Copy link
Copy Markdown
Contributor

@pullfrog pullfrog bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: medium-high — the MCP fix is correct but the same vulnerability exists in another path.

The mergeHeadersWithoutOverrides helper and its application in AgentMcpManager are well-implemented. However, the same class of bug — untrusted forwardedHeaders overriding trusted auth headers via object spread — also exists in executionHandler.ts:318, where forwardedHeaders is spread after Authorization and x-inkeep-* headers in the A2A client construction. Since forwardedHeaders can originate from the unfiltered body.forwardedHeaders in the /executions route, this is an equivalent spoofing vector that should be addressed in the same PR.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

mergedHeaders[headerName] = headerValue;
}

return mergedHeaders;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The helper is clean and correct. One minor observation: the existingHeaders parameter type is Record<string, string> | undefined but forwardedHeaders is non-optional. This is fine since the call site guards with Object.keys(...).length > 0, but consider whether a forwardedHeaders: undefined guard is ever needed at the call site vs. inside the function for defensive robustness.

const headers = vi.mocked(McpClient).mock.calls[0]?.[0]?.server?.headers;
expect(headers?.Authorization).toBe('Bearer test-slack-key');
expect(headers?.['x-inkeep-tenant-id']).toBe('tenant-1');
expect(headers?.['x-custom-forwarded']).toBe('allowed');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good coverage for the Slack path. Consider also adding a test for the GitHub work-app tool path (isGithubWorkAppTool) — it uses the same header injection pattern at lines 156-162 and the same mergeHeadersWithoutOverrides call protects it, but an explicit test would prevent future regressions if those code paths diverge.

...this.config.forwardedHeaders,
};
serverConfig.headers = mergeHeadersWithoutOverrides(
serverConfig.headers as Record<string, string> | undefined,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The as Record<string, string> | undefined cast is safe here since serverConfig.headers is typed as Record<string, string> in practice, but it papers over a type mismatch. If the upstream McpServerConfig.headers type changes, this cast will silently suppress errors. Not blocking, but worth a follow-up to tighten the type.

Copy link
Copy Markdown
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review Summary

(4) Total Issues | Risk: High

🔴❗ Critical (1) ❗🔴

🔴 1) executionHandler.ts:309-321 Same header override vulnerability exists in A2A client construction

Issue: The exact same vulnerability pattern this PR fixes in AgentMcpManager.ts exists in executionHandler.ts. On lines 309-321, forwardedHeaders is spread AFTER trusted auth headers when constructing the A2A client:

const a2aClient = new A2AClient(agentBaseUrl, {
  headers: {
    Authorization: `Bearer ${authToken}`,
    'x-inkeep-tenant-id': tenantId,
    'x-inkeep-project-id': projectId,
    'x-inkeep-agent-id': agentId,
    'x-inkeep-sub-agent-id': currentAgentId,
    ...(runAsUserId ? { 'x-inkeep-run-as-user-id': runAsUserId } : {}),
    ...(appPrompt ? { 'x-inkeep-app-prompt': appPrompt } : {}),
    ...(forwardedHeaders || {}),  // ← Can override ALL headers above!
  },
  ...
});

In JavaScript object spread, later properties override earlier ones with the same key. Since forwardedHeaders comes from the API request body without sanitization, an attacker with API key access could override Authorization, x-inkeep-tenant-id, x-inkeep-project-id, etc.

Why: This could allow cross-tenant requests or privilege escalation within internal A2A communication. The attack surface is identical to what this PR fixes in MCP server requests.

Fix: Apply the same mergeHeadersWithoutOverrides() pattern:

import { mergeHeadersWithoutOverrides } from '../agents/services/AgentMcpManager';

// Then in the A2AClient construction:
const trustedHeaders = {
  Authorization: `Bearer ${authToken}`,
  'x-inkeep-tenant-id': tenantId,
  'x-inkeep-project-id': projectId,
  'x-inkeep-agent-id': agentId,
  'x-inkeep-sub-agent-id': currentAgentId,
  ...(runAsUserId ? { 'x-inkeep-run-as-user-id': runAsUserId } : {}),
  ...(appPrompt ? { 'x-inkeep-app-prompt': appPrompt } : {}),
};

const a2aClient = new A2AClient(agentBaseUrl, {
  headers: mergeHeadersWithoutOverrides(trustedHeaders, forwardedHeaders || {}),
  fetchFn: getInProcessFetch(),
});

Alternatively, consider exporting mergeHeadersWithoutOverrides from a shared utility module since it's now needed in multiple places.

Refs:

🟠⚠️ Major (1) 🟠⚠️

Inline Comments:

  • 🟠 Major: AgentMcpManager.test.ts:236 Missing test for GitHub MCP header override protection

🟡 Minor (2) 🟡

Inline Comments:

  • 🟡 Minor: AgentMcpManager.test.ts:225 Test doesn't verify case-insensitive header blocking

🟡 1) AgentMcpManager.test.ts:225-228 Test assertions don't cover all protected headers

Issue: The test verifies Authorization and x-inkeep-tenant-id cannot be overridden, but the implementation injects four trusted headers: Authorization, x-inkeep-tenant-id, x-inkeep-project-id, and x-inkeep-tool-id.

Why: If the merge function fails to protect project-id or tool-id, an attacker could access resources from other projects or tools.

Fix: Extend forwardedHeaders and assertions:

forwardedHeaders: {
  Authorization: 'Bearer attacker-token',
  'x-inkeep-tenant-id': 'attacker-tenant',
  'x-inkeep-project-id': 'attacker-project',
  'x-inkeep-tool-id': 'attacker-tool',
  'x-custom-forwarded': 'allowed',
},
// ...
expect(headers?.['x-inkeep-project-id']).toBe('project-1');
expect(headers?.['x-inkeep-tool-id']).toBe('test-tool-id');

Refs:


🚫 REQUEST CHANGES

Summary: The fix to AgentMcpManager.ts is well-implemented with proper case-insensitive header protection. However, the same vulnerability exists in executionHandler.ts (lines 309-321) where forwardedHeaders can override trusted A2A auth headers. This is a critical security issue that should be fixed in the same PR since it's the identical attack pattern. Additionally, test coverage should be expanded to cover GitHub MCP endpoints and case-insensitive blocking.

Discarded (0)

No findings were discarded.

Reviewers (4)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
pr-review-security-iam 2 1 0 0 0 0 0
pr-review-tests 4 0 0 0 2 0 1
pr-review-standards 0 0 0 0 0 0 0
pr-review-appsec 0 0 0 0 0 0 0
Total 6 1 0 0 2 0 1

Note: The security-iam reviewer correctly identified the executionHandler.ts vulnerability as a related but pre-existing issue. This was elevated to Critical because it's the exact same attack pattern being fixed and should be addressed together.

expect(headers?.Authorization).toBe('Bearer test-slack-key');
expect(headers?.['x-inkeep-tenant-id']).toBe('tenant-1');
expect(headers?.['x-custom-forwarded']).toBe('allowed');
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 MAJOR: Missing test for GitHub MCP header override protection

Issue: This test validates Slack MCP header protection, but no equivalent test exists for GitHub MCP endpoints which inject the same sensitive headers (Authorization, x-inkeep-tenant-id, x-inkeep-project-id, x-inkeep-tool-id).

Why: Both Slack and GitHub code paths (lines 148-163 and 165-176 in AgentMcpManager.ts) inject trusted headers before the mergeHeadersWithoutOverrides call. If someone accidentally modifies the GitHub path differently, there would be no regression test to catch it.

Fix: Add an equivalent test in the 'GitHub MCP API key forwarding' describe block:

test('does not allow forwarded headers to override trusted GitHub auth headers', async () => {
  const { isGithubWorkAppTool } = await import('@inkeep/agents-core');
  vi.mocked(isGithubWorkAppTool).mockReturnValue(true);

  const trustedGithubTool = createMcpTool({
    config: {
      type: 'mcp',
      mcp: {
        server: { url: 'https://api.inkeep.example/work-apps/github/mcp' },
      },
    },
  });

  await createManager({
    forwardedHeaders: {
      Authorization: 'Bearer attacker-token',
      'x-inkeep-tenant-id': 'attacker-tenant',
      'x-custom-forwarded': 'allowed',
    },
  }).getToolSet(trustedGithubTool);

  const headers = vi.mocked(McpClient).mock.calls[0]?.[0]?.server?.headers;
  expect(headers?.Authorization).toBe('Bearer test-github-key');
  expect(headers?.['x-inkeep-tenant-id']).toBe('tenant-1');
  expect(headers?.['x-custom-forwarded']).toBe('allowed');
});

Refs:

});

await createManager({
forwardedHeaders: {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Minor: Test doesn't verify case-insensitive header blocking

Issue: The mergeHeadersWithoutOverrides() function uses case-insensitive comparison (toLowerCase()), but this test only uses exact-case header names. HTTP headers are case-insensitive per RFC 7230, so an attacker could attempt authorization or AUTHORIZATION to bypass protection.

Why: If the case-insensitive logic is accidentally removed during refactoring, this test wouldn't catch the regression.

Fix: Add a test case for case variations:

test('blocks forwarded headers regardless of casing (case-insensitive)', async () => {
  const { isSlackWorkAppTool } = await import('@inkeep/agents-core');
  vi.mocked(isSlackWorkAppTool).mockReturnValue(true);

  const trustedSlackTool = createMcpTool({
    config: {
      type: 'mcp',
      mcp: {
        server: { url: 'https://api.inkeep.example/work-apps/slack/mcp' },
      },
    },
  });

  await createManager({
    forwardedHeaders: {
      authorization: 'Bearer attacker-token',  // lowercase
      'X-INKEEP-TENANT-ID': 'attacker-tenant',  // uppercase
    },
  }).getToolSet(trustedSlackTool);

  const headers = vi.mocked(McpClient).mock.calls[0]?.[0]?.server?.headers;
  expect(headers?.Authorization).toBe('Bearer test-slack-key');
  expect(headers?.['x-inkeep-tenant-id']).toBe('tenant-1');
});

Refs:

@itoqa
Copy link
Copy Markdown

itoqa bot commented Mar 30, 2026

Ito Test Report ✅

10 test cases ran. 10 passed.

The unified local verification run passed all 10 of 10 executed test cases with zero failures, and no confirmed production-code defects were found; a separate durable-execution lifecycle case was investigated but excluded because evidence indicated environment credential dependency rather than an application bug. Key findings were that streaming and non-stream chat paths remained stable across turns, forwarded-header hardening correctly blocked attempts to override trusted MCP authorization/tool identity while still allowing additive headers, and resilience held under large/malicious headers, reconnect and rapid double-submit scenarios, and mobile back/forward resend flows without crashes, cross-run contamination, or auth/context leakage (with noted local fallback/bypass setup in some non-production checks).

✅ Passed (10)
Category Summary Screenshot
Adversarial Baseline and spoofed x-inkeep-tool-id execution requests both returned 200 SSE with the same outcome class and no observed escalation. ADV-3
Adversarial Near-simultaneous malicious-header submissions remained isolated with distinct run and conversation identities. ADV-4
Adversarial Mobile replay with back navigation and resend kept requests isolated and auth-stable; calls returned HTTP 200 with safe failure mode (no context leakage). ADV-5
Edge Omitted and empty-object forwardedHeaders both produced equivalent execution response class and stream behavior. EDGE-2
Edge Large forwardedHeaders payloads were accepted with no 5xx or crash, and execution lifecycle remained reachable. EDGE-3
Edge Reconnect stream handling stayed functional for malicious-header execution after local dependency bypass setup. EDGE-4
Logic Trusted MCP headers stay authoritative when forwarded Authorization is supplied; code and regression test confirm override resistance. LOGIC-1
Logic Code and unit-test evidence confirm additive forwarded headers are preserved while trusted headers remain authoritative. LOGIC-3
Happy-path Streaming chat completions succeeded on both turns with the same conversation context after providing required headers and model. ROUTE-2
Happy-path Non-stream /run/api/chat returned valid assistant content across two turns after local credential fallback remediation. ROUTE-3

Commit: 0d387be

View Full Run


Tell us how we did: Give Ito Feedback

Copy link
Copy Markdown
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review Summary

(1) Total Issues | Risk: Low

This PR successfully addresses the header-injection vulnerability from the previous review. The implementation is correct and secure. One test coverage gap remains.

Previous Feedback Status

All 4 issues from the prior review have been addressed:

  • Critical: executionHandler.ts vulnerability → Fixed with mergeHeadersWithoutOverrides()
  • Major: Missing GitHub MCP test → Added (lines 304-333)
  • Minor: Case-insensitive test missing → Added (lines 242-267)
  • Minor: Test assertions incomplete → Now covers all 4 protected headers

🟠⚠️ Major (1) 🟠⚠️

🟠 1) executionHandler.ts:321 Missing test coverage for A2A header override protection

Issue: The PR fixes the header-injection vulnerability in executionHandler.ts using mergeHeadersWithoutOverrides(), but there are no regression tests verifying this fix in the A2A client path. The MCP path has comprehensive tests (Slack + GitHub + case-insensitive), but the A2A path does not.

Why: Without a regression test, a future refactor could reintroduce the vulnerability by changing the merge order back to {...trustedHeaders, ...forwardedHeaders}. The A2A path handles internal agent-to-agent communication with service tokens, making header spoofing a cross-tenant and privilege escalation risk. This creates an asymmetric coverage gap where the same class of vulnerability is tested in one location but not the other.

Fix: Add tests in a new file or extend executionHandler-run-as-user.test.ts (which already has infrastructure for capturing A2A client headers via getA2AClientHeaders()):

describe('ExecutionHandler - forwardedHeaders security', () => {
  it('does not allow forwardedHeaders to override trusted headers', async () => {
    await handler.execute({
      // ... existing params
      forwardedHeaders: {
        Authorization: 'Bearer attacker-token',
        'x-inkeep-tenant-id': 'attacker-tenant',
        'x-inkeep-project-id': 'attacker-project',
      },
    });
    const headers = getA2AClientHeaders();
    expect(headers?.Authorization).toBe('Bearer mock-service-token');
    expect(headers?.['x-inkeep-tenant-id']).toBe('test-tenant');
    expect(headers?.['x-inkeep-project-id']).toBe('test-project');
  });

  it('allows non-conflicting forwardedHeaders through', async () => {
    await handler.execute({
      // ... existing params
      forwardedHeaders: { 'x-custom-header': 'allowed-value' },
    });
    expect(getA2AClientHeaders()?.['x-custom-header']).toBe('allowed-value');
  });

  it('blocks case-insensitive header override attempts', async () => {
    await handler.execute({
      // ... existing params
      forwardedHeaders: {
        authorization: 'Bearer attacker',
        'X-INKEEP-TENANT-ID': 'attacker',
      },
    });
    const headers = getA2AClientHeaders();
    expect(headers?.Authorization).toBe('Bearer mock-service-token');
    expect(headers?.['x-inkeep-tenant-id']).toBe('test-tenant');
  });
});

Refs:

💭 Consider (1) 💭

💭 1) merge-headers.ts Consider adding JSDoc for the utility function

Issue: The mergeHeadersWithoutOverrides() function is a security-critical utility but lacks documentation explaining its purpose and behavior.
Why: Future maintainers may not understand why this function exists or accidentally modify it incorrectly.
Fix: Add a brief JSDoc comment explaining the security rationale.


💡 APPROVE WITH SUGGESTIONS

Summary: Excellent security fix! The mergeHeadersWithoutOverrides() implementation is correct and secure — case-insensitive comparison, no prototype pollution vectors, and proper handling of undefined inputs. All feedback from the previous review has been addressed. The only remaining item is adding regression tests for the executionHandler.ts A2A path to match the comprehensive test coverage already in place for the MCP path. This is a non-blocking suggestion given the security fix is correct.

Discarded (0)

No findings were discarded.

Reviewers (2)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
pr-review-appsec 0 0 0 0 0 0 0
pr-review-tests 1 1 0 0 0 0 0
Total 1 1 0 0 0 0 0

Note: AppSec reviewer confirmed the fix is complete with no bypass vectors. Tests reviewer identified the A2A coverage gap.

@github-actions github-actions bot deleted a comment from claude bot Mar 30, 2026
Copy link
Copy Markdown
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review Summary

(0) Total Issues | Risk: Low

This re-review covers the delta since the last automated review (commit 278b53f12ca2b9957f696945).

Previous Feedback Status

The single Major finding from the prior review has been addressed:

  • Major: Missing test coverage for A2A header override protection → Added (lines 204-315 in executionHandler-run-as-user.test.ts)

Delta Analysis

The new ExecutionHandler - A2A client header override protection test suite (113 lines) provides comprehensive coverage:

Test Protection Verified
Individual header tests Authorization, x-inkeep-tenant-id, x-inkeep-project-id, x-inkeep-agent-id, x-inkeep-sub-agent-id
Case-insensitive test Mixed case variants (authorization, X-INKEEP-TENANT-ID, X-Inkeep-Project-Id)
Pass-through test Non-conflicting headers (x-forwarded-cookie, x-inkeep-client-timezone, x-custom-header)
Combined scenario All trusted headers + legitimate headers simultaneously

The tests correctly verify:

  1. Trusted headers cannot be overridden by forwardedHeaders (security)
  2. Non-conflicting headers pass through (functionality)
  3. Case-insensitive blocking works per RFC 7230 (completeness)

✅ APPROVE

Summary: All previous feedback has been addressed. The A2A header override protection now has comprehensive regression test coverage matching the MCP path tests. The security fix is complete and well-tested. 🎉

Note: Unable to submit formal APPROVE verdict due to GitHub API permissions. This is a recommendation to approve.

Discarded (0)

No findings were discarded.

Reviewers (1)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
orchestrator-direct 0 0 0 0 0 0 0
Total 0 0 0 0 0 0 0

Note: Delta scope (test-only additions) was small enough for direct orchestrator analysis without dispatching sub-reviewers.

@vercel vercel bot temporarily deployed to Preview – agents-docs March 30, 2026 17:58 Inactive
Copy link
Copy Markdown
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review Summary

(0) Total Issues | Risk: Low

This re-review covers the delta since the last automated review (commit b9957f696945fa23e2ccb5a9).

Delta Analysis

The changes since the last APPROVE review are minimal:

Change File Impact
Removed type cast AgentMcpManager.ts:182 Code quality improvement — serverConfig.headers is now properly typed, eliminating the need for the as Record<string, string> | undefined cast
Merge from main Multiple files Brings in unrelated CSP/runtime config changes already reviewed in their respective PRs (#2896, #2898, #2899)

Security Fix Status

The header-injection vulnerability fix is complete and unchanged:

  • mergeHeadersWithoutOverrides() correctly implements case-insensitive header blocking
  • ✅ Applied in AgentMcpManager.ts (MCP path)
  • ✅ Applied in executionHandler.ts (A2A path)
  • ✅ Comprehensive test coverage for all protected headers
  • ✅ Case-insensitive blocking tests per RFC 7230

✅ APPROVE

Summary: The delta since the last review is trivial — a type cast removal that improves code quality without changing behavior. The security fix remains complete and well-tested. Ship it! 🚀

Note: Unable to submit formal APPROVE verdict due to GitHub App permissions. This is a recommendation to approve.

Reviewers (0)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
orchestrator-direct 0 0 0 0 0 0 0
Total 0 0 0 0 0 0 0

Note: Delta scope (1 line type cast removal + main merge) was trivial enough for direct orchestrator analysis without dispatching sub-reviewers.

@github-actions github-actions bot deleted a comment from claude bot Mar 30, 2026
@miles-kt-inkeep miles-kt-inkeep added this pull request to the merge queue Mar 30, 2026
Merged via the queue into main with commit 54b712b Mar 30, 2026
18 checks passed
@miles-kt-inkeep miles-kt-inkeep deleted the codex/fix-durable-execution-untrusted-header-vulnerability branch March 30, 2026 18:15
@inkeep
Copy link
Copy Markdown
Contributor

inkeep bot commented Mar 30, 2026

No documentation updates needed for this PR. This is an internal security fix that hardens header merging behavior without changing any public APIs or developer-facing behavior.

@itoqa
Copy link
Copy Markdown

itoqa bot commented Mar 30, 2026

Ito Test Report ❌

14 test cases ran. 1 failed, 13 passed.

Across 14 test cases, 13 passed and 1 failed, showing broad stability of run/chat routes, SSE and non-streaming behavior, mobile flow, MCP execution, and security hardening (trusted auth/scope headers resisted override and spoof attempts while additive forwarded metadata remained accepted, with no scope leakage after interrupted streaming). The key finding is one confirmed high-severity production reliability bug in /run/api/executions: parallel requests reusing the same conversationId can intermittently return HTTP 500 due to a pre-existing non-atomic read-then-insert conversation creation path lacking an atomic upsert/ON CONFLICT safeguard.

❌ Failed (1)
Category Summary Screenshot
Edge ⚠️ Parallel requests sharing one conversationId intermittently returned HTTP 500 due to a non-atomic conversation create path. EDGE-5
⚠️ Parallel same-conversation execution can return intermittent 500 errors
  • What failed: The endpoint intermittently returns HTTP 500 instead of consistently handling repeated same-conversation submissions (expected stable non-500 behavior under this stress pattern).
  • Impact: Concurrent client retries or burst traffic can fail unpredictably, causing execution drops for otherwise valid requests. This reduces reliability for real-time workloads that reuse conversation context.
  • Introduced by this PR: No – pre-existing bug (code not changed in this PR)
  • Steps to reproduce:
    1. Send 10 near-simultaneous POST requests to /run/api/executions with the same conversationId.
    2. Keep valid authenticated tenant/project/agent scope and include conflicting forwarded trusted headers to match adversarial stress traffic.
    3. Observe mixed outcomes where some requests succeed and others intermittently return HTTP 500.
  • Stub / mock context: Stress traffic used a local bypass bearer token and intentionally forged forwarded trusted headers while issuing 10 parallel requests with one shared conversationId. The setup simulated adversarial concurrency without mocked response payloads, which exposed real database write contention in conversation creation.
  • Code analysis: I reviewed the run execution route and conversation persistence path. The conversation persistence uses a read-before-insert flow and then a plain insert without an atomic upsert/ON CONFLICT guard, which allows a race when multiple requests create the same conversation simultaneously.
  • Why this is likely a bug: The production code uses a non-atomic check-then-insert flow on a primary-keyed conversation record, which can deterministically produce duplicate-key insert failures under parallel same-ID creation.

Relevant code:

packages/agents-core/src/data-access/runtime/conversations.ts (lines 163-180)

const existing = await db.query.conversations.findFirst({
  where: and(eq(conversations.tenantId, input.tenantId), eq(conversations.id, input.id)),
});

if (existing) {
  if (existing.activeSubAgentId !== input.activeSubAgentId) {
    await db
      .update(conversations)
      .set({
        activeSubAgentId: input.activeSubAgentId,
        updatedAt: new Date().toISOString(),
      })
      .where(eq(conversations.id, input.id));

packages/agents-core/src/data-access/runtime/conversations.ts (lines 198-199)

await db.insert(conversations).values(newConversation);
return newConversation;

packages/agents-core/src/db/runtime/runtime-schema.ts (lines 109-123)

export const conversations = pgTable(
  'conversations',
  {
    ...projectScoped,
    userId: varchar('user_id', { length: 256 }),
    agentId: varchar('agent_id', { length: 256 }),
    activeSubAgentId: varchar('active_sub_agent_id', { length: 256 }).notNull(),
    ref: jsonb('ref').$type<ResolvedRef>(),
    title: text('title'),
    lastContextResolution: timestamp('last_context_resolution', { mode: 'string' }),
    metadata: jsonb('metadata').$type<ConversationMetadata>(),
    ...timestamps,
  },
  (table) => [primaryKey({ columns: [table.tenantId, table.projectId, table.id] })]
);
✅ Passed (13)
Category Summary Screenshot
Adversarial Confirmed Authorization override attempts cannot poison internal A2A auth; trusted token/header precedence is preserved. ADV-1
Adversarial Baseline and spoofed forwarded project header requests both stayed stable at HTTP 200 SSE, with no cross-scope reroute observed. ADV-2
Adversarial Refresh/back-forward interruption plus spoofed-scope follow-up showed no scope leakage; spoofed project was rejected and valid scope continued. ADV-5
Edge Confirmed no product defect in header hardening; case-insensitive trusted-header collisions are blocked and targeted coverage passed. EDGE-1
Edge Confirmed selective merge behavior is correct; trusted collisions are ignored while benign additive headers are preserved. EDGE-2
Edge Omitted and empty forwardedHeaders variants behaved as no-op equivalents to baseline. EDGE-3
Edge Cookie precedence remained deterministic across cookie-only, forwarded-only, and dual-cookie requests. EDGE-4
Edge Mobile viewport run/chat flow and metadata-bearing request path remained functional after environment remediation. EDGE-6
Happy-path Durable baseline execution succeeded with SSE output and no forwarded header overrides. ROUTE-1
Happy-path Additive forwarded headers were preserved without trusted-header collision or auth/scope regression. ROUTE-2
Happy-path OpenAI-compatible SSE chat completed with HTTP 200 and streaming chunks after valid request payload. ROUTE-3
Happy-path Vercel chat JSON mode returned HTTP 200 with valid choices content and stop finish reason. ROUTE-4
Happy-path Source and tests confirm additive MCP header merge without trusted-header override regression. ROUTE-5

Commit: fa23e2c

View Full Run


Tell us how we did: Give Ito Feedback

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants