mcp-per-user-auth (#13): security review follow-ups (hardening + cleanup) #24
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Follow-up items from the security review of #13 (
feat/mcp-per-user-auth), merged as-is intodevelop(merge commitf72e44c9). No blocker auth bypass or credential leak was found; these are hardening / cleanup items.medium
Align
isSessionInitwith the SDKisInitializeRequest—isInitializeRequestBody(apps/server/src/integrations/mcp/mcp-auth.helpers.ts) only checksmethod === 'initialize', whilepackages/mcp/src/http.tsuses the SDK's fullInitializeRequestSchema.safeParse(requiresjsonrpc,id, validparams). An authenticated client can POST{"method":"initialize"}withoutparamsand nomcp-session-id:McpServicetreats it as session-init and runs the side-effectinglogin()(inserts auser_sessionsrow, writes aUSER_LOGINaudit event, bumpslastLoginAt), thenhttp.ts400s it without creating an MCP session. Result: repeatable audit-spam / session-table growth (authenticated abuse). Fix: reuse/import the SDKisInitializeRequestpredicate (or tightenisInitializeRequestBodyto requirejsonrpc+params.protocolVersion) so the session-minting side and the session-creating side use the identical signal.Document trusted-proxy requirement for the brute-force limiter —
FastifyAdapter({ trustProxy: true })(apps/server/src/main.ts) is unconditional, soclientIpderivesreq.ipfrom a client-forgeableX-Forwarded-For. The per-IP and per-IP+email limiter keys are spoofable when no trusted proxy sits in front. The global per-email key is the real backstop and is IP-independent. Action: document in.env.examplethat a trusted proxy / correcttrustProxyhop config is required for per-IP throttling to be meaningful (and consider makingtrustProxyconfigurable).low
Bind the Bearer path to the instance workspace —
verifyMcpBearer/verifyBearerAccess(apps/server/src/integrations/mcp/mcp.service.ts) verifies signature/exp/type=ACCESS + not-disabled user + active session, but does not checkpayload.workspaceIdagainst the MCP instance's workspace (unlikeJwtStrategywhich loads the workspace). Negligible for a single-workspace community build; in a multi-workspace deployment a token from another workspace could pass.User-enumeration timing oracle in
verifyUserCredentials—apps/server/src/core/auth/services/auth.service.ts: for a non-existent email it throws beforecomparePasswordHash; for an existing one it runs bcrypt. The timing difference reveals whether an email exists. Pre-existing (lifted fromlogin()), but now reachable via/mcpwhere throttling is the in-memory limiter (spoofable per-IP keys) rather than the controller's ThrottlerGuard.Cover the real SSO/MFA wiring in tests —
apps/server/src/integrations/mcp/mcp.service.spec.tsSSO/MFA tests stubenforceBasicGate, so they only assert the helper calls the gate beforelogin()/verifyCredentials. The actualMcpService.enforceBasicLoginGatewiring (validateSsoEnforcement+ lazy EE MFA require + readingmfaResultflags) is not instantiated by any test. Add coverage for the real gate (the most security-sensitive part).Filed from a post-merge security review. Source files:
apps/server/src/integrations/mcp/mcp-auth.helpers.ts,apps/server/src/integrations/mcp/mcp.service.ts,apps/server/src/core/auth/services/auth.service.ts,apps/server/src/main.ts,packages/mcp/src/http.ts.