Skip to content

fix: gateway security audit — SQL injection, auth bypass, service bindings - #34

Merged
chitcommit merged 3 commits into
mainfrom
fix/gateway-security-audit
Jun 13, 2026
Merged

fix: gateway security audit — SQL injection, auth bypass, service bindings#34
chitcommit merged 3 commits into
mainfrom
fix/gateway-security-audit

Conversation

@chitcommit

@chitcommit chitcommit commented Mar 28, 2026

Copy link
Copy Markdown
Contributor

Summary

  • P0 SQL injection: chronicleTimeline used sql.unsafe() with string interpolation — replaced with parameterized queries
  • P0 Auth bypass: Public mode fallback silently allowed unauthenticated access when no secrets configured — now rejects
  • P1 Service bindings: Added 5 same-zone Worker bindings to fix route-to-route fetch (CF docs require bindings, not URL fetch, for Workers on the same zone)
  • P1 Route conflict: Removed mcp.chitty.cc/* (owned by chittyconnect), assigned mcp-chatgpt.chitty.cc/*
  • P2 Compliance: Renamed chatgpt-mcp-gatewaychittymcp-gateway, updated compat date, added observability, standardized health endpoint

Test plan

  • Verify chronicleTimeline tool works with parameterized queries (entityId, date range, service filter)
  • Confirm unauthenticated requests to /mcp/message return 401 (not 200 public mode)
  • Validate service bindings resolve correctly in staging deploy (wrangler deploy --dry-run passes)
  • Check no route conflict with connect.chitty.cc MCP endpoints
  • Verify health endpoint returns {"status":"ok","service":"chittymcp-gateway"}

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Enabled observability for the gateway service.
    • Added inter-service communication bindings to connect with multiple platform services.
  • Bug Fixes

    • Authentication now rejects requests when the server lacks proper credential configuration.
  • Chores

    • Updated service identifier and version to 1.1.0 in API responses.
    • Updated deployment route hostnames for staging and production environments.
    • Added status badges to documentation.

chitcommit and others added 2 commits March 24, 2026 02:44
Adds standard ChittyOS branding header with tier badge, org badge,
and accurate service description to README.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…dings

P0: Fix SQL injection in chronicleTimeline — replaced sql.unsafe() string
interpolation with parameterized tagged template queries for all user input
(entityId, startDate, endDate, services).

P0: Remove public mode auth fallback — requests are now rejected when
MCP_API_KEY or OAUTH_CLIENT_ID/SECRET are not configured, instead of
silently allowing unauthenticated access.

P1: Add service bindings for same-zone Workers (SVC_CHITTYID, SVC_CHITTYAUTH,
SVC_CHITTYCONNECT, SVC_CHITTYROUTER, SVC_CHITTYREGISTRY) to fix route-to-route
fetch issue per CF docs. executeHttpTool now routes through bindings with
direct fetch fallback for external services only.

P1: Remove mcp.chitty.cc/* route conflict (owned by chittyconnect), assign
own hostname mcp-chatgpt.chitty.cc/*.

P2: Rename worker chatgpt-mcp-gateway → chittymcp-gateway, update
compatibility_date to 2026-03-16, add [observability] block, standardize
health endpoint to status:"ok" format.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@github-actions

Copy link
Copy Markdown

@coderabbitai review
@copilot review
Adversarial review request: evaluate security, policy bypass paths, and regression risk.

@coderabbitai

coderabbitai Bot commented Mar 28, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@chitcommit, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 47 minutes and 31 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bcd0d3c5-c116-414f-bcd4-4b75f3c7ed83

📥 Commits

Reviewing files that changed from the base of the PR and between 33bd1ab and ec85752.

📒 Files selected for processing (3)
  • mcp-gateway- chatgpt/src/index.js
  • mcp-gateway- chatgpt/src/services/tool-executor.js
  • mcp-gateway- chatgpt/wrangler.toml
📝 Walkthrough

Walkthrough

This pull request rebrands the service from "ChatGPT MCP Gateway" to "ChittyMCP Gateway", updates infrastructure bindings for inter-service communication via Cloudflare Workers, strengthens authentication validation logic, and refactors SQL query construction. Version bumped to 1.1.0 with updated API response fields and routing patterns.

Changes

Cohort / File(s) Summary
Documentation & Branding
README.md
Added Shields.io badges indicating ChittyOS service and Tier 3 operational status.
API Response Updates
mcp-gateway-chatgpt/src/index.js
Updated health check and root endpoints: changed status from 'healthy' to 'ok', updated service identifier to 'chittymcp-gateway', and bumped version from '1.0.0' to '1.1.0'.
Authentication Enforcement
mcp-gateway-chatgpt/src/lib/auth.js
Modified authenticateRequest to reject requests with error when neither API key nor OAuth credentials are configured; previously allowed public access with a warning.
Service Execution & Query Logic
mcp-gateway-chatgpt/src/services/tool-executor.js
Added SERVICE_BINDING_MAP to prefer same-zone service bindings for tool execution with fallback to direct fetch; updated User-Agent header to ChittyMCP-Gateway/1.0; refactored chronicleTimeline to use parameterized SQL queries for date/service filtering instead of unsafe dynamic construction, with explicit entityId validation.
Infrastructure & Configuration
mcp-gateway-chatgpt/wrangler.toml
Renamed Worker to chittymcp-gateway, advanced compatibility date to 2026-03-16, added 5 inter-service bindings (SVC_CHITTYID, SVC_CHITTYAUTH, SVC_CHITTYCONNECT, SVC_CHITTYROUTER, SVC_CHITTYREGISTRY), updated route patterns to include -chatgpt in hostnames, enabled observability, and updated secret expectations from service tokens to OAuth credentials.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Chitty, chitty, bang bang, the gateway's got a new name!
Service bindings tied tight, observability's the game,
SQL queries now safe, auth holds the line,
From gateway to ChittyMCP—version one-point-one so fine!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main objectives: it directly reflects the three primary security fixes (SQL injection, auth bypass, service bindings) mentioned in the PR objectives.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/gateway-security-audit

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
mcp-gateway- chatgpt/src/index.js (1)

53-70: ⚠️ Potential issue | 🟡 Minor

MCP discovery document references old hostname mcp.chitty.cc.

The issuer, authorization_endpoint, and token_endpoint fields still point to mcp.chitty.cc, but the worker now serves mcp-chatgpt.chitty.cc. OAuth clients relying on this discovery document may fail if these endpoints are unreachable at the old hostname or if issuer validation is strict.

🔧 Suggested fix
   return c.json({
     schema_version: '2024-11-05',
-    issuer: 'https://mcp.chitty.cc',
-    authorization_endpoint: 'https://mcp.chitty.cc/oauth/authorize',
-    token_endpoint: 'https://mcp.chitty.cc/oauth/token',
+    issuer: 'https://mcp-chatgpt.chitty.cc',
+    authorization_endpoint: 'https://mcp-chatgpt.chitty.cc/oauth/authorize',
+    token_endpoint: 'https://mcp-chatgpt.chitty.cc/oauth/token',
     scopes_supported: ['mcp:read', 'mcp:write'],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcp-gateway-` chatgpt/src/index.js around lines 53 - 70, The discovery
document returned by the route app.get('/.well-known/mcp.json', (c) => { ... })
still hardcodes the old hostname in the issuer, authorization_endpoint, and
token_endpoint fields; update those fields to use the current service hostname
(mcp-chatgpt.chitty.cc) or derive the host dynamically from the request/context
so the discovery JSON matches the actual worker domain and avoids mismatched
issuer/endpoints.
mcp-gateway- chatgpt/src/services/tool-executor.js (1)

247-256: ⚠️ Potential issue | 🟠 Major

chronicleSearch still uses sql.unsafe() — SQL injection risk remains.

The chronicleSearch function at line 253 still uses sql.unsafe(whereClause) with string interpolation. The whereClauses array is built with positional placeholders ($1, $2, etc.) and params array, but this pattern bypasses the tagged template's automatic escaping. Unlike chronicleTimeline, this function was not refactored.

🛡️ Suggested refactor using conditional tagged templates

Apply a similar pattern to what was done for chronicleTimeline:

 async function chronicleSearch(sql, args) {
   const { query, filters = {}, limit = 100, offset = 0 } = args;
 
-  let whereClauses = [];
-  let params = [];
-
-  // Add filters
-  if (filters.service) {
-    whereClauses.push(`service = $${params.length + 1}`);
-    params.push(filters.service);
-  }
-  // ... other filters ...
-
-  const whereClause = whereClauses.length > 0 ? `WHERE ${whereClauses.join(' AND ')}` : '';
-
-  const results = await sql`
-    SELECT id, service, action, entity_id, user_id, status, metadata, created_at,
-           ts_rank(search_vector, to_tsquery('english', ${query || ''})) as relevance
-    FROM chronicle_events
-    ${whereClause ? sql.unsafe(whereClause) : sql``}
-    ORDER BY ${query ? sql`relevance DESC,` : sql``} created_at DESC
-    LIMIT ${limit} OFFSET ${offset}
-  `;
+  // Build explicit parameterized query branches similar to chronicleTimeline
+  // This is more verbose but eliminates sql.unsafe() usage

Alternatively, consider using a query builder library that safely composes WHERE clauses.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcp-gateway-` chatgpt/src/services/tool-executor.js around lines 247 - 256,
chronicleSearch currently injects a raw string via sql.unsafe(whereClause) which
bypasses parameter escaping; replace that with composing the WHERE clause as an
array of tagged-template fragments (the existing whereClauses array should hold
sql`...` fragments or be rebuilt into them) and then inject it safely using the
tagged template helpers (e.g. sql.join(whereClauses, sql` AND `)) instead of
sql.unsafe; remove the string interpolation of whereClause, ensure the
params/values are passed as template values to sql so limit, offset, query, and
any positional parameters are properly escaped, and keep the conditional logic
that omits the WHERE section when whereClauses is empty.
🧹 Nitpick comments (1)
mcp-gateway- chatgpt/src/lib/auth.js (1)

98-125: Timing-safe comparison has an early exit on length mismatch.

The constantTimeCompare function returns false immediately when string lengths differ (line 108-110). This leaks timing information about whether the lengths match. While not exploitable for most API key formats, consider using a constant-time approach for length comparison as well.

🔒 Optional: Pad or hash before length check

One approach is to always hash both inputs regardless of length, letting the hash comparison be the sole timing-safe check:

 async function constantTimeCompare(a, b) {
   if (typeof a !== 'string' || typeof b !== 'string') {
     return false;
   }
 
-  // Use Web Crypto API for constant-time comparison
   const encoder = new TextEncoder();
   const aBytes = encoder.encode(a);
   const bBytes = encoder.encode(b);
 
-  if (aBytes.length !== bBytes.length) {
-    return false;
-  }
-
   // Hash both strings and compare hashes
   const aHash = await crypto.subtle.digest('SHA-256', aBytes);
   const bHash = await crypto.subtle.digest('SHA-256', bBytes);
 
   const aArray = new Uint8Array(aHash);
   const bArray = new Uint8Array(bHash);
 
   let result = 0;
   for (let i = 0; i < aArray.length; i++) {
     result |= aArray[i] ^ bArray[i];
   }
 
   return result === 0;
 }

Note: This changes semantics slightly (different-length strings with same prefix hash differently), but for credential comparison this is acceptable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcp-gateway-` chatgpt/src/lib/auth.js around lines 98 - 125, The function
constantTimeCompare currently returns early when lengths differ (aBytes.length
!== bBytes.length); remove that early return and instead perform the same
hashing-and-bytewise comparison for both inputs so timing does not reveal length
differences: always encode and call crypto.subtle.digest on a and b (e.g., using
TextEncoder -> aBytes/bBytes -> digest -> Uint8Array) and compare the resulting
hash bytes with the existing XOR-accumulate loop (result |= aArray[i] ^
bArray[i]) to decide equality; if you must normalize lengths, do so
deterministically (pad or hash full input) before hashing so the length check is
not an early exit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@mcp-gateway-` chatgpt/src/index.js:
- Around line 53-70: The discovery document returned by the route
app.get('/.well-known/mcp.json', (c) => { ... }) still hardcodes the old
hostname in the issuer, authorization_endpoint, and token_endpoint fields;
update those fields to use the current service hostname (mcp-chatgpt.chitty.cc)
or derive the host dynamically from the request/context so the discovery JSON
matches the actual worker domain and avoids mismatched issuer/endpoints.

In `@mcp-gateway-` chatgpt/src/services/tool-executor.js:
- Around line 247-256: chronicleSearch currently injects a raw string via
sql.unsafe(whereClause) which bypasses parameter escaping; replace that with
composing the WHERE clause as an array of tagged-template fragments (the
existing whereClauses array should hold sql`...` fragments or be rebuilt into
them) and then inject it safely using the tagged template helpers (e.g.
sql.join(whereClauses, sql` AND `)) instead of sql.unsafe; remove the string
interpolation of whereClause, ensure the params/values are passed as template
values to sql so limit, offset, query, and any positional parameters are
properly escaped, and keep the conditional logic that omits the WHERE section
when whereClauses is empty.

---

Nitpick comments:
In `@mcp-gateway-` chatgpt/src/lib/auth.js:
- Around line 98-125: The function constantTimeCompare currently returns early
when lengths differ (aBytes.length !== bBytes.length); remove that early return
and instead perform the same hashing-and-bytewise comparison for both inputs so
timing does not reveal length differences: always encode and call
crypto.subtle.digest on a and b (e.g., using TextEncoder -> aBytes/bBytes ->
digest -> Uint8Array) and compare the resulting hash bytes with the existing
XOR-accumulate loop (result |= aArray[i] ^ bArray[i]) to decide equality; if you
must normalize lengths, do so deterministically (pad or hash full input) before
hashing so the length check is not an early exit.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fd2c3d81-1df5-490e-9a23-f96d4ddf2507

📥 Commits

Reviewing files that changed from the base of the PR and between b75d753 and 33bd1ab.

📒 Files selected for processing (5)
  • README.md
  • mcp-gateway- chatgpt/src/index.js
  • mcp-gateway- chatgpt/src/lib/auth.js
  • mcp-gateway- chatgpt/src/services/tool-executor.js
  • mcp-gateway- chatgpt/wrangler.toml

@chitcommit
chitcommit enabled auto-merge (squash) June 13, 2026 21:25
@chitcommit
chitcommit merged commit 7a94b45 into main Jun 13, 2026
9 of 10 checks passed
@chitcommit
chitcommit deleted the fix/gateway-security-audit branch June 13, 2026 21:29
@github-actions

Copy link
Copy Markdown

@coderabbitai review
@copilot review
Adversarial review request: evaluate security, policy bypass paths, and regression risk.

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== DIRECTORY STRUCTURE ==="
tree mcp-gateway-chatgpt/ 2>/dev/null || find . -type f | grep -v node_modules | grep -v .git | head -60

echo ""
echo "=== wrangler.toml ==="
cat mcp-gateway-chatgpt/wrangler.toml 2>/dev/null || fd -i "wrangler.toml" --exec cat {}

echo ""
echo "=== src/index.js ==="
cat mcp-gateway-chatgpt/src/index.js 2>/dev/null || fd -i "index.js" --exec cat {}

echo ""
echo "=== src/lib/auth.js ==="
cat mcp-gateway-chatgpt/src/lib/auth.js 2>/dev/null || fd -i "auth.js" --exec cat {}

echo ""
echo "=== src/services/tool-executor.js ==="
cat mcp-gateway-chatgpt/src/services/tool-executor.js 2>/dev/null || fd -i "tool-executor.js" --exec cat {}

Length of output: 60291



@github-actions[bot] — adversarial review complete. I am replying to the latest comment only and am not aware of any earlier conversation thread.


Adversarial Security Review — PR #34

Summary verdict: the PR introduces or preserves five independently exploitable issues, two of which would have been present on the first production deploy.


🔴 P0 — Runtime Crash: chronicleTimeline always throws ReferenceError

tool-executor.js — return statement after the fix:

return {
  success: true,
  entityId,
  timeline: results,
  count: results.length,
  timeframe: { startDate, endDate },
  services: servicesOrNull,   // ← never declared anywhere
  groupBy: groupBy || null    // ← removed from destructuring, still read here
};

servicesOrNull is not defined anywhere in the file. groupBy was intentionally removed from the destructured args but was left in the return object. Every call to chronicle_timeline will throw a ReferenceError before returning results, making the SQL injection fix moot — the query runs but the response handler crashes. This is a regression introduced by this PR.

Fix:

return {
  success: true,
  entityId,
  timeline: results,
  count: results.length,
  timeframe: { startDate, endDate },
  services: services || null,
};

🔴 P0 — Auth Bypass Incomplete: validateAccessToken is a prefix check, not a real validation

src/lib/oauth.js:

export async function validateAccessToken(token, env) {
  if (!token || !token.startsWith('mcp_token_')) {
    // ... fallback to MCP_API_KEY
  }
  // If token starts with mcp_token_ — unconditionally accepted:
  return {
    valid: true,
    clientId: 'oauth_client'
  };
}

Any string beginning with mcp_token_ is accepted as a valid OAuth token with no lookup, no expiry check, no revocation check, and no cryptographic verification. The comment in the function body acknowledges tokens should be stored in KV/D1 but that was never implemented. Since auth.js tries OAuth validation first, this is a working bypass: Authorization: Bearer mcp_token_anything will always succeed even with OAUTH_CLIENT_ID/OAUTH_CLIENT_SECRET configured. The P0 auth bypass removal (public-mode fallback) was addressed, but this secondary bypass was left open.


🔴 P0 — SQL Injection Fix Is Incomplete: chronicleSearch still uses sql.unsafe()

tool-executor.js, chronicleSearch:

const whereClause = whereClauses.length > 0 ? `WHERE ${whereClauses.join(' AND ')}` : '';

const results = await sql`
  SELECT …
  FROM chronicle_events
  ${whereClause ? sql.unsafe(whereClause) : sql``}   // ← still unsafe
  ORDER BY …
`;

sql.unsafe() is still present in chronicleSearch. The whereClauses array is built with $N positional placeholders but params is a plain array — it is never passed to the Neon tagged-template. The unsafe clause string is injected directly into the query. An attacker controlling filters.service, filters.action, filters.entityId, etc. can inject SQL. The fix applied to chronicleTimeline was not applied to chronicleSearch.


🔴 P1 — Production Service Bindings Missing from [env.production]

wrangler.toml contains an explicit comment:

# NOTE: Cloudflare named environments do NOT inherit top-level [[services]],
# [[kv_namespaces]], or [[d1_databases]] — they must be re-declared per env.

[env.staging] correctly redeclares all five [[env.staging.services]] entries. [env.production] does not:

[env.production]

[env.production.vars]


[[env.production.routes]]
pattern = "mcp-chatgpt.chitty.cc/*"
zone_name = "chitty.cc"

In production, env.SVC_CHITTYID, env.SVC_CHITTYAUTH, etc. will all be undefined. executeHttpTool will fall through to direct fetch (with a console.warn), defeating the entire P1 service-bindings fix.

Additionally, wrangler.toml appears to contain concatenated stale fragments from at least two other workers (chronicle-mcp-http, chittymcp-extension-121) starting after the [observability] block — this will cause a wrangler deploy parse error or unpredictable behavior.


🟡 P1 — Timing Oracle in validateClientCredentials

src/lib/oauth.js:

if (clientId !== expectedClientId) {
  return { valid: false, error: 'Invalid client_id' };
}
if (clientSecret !== expectedClientSecret) {
  return { valid: false, error: 'Invalid client_secret' };
}

JavaScript !== on strings is not constant-time. Compare against the correct pattern already used in auth.js (constantTimeCompare). This also leaks which field is wrong — separate error messages confirm to an attacker that they have a correct client_id, narrowing the brute-force surface to client_secret only.


🟡 P2 — SSE Endpoint: No Rate Limiting / Resource Exhaustion

src/index.js, /mcp/sse:

setTimeout(cleanup, 300000); // Close after 5 minutes
const interval = setInterval(() => {  }, 30000);

Authenticated requests hold a Worker CPU execution context for up to 5 minutes with a live 30-second ping interval. Cloudflare Workers have a 30-second CPU wall-clock limit on the free tier and 15 minutes on Paid, but streaming responses still consume subrequest budget. No concurrent connection limit or per-IP rate limit is applied, making this trivially exploitable for resource exhaustion once a valid mcp_token_ token is obtained (which, per finding #2, requires no secret knowledge).


Summary Table

Severity Finding File
🔴 P0 ReferenceError crash in chronicleTimeline return — regression tool-executor.js
🔴 P0 validateAccessToken prefix-only check — auth bypass survives oauth.js
🔴 P0 sql.unsafe() in chronicleSearch — SQL injection unfixed tool-executor.js
🔴 P1 [env.production] missing service binding redeclarations + corrupted wrangler.toml tail wrangler.toml
🟡 P1 !== timing oracle + split error messages in validateClientCredentials oauth.js
🟡 P2 SSE 5-min connection hold, no rate limit index.js

This PR should not be merged in its current state. The three P0 items are independently sufficient to block.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant