Skip to content

fix: mcp adapter service exposes three critical http... in mcp-adapte...#1522

Open
orbisai0security wants to merge 3 commits intomoeru-ai:mainfrom
orbisai0security:fix-fix-v-003-mcp-adapter-auth
Open

fix: mcp adapter service exposes three critical http... in mcp-adapte...#1522
orbisai0security wants to merge 3 commits intomoeru-ai:mainfrom
orbisai0security:fix-fix-v-003-mcp-adapter-auth

Conversation

@orbisai0security
Copy link
Copy Markdown

Summary

Fix high severity security issue in services/twitter-services/src/adapters/mcp-adapter.ts.

Vulnerability

Field Value
ID V-003
Severity HIGH
Scanner multi_agent_ai
Rule V-003
File services/twitter-services/src/adapters/mcp-adapter.ts:439

Description: MCP adapter service exposes three critical HTTP endpoints (/sse for Server-Sent Events, /messages for message handling, and / for root access) without any authentication checks. The code at lines 4...

Changes

  • services/twitter-services/src/adapters/mcp-adapter.ts

Verification

  • Build passes
  • Scanner re-scan confirms fix
  • Code review passed

Automated security fix by OrbisAI Security

MCP adapter service exposes three critical HTTP endpoints (/sse for Server-Sent Events, /messages for message handling, and / for root access) without any authentication checks
Resolves V-003
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 89c29cbc0e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +440 to +443
const authToken = process.env.MCP_AUTH_TOKEN
if (authToken) {
const authHeader = event.node.req.headers['authorization']
if (!authHeader || authHeader !== `Bearer ${authToken}`) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Allow auth header for token-protected MCP routes

When MCP_AUTH_TOKEN is set, these handlers require an Authorization header, but the same router still sets Access-Control-Allow-Headers to only Content-Type in the CORS middleware (services/twitter-services/src/adapters/mcp-adapter.ts:427-431). In browser clients, preflight requests for authenticated /messages (and any authenticated fetch-based /sse calls) will be rejected before reaching this auth check, so valid clients cannot connect. Add Authorization (at minimum) to allowed CORS headers whenever token auth is enabled.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces authentication checks for the SSE, messages, and root endpoints within the MCP adapter, utilizing a Bearer token mechanism. A critical security review identified a timing attack vulnerability due to the use of standard string comparison for tokens. The feedback recommends implementing constant-time comparison using Node.js's crypto.timingSafeEqual, properly parsing the 'Bearer ' prefix, and validating environment variables upon service startup to ensure robustness.

Comment on lines +442 to +448
const authHeader = event.node.req.headers['authorization']
if (!authHeader || authHeader !== `Bearer ${authToken}`) {
logger.mcp.warn('Unauthorized MCP SSE connection attempt rejected')
event.node.res.statusCode = 401
event.node.res.end(JSON.stringify({ error: 'Unauthorized' }))
return
}
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.

security-critical critical

Using !== for comparing authentication tokens is vulnerable to timing attacks, which could allow an attacker to guess the token. Please use a constant-time comparison function like timingSafeEqual from Node's crypto module. Additionally, ensure that the MCP_AUTH_TOKEN environment variable is validated on startup to prevent runtime errors or undefined behavior, rather than relying on checks within the request handler. The current check should also be updated to handle the Bearer prefix correctly.

This vulnerability is present in all three authentication checks added in this PR. Please apply a similar fix to all of them.

You will need to add import { timingSafeEqual } from 'node:crypto'; at the top of the file.

        const authHeader = event.node.req.headers['authorization']
        const prefix = 'Bearer '

        let authorized = false
        if (authHeader?.startsWith(prefix)) {
          const providedToken = authHeader.substring(prefix.length)
          if (authToken.length === providedToken.length) {
            try {
              const expectedTokenBuf = Buffer.from(authToken)
              const providedTokenBuf = Buffer.from(providedToken)
              authorized = timingSafeEqual(expectedTokenBuf, providedTokenBuf)
            } catch {
              // Buffer.from can throw on invalid input. Treat as unauthorized.
              authorized = false
            }
          }
        }

        if (!authorized) {
          logger.mcp.warn('Unauthorized MCP SSE connection attempt rejected')
          event.node.res.statusCode = 401
          event.node.res.end(JSON.stringify({ error: 'Unauthorized' }))
          return
        }
References
  1. Validate environment variables on startup to ensure all required variables are present, preventing runtime errors from undefined values.

Copy link
Copy Markdown
Contributor

@Zazzik1 Zazzik1 left a comment

Choose a reason for hiding this comment

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

Hi, what do you think about this idea?

Comment on lines +439 to +448
router.get('/sse', defineEventHandler(async (event) => {
const authToken = process.env.MCP_AUTH_TOKEN
if (authToken) {
const authHeader = event.node.req.headers['authorization']
if (!authHeader || authHeader !== `Bearer ${authToken}`) {
logger.mcp.warn('Unauthorized MCP SSE connection attempt rejected')
event.node.res.statusCode = 401
event.node.res.end(JSON.stringify({ error: 'Unauthorized' }))
return
}
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.

Since the token validation logic is nearly identical across all three cases, what do you think about extracting it into a shared “guard”? This could improve maintainability and reduce the risk of someone updating it in one place while forgetting the others.

It could look like this (plus, we would need to address the part vulnerable to timing attacks):

Suggested change
router.get('/sse', defineEventHandler(async (event) => {
const authToken = process.env.MCP_AUTH_TOKEN
if (authToken) {
const authHeader = event.node.req.headers['authorization']
if (!authHeader || authHeader !== `Bearer ${authToken}`) {
logger.mcp.warn('Unauthorized MCP SSE connection attempt rejected')
event.node.res.statusCode = 401
event.node.res.end(JSON.stringify({ error: 'Unauthorized' }))
return
}
function defineProtectedEventHandler(label: string, handler: EventHandler<EventHandlerRequest, void>) {
return defineEventHandler((event) => {
const authToken = process.env.MCP_AUTH_TOKEN
if (authToken) {
const authHeader = event.node.req.headers.authorization
if (!authHeader || authHeader !== `Bearer ${authToken}`) {
logger.mcp.warn(`Unauthorized ${label} rejected`)
event.node.res.statusCode = 401
return { error: 'Unauthorized' }
}
}
return handler(event)
})
}
// SSE endpoint
router.get('/sse', defineProtectedEventHandler('MCP SSE connection attempt', async (event) => {

and on top of the file

import type { EventHandler, EventHandlerRequest } from 'h3'

Comment on lines +474 to +484
router.post('/messages', defineEventHandler(async (event) => {
const authToken = process.env.MCP_AUTH_TOKEN
if (authToken) {
const authHeader = event.node.req.headers['authorization']
if (!authHeader || authHeader !== `Bearer ${authToken}`) {
logger.mcp.warn('Unauthorized MCP message request rejected')
event.node.res.statusCode = 401
return { error: 'Unauthorized' }
}
}

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.

this would be simplified to

Suggested change
router.post('/messages', defineEventHandler(async (event) => {
const authToken = process.env.MCP_AUTH_TOKEN
if (authToken) {
const authHeader = event.node.req.headers['authorization']
if (!authHeader || authHeader !== `Bearer ${authToken}`) {
logger.mcp.warn('Unauthorized MCP message request rejected')
event.node.res.statusCode = 401
return { error: 'Unauthorized' }
}
}
router.post('/messages', defineProtectedEventHandler('MCP message request', async (event) => {

Comment on lines +516 to +525
router.get('/', defineEventHandler((event) => {
const authToken = process.env.MCP_AUTH_TOKEN
if (authToken) {
const authHeader = event.node.req.headers['authorization']
if (!authHeader || authHeader !== `Bearer ${authToken}`) {
logger.mcp.warn('Unauthorized MCP root request rejected')
event.node.res.statusCode = 401
return { error: 'Unauthorized' }
}
}
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.

and this would simplify to

Suggested change
router.get('/', defineEventHandler((event) => {
const authToken = process.env.MCP_AUTH_TOKEN
if (authToken) {
const authHeader = event.node.req.headers['authorization']
if (!authHeader || authHeader !== `Bearer ${authToken}`) {
logger.mcp.warn('Unauthorized MCP root request rejected')
event.node.res.statusCode = 401
return { error: 'Unauthorized' }
}
}
router.get('/', defineProtectedEventHandler('MCP root request', async (event) => {

import type { Context } from '../core/browser/context'
import type { Tweet } from '../core/services/tweet'
import type { TwitterServices } from '../types/services'

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.

for completeness - if we decide to implement the shared guard

Suggested change
import type { EventHandler, EventHandlerRequest } from 'h3'

@anupamme
Copy link
Copy Markdown

anupamme commented Apr 3, 2026

@orbisai0security can you address code review comments?

@orbisai0security
Copy link
Copy Markdown
Author

Changes Applied

I've updated the code based on your feedback:

Address all code review feedback on the MCP adapter security PR:

  1. Timing-attack fix (gemini-code-assist): Replace the vulnerable !== string comparison with a timingSafeStringEqual helper backed by Node's crypto.timingSafeEqual. This prevents an attacker from guessing the token character-by-character via response-time differences.

  2. CORS Authorization header (chatgpt-codex-connector): Add Authorization to Access-Control-Allow-Headers in the CORS middleware so browser preflight (OPTIONS) requests allow the Authorization header through, enabling browser-based clients to reach the now-protected endpoints.

  3. Shared auth guard (Zazzik1): Extract the identical per-route auth block into a defineProtectedEventHandler(label, handler) wrapper, following the reviewer's suggested API. This eliminates the triplication and ensures any future fix (e.g., token rotation) only needs to be applied in one place. Import EventHandler and EventHandlerRequest types from h3 as also suggested.

  4. Startup validation (gemini-code-assist): Emit a warning log in the constructor when MCP_AUTH_TOKEN is absent so operators know their endpoints are publicly accessible.

Files modified:

  • services/twitter-services/src/adapters/mcp-adapter.ts

The changes have been pushed to this PR branch. Please review!

@nekomeowww nekomeowww added scope/agent Scope related to how we harness agent, or build the agent workflow priority/urgent Issue, or Pull Request that urgent to be fixed or processed security Security issue, Pull Request, or advisory related. apps/stage-tamagotchi Desktop App: Windows & macOS & Linux labels Apr 7, 2026
if (authToken) {
const authHeader = event.node.req.headers['authorization']
const providedToken
= typeof authHeader === 'string' && authHeader.startsWith('Bearer ')
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.

LGTM, I'd just update this line, as the Bearer part ideally should be case-insensitive ("Bearer" is correct, but "bearer" and "BEARER" are correct as well)

https://www.rfc-editor.org/rfc/rfc7235.html#section-2.1

Suggested change
= typeof authHeader === 'string' && authHeader.startsWith('Bearer ')
= typeof authHeader === 'string' && authHeader.toLowerCase().startsWith("bearer ")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@orbisai0security can you address the code review comments?

@orbisai0security
Copy link
Copy Markdown
Author

Changes Applied

I've updated the code based on your feedback:

The only remaining unaddressed review comment is from @Zazzik1: the Authorization header's scheme token (Bearer) should be matched case-insensitively per RFC 7235. The current code uses authHeader.startsWith('Bearer '), which rejects valid headers like bearer <token> or BEARER <token>. The fix is to compare against the lowercased header (authHeader.toLowerCase().startsWith('bearer ')), while still slicing the actual token from the original (unmodified) authHeader — the prefix "bearer " is 7 characters just like "Bearer ", so slice(7) remains correct.

Files modified:

  • services/twitter-services/src/adapters/mcp-adapter.ts

The changes have been pushed to this PR branch. Please review!

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

Labels

apps/stage-tamagotchi Desktop App: Windows & macOS & Linux priority/urgent Issue, or Pull Request that urgent to be fixed or processed scope/agent Scope related to how we harness agent, or build the agent workflow security Security issue, Pull Request, or advisory related.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants