Skip to content

v0.41.3.0 fix(security/mcp): OAuth CORS lockdown + pre-register without DCR + validator surface#1403

Merged
garrytan merged 4 commits into
masterfrom
garrytan/security-mcp-fix-wave
May 25, 2026
Merged

v0.41.3.0 fix(security/mcp): OAuth CORS lockdown + pre-register without DCR + validator surface#1403
garrytan merged 4 commits into
masterfrom
garrytan/security-mcp-fix-wave

Conversation

@garrytan

Copy link
Copy Markdown
Owner

Summary

Pre-register Claude and ChatGPT clients without --enable-dcr — the SECURITY.md-recommended setup actually works now.

The wave shipped three expanded cherry-picks (#894, #983, #817) plus codex outside-voice catches:

  • feat(auth): add --redirect-uri + --token-endpoint-auth-method to register-client #894 expandedgbrain auth register-client gets --redirect-uri (repeatable) and --token-endpoint-auth-method. Argv parser rewritten from indexOf-based lookahead (which silently dropped repeated flags) to a proper loop.
  • fix(serve-http): gate CORS preflight headers behind the Origin allowlist #983 expanded — Consolidated corsHeaders + corsPreflightHeaders in the legacy transport, AND closed the BIGGER bug codex found: the live Express OAuth server (/mcp, /token, /authorize, /register, /revoke) was using default-wide-open cors() middleware. Any web origin could complete a token exchange from a logged-in operator's browser. Now default-deny via GBRAIN_HTTP_CORS_ORIGIN allowlist.
  • feat(serve-http): GBRAIN_TRUST_PROXY env var + Supabase deploy docs #817 expandedGBRAIN_HTTP_TRUST_PROXY env on Express server (the legacy bearer transport already read this env var; the two transports now agree on a single source of truth). SECURITY.md rewritten to match reality — was lying that trust proxy was "disabled by default" while code hardcoded 'loopback'.
  • Validator surfaceALLOWED_TOKEN_ENDPOINT_AUTH_METHODS = {client_secret_post, client_secret_basic, none} gated at all three registration entry points (CLI, admin endpoint, DCR /register) so --enable-dcr is no longer the looser path. Plus an atomicity fix on the admin endpoint: pre-fix it did INSERT (confidential) → UPDATE (NULL out secret_hash) for public clients, which left a stranded confidential row if the UPDATE failed mid-flight (codex F4).

Defers PR #1316's "Phase 4 multi-agent hardening" cathedral. Its three real wins (deny-by-default fine-grained scopes, real operation names in mcp_request_log, access_tokens.last_used_at LRU debounce) are filed as TODOS T13a/T13b/T13c. Its RLS posture rewrite stays deferred because it changes the v0.26.7 auto-RLS event trigger that gbrain doctor's rls_event_trigger check treats as load-bearing.

Closes community PRs #685 (chipoto69, superseded by #1316), #876 (toilalesondev, already merged via v0.34.1.0 #996), #1076 (lukejduncan, already in master), #1077 (lukejduncan, already in master), #620 (ArshyaAI, already in master).

Test Coverage

183/183 directly-touched tests pass in 1.9s. Three new test files + extensions:

File Cases Notes
test/oauth.test.ts 91 (+18 new) v0.41.3 validator, registerClientManual atomic INSERT, DCR gate, client_secret_basic regression
test/http-transport.test.ts 28 (+4 IRON RULE regressions) CORS preflight matrix: preflight × allowlisted/non-allowlisted — pins the bug class
test/serve-http-trust-proxy.test.ts 15 (new) resolveTrustProxy() env value mapping
test/serve-http-cors.test.ts 13 (new) parseCorsAllowlistOAuth() + resolveCorsOrigin()
test/auth-register-client-args.test.ts 27 (new) repeatable --redirect-uri parser, smart --grant-types default
test/fix-wave-structural.test.ts 9 Updated to assert NEW atomic shape; regression guard pins post-insert UPDATE is gone
test/oauth-confidential-client.test.ts type-tightened for clientSecret?

Full local fast-loop suite (4 shards) hit infrastructure timeout on 2/4 shards at the per-shard 1500s cap — pre-existing macOS issue, not introduced by this wave. CI's per-matrix timing won't reproduce it.

Pre-Landing Review

Covered by the /plan-eng-review run that produced the plan file: 7 issues found across Step 0 scope challenge (4 PRs already shipped — #1076, #1077, #620, #876 — confirmed via master state inspection), Architecture (env naming D3, CORS DRY D4), Code Quality (validator scope D5), Tests (10 gaps; 5 IRON RULE regressions auto-included). All resolved via D1-D11 with user decisions captured in plan file. Plus codex outside-voice surfaced 7 substantive findings (F1-F7) that became D9/D10/D11 expansions — all integrated.

Verification Results

  • Local: bun run verify clean (typecheck + 4 shell pre-checks).
  • Local: 183 directly-touched tests pass in 1.9s.
  • Manual smoke commands documented in plan file + SECURITY.md update — operator runs gbrain auth register-client claude-ai --scopes "read write" --redirect-uri ... then GBRAIN_HTTP_CORS_ORIGIN=https://claude.ai gbrain serve --http; OPTIONS preflight from non-allowlisted origin returns no Allow-Methods header (was leaking pre-v0.41.3).

Plan Completion

D1-D11 all answered. T1-T10 + T12 done in commit. T11 (close 5 community PRs with attribution) deferred to post-merge per CLAUDE.md "Executing actions with care" — closing community PRs is irreversible external state and gets handled after this PR lands so close comments can reference the merged replacement.

TODOS

Three new entries filed under "v0.41.3 security/MCP fix wave follow-ups":

PR #1316's RLS posture rewrite deliberately NOT filed (load-bearing v0.26.7 doctor check, needs its own plan).

Test plan

  • bun run verify clean
  • 183 directly-touched tests pass in isolation
  • CI matrix all-green (this PR's GH Actions run)
  • Manual smoke against gbrain serve --http with claude.ai allowlisted

🤖 Generated with Claude Code

…t DCR, validator surface

Three expanded cherry-picks plus codex-surfaced live-CORS fix, parser
rewrite, atomicity fix, DCR validator gate, SECURITY.md reconciliation.

What ships
- gbrain auth register-client gets --redirect-uri (repeatable) and
  --token-endpoint-auth-method flags so the SECURITY.md-recommended
  "pre-register without --enable-dcr" path actually works for claude.ai
  and ChatGPT custom connectors.
- ALLOWED_TOKEN_ENDPOINT_AUTH_METHODS = {client_secret_post,
  client_secret_basic, none} validator gates all three registration
  entry points (CLI, admin endpoint, DCR /register) so --enable-dcr is
  no longer the looser path.
- Live Express OAuth server (/mcp, /token, /authorize, /register,
  /revoke) was using default-wide-open cors() middleware — every
  origin could complete a token exchange from a logged-in operator's
  browser. Now default-deny; allowlist via GBRAIN_HTTP_CORS_ORIGIN.
- GBRAIN_HTTP_TRUST_PROXY env var on Express server with the same
  semantics as the legacy bearer transport already had. Default
  'loopback' preserved. SECURITY.md doc rewritten to match reality
  (was lying that trust proxy was "disabled by default" while code
  hardcoded 'loopback').
- Admin endpoint registration now atomic — INSERT-then-UPDATE for
  public clients replaced with single INSERT via the new
  registerClientManual(..., tokenEndpointAuthMethod) parameter (codex
  outside-voice F4 catch).
- Legacy transport corsHeaders + corsPreflightHeaders consolidated
  into one function gated on the allowlist for BOTH Allow-Origin and
  Allow-Methods/Headers (codex F1; #983 thematically).

Surfaced by D7 codex outside-voice review on the v0.41.3 plan:
F1 (live Express CORS wide-open), F2 (indexOf parser couldn't do
repeatable flags), F3 (client_secret_basic missing from validator),
F4 (admin endpoint INSERT-then-UPDATE atomicity), F5 (DCR path
bypassed validator), F6 (env var already existed on legacy transport),
F7 (SECURITY.md vs impl doc disagreement).

Tests: 183 directly-touched cases green. Three new test files
(test/serve-http-trust-proxy.test.ts, test/serve-http-cors.test.ts,
test/auth-register-client-args.test.ts) + 18 new oauth.test.ts cases
+ 4 IRON RULE CORS preflight regressions.

Plan: ~/.claude/plans/system-instruction-you-are-working-wise-piglet.md
(D1-D11 captured, codex outside-voice integrated, GSTACK REVIEW REPORT
verdict CLEARED).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
garrytan and others added 3 commits May 24, 2026 22:17
…cp-fix-wave

# Conflicts:
#	CHANGELOG.md
#	VERSION
#	package.json
writer.log() uses real `new Date()` for filename computation, but the
test mocked `now` to 2026-05-22. When CI runs on a date in a different
ISO week (e.g. 2026-05-25 W22 vs the mocked W21), log() writes to one
file but readRecent(now) reads a different one — zero events overlap,
expect(2).toBe(0) fails.

Fix: write events directly to the file matching the test's mocked
`now` via writer.computeFilename(now), same pattern the cross-week
straddle test (line 234+) already used for the previous-week event.

Pre-existing test bug, surfaced when CI rolled past the week boundary
the original author wrote against. Not introduced by v0.41.3.0; fix
included here because /ship found it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…cp-fix-wave

# Conflicts:
#	CHANGELOG.md
#	VERSION
#	package.json
@garrytan garrytan merged commit 6af0c91 into master May 25, 2026
8 checks passed
garrytan added a commit that referenced this pull request May 25, 2026
….9.0

Master moved from v0.41.6.0 to v0.41.3.0 since the last ship:
- v0.41.2.0: lens packs + epistemology unification (#1364)
- v0.41.3.0: OAuth CORS lockdown + pre-register without DCR (#1403)

Master's v0.40.4.0+ audit-writer fix (ts-aware filename selection)
supersedes my v0.41.6.0 workaround in test/audit/audit-writer.test.ts.
Resolved conflict by keeping master's superior fix.

Version retarget per user request: 0.41.6.0 → 0.41.9.0 to claim a
clean slot beyond master's v0.41.3.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mgunnin added a commit to mgunnin/gbrain that referenced this pull request May 28, 2026
* upstream/master: (22 commits)
  v0.41.4.0 wave: local providers + cross-platform stdin + gateway-routed dream judge (6 community PRs) (garrytan#1377)
  v0.41.3.0 fix(security/mcp): OAuth CORS lockdown + pre-register without DCR + validator surface (garrytan#1403)
  v0.41.2.0 feat: lens packs + epistemology unification — atoms + concepts as first-class units, calibration profile widening, gstack-learnings bridge (garrytan#1364)
  v0.41.1.0 feat: eval-loop wave — gbrain bench publish + gbrain eval gate close the LOOP (garrytan#1352)
  v0.41.0.0 feat(minions): fleet you supervise (4 field bugs + cathedral) (garrytan#1367)
  v0.40.10.0 feat: content sanity defense — junk-pattern throw + oversize-skip-embed (garrytan#1351)
  v0.40.9.0 feat(chunker): .sql indexing via tree-sitter + code-def on SQL DDL (garrytan#1173) (garrytan#1350)
  v0.40.8.1 docs: README rewrite + personal-brain + company-brain tutorials (garrytan#1345)
  v0.40.8.0 test: e2e + unit gap coverage + master flake root-cause fixes (garrytan#1313)
  v0.40.6.1 docs(todos): file v0.41 wave commitments + 7 verified-missing items (garrytan#1333)
  v0.40.7.0 Schema Cathedral v3 — agent-on-ramp + production rebuild of PR garrytan#1321 (garrytan#1327)
  v0.40.6.0 feat(sync): parallel sync --all + per-source lock invariant + sources status dashboard (productionized from PR garrytan#1314) (garrytan#1324)
  v0.40.5.0 Federated Sync v2 — parallel source sync + push triggers + per-source health (garrytan#1322)
  v0.40.4.0 feat(search): selective graph signals + per-stage attribution + audit-writer unification (garrytan#1300)
  v0.40.3.0 feat: contextual retrieval + cache invalidation gate + 4 deferred-item closures (garrytan#1323)
  v0.40.2.0 feat: trajectory routing for temporal + knowledge_update (gbrain think + LongMemEval) (garrytan#1296)
  v0.40.1.0 Track D — eval infrastructure (catch retrieval regressions, prove answer-quality wins) (garrytan#1298)
  v0.40.0.0 feat: agent-voice (Mars + Venus) + copy-into-host-repo skillpack paradigm (garrytan#1128)
  v0.39.3.0: productionize the v0.38 ingestion cathedral (smoke-test fix wave from PR garrytan#1299) (garrytan#1308)
  v0.39.2.0 feat(autopilot): per-source fan-out + cycle lock primitive + phase taxonomy (garrytan#1295)
  ...
garrytan-agents pushed a commit to garrytan-agents/gbrain that referenced this pull request Jun 13, 2026
…ut DCR + validator surface (garrytan#1403)

* v0.41.3.0 fix(security/mcp): OAuth CORS lockdown, pre-register without DCR, validator surface

Three expanded cherry-picks plus codex-surfaced live-CORS fix, parser
rewrite, atomicity fix, DCR validator gate, SECURITY.md reconciliation.

What ships
- gbrain auth register-client gets --redirect-uri (repeatable) and
  --token-endpoint-auth-method flags so the SECURITY.md-recommended
  "pre-register without --enable-dcr" path actually works for claude.ai
  and ChatGPT custom connectors.
- ALLOWED_TOKEN_ENDPOINT_AUTH_METHODS = {client_secret_post,
  client_secret_basic, none} validator gates all three registration
  entry points (CLI, admin endpoint, DCR /register) so --enable-dcr is
  no longer the looser path.
- Live Express OAuth server (/mcp, /token, /authorize, /register,
  /revoke) was using default-wide-open cors() middleware — every
  origin could complete a token exchange from a logged-in operator's
  browser. Now default-deny; allowlist via GBRAIN_HTTP_CORS_ORIGIN.
- GBRAIN_HTTP_TRUST_PROXY env var on Express server with the same
  semantics as the legacy bearer transport already had. Default
  'loopback' preserved. SECURITY.md doc rewritten to match reality
  (was lying that trust proxy was "disabled by default" while code
  hardcoded 'loopback').
- Admin endpoint registration now atomic — INSERT-then-UPDATE for
  public clients replaced with single INSERT via the new
  registerClientManual(..., tokenEndpointAuthMethod) parameter (codex
  outside-voice F4 catch).
- Legacy transport corsHeaders + corsPreflightHeaders consolidated
  into one function gated on the allowlist for BOTH Allow-Origin and
  Allow-Methods/Headers (codex F1; garrytan#983 thematically).

Surfaced by D7 codex outside-voice review on the v0.41.3 plan:
F1 (live Express CORS wide-open), F2 (indexOf parser couldn't do
repeatable flags), F3 (client_secret_basic missing from validator),
F4 (admin endpoint INSERT-then-UPDATE atomicity), F5 (DCR path
bypassed validator), F6 (env var already existed on legacy transport),
F7 (SECURITY.md vs impl doc disagreement).

Tests: 183 directly-touched cases green. Three new test files
(test/serve-http-trust-proxy.test.ts, test/serve-http-cors.test.ts,
test/auth-register-client-args.test.ts) + 18 new oauth.test.ts cases
+ 4 IRON RULE CORS preflight regressions.

Plan: ~/.claude/plans/system-instruction-you-are-working-wise-piglet.md
(D1-D11 captured, codex outside-voice integrated, GSTACK REVIEW REPORT
verdict CLEARED).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(test): audit-writer readRecent calendar-boundary flake

writer.log() uses real `new Date()` for filename computation, but the
test mocked `now` to 2026-05-22. When CI runs on a date in a different
ISO week (e.g. 2026-05-25 W22 vs the mocked W21), log() writes to one
file but readRecent(now) reads a different one — zero events overlap,
expect(2).toBe(0) fails.

Fix: write events directly to the file matching the test's mocked
`now` via writer.computeFilename(now), same pattern the cross-week
straddle test (line 234+) already used for the previous-week event.

Pre-existing test bug, surfaced when CI rolled past the week boundary
the original author wrote against. Not introduced by v0.41.3.0; fix
included here because /ship found it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
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