Skip to content

fix(credentials): fail closed on plaintext credential-reveal routes#271

Open
chitcommit wants to merge 2 commits into
mainfrom
claude/dreamy-jackson-f7f412
Open

fix(credentials): fail closed on plaintext credential-reveal routes#271
chitcommit wants to merge 2 commits into
mainfrom
claude/dreamy-jackson-f7f412

Conversation

@chitcommit

@chitcommit chitcommit commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

What

GET /api/credentials/:vault/:item/:field (credentials.js:573) and GET /api/credentials/twilio (:467) return broker-resolved secrets as plaintext in the response body. The only authorization gate was keyInfo.status === "active" at auth.js:142 — grep for scope across both files finds zero authorization checks. Any active API key could reveal any credential across the infrastructure, services, and integrations vaults.

This adds src/api/middleware/require-scope.js and applies it to both routes, deny-by-default.

Design decisions

  • Deny-by-default, no observe mode. Missing / empty / non-array scopes all reject. The sensitive-intent contract requires the credential path to fail closed; a log-only phase means the hole stays fully open for the window and tends not to get flipped.
  • Per-vault scopescredentials:reveal:{infrastructure,services,integrations}, derived from the same vault list the handler validates against. An unrecognised vault yields no scope and is denied rather than guessed at.
  • Namespaced apart from mcp:read|write|admin. Both vocabularies land in the same keyInfo.scopes array; mcp:admin must not satisfy a credential-reveal check by coincidence. There's an explicit test for this.
  • Self-describing 403 — the body carries required: "<scope>" so an under-provisioned caller can be reissued without reading source.

Scope-gating is harm reduction, not the cure

A scoped plaintext reveal is still a plaintext long-lived secret in a response body. No in-repo consumer of either route exists — server-side callers (github-actions.js:172, credential-provisioner-enhanced.js:27, credential-helper.js:18) use createCredentialBroker(env) in-process and are unaffected. Intended follow-up is deleting both routes once 403 telemetry confirms no external caller. Filed separately so gating doesn't quietly become the destination.

Validation

  • npx vitest run512 passed, 1 skipped, 0 failed
  • npx eslint on all three changed files → clean
  • Tests mount the real route module with no env bindings. The handlers need a credential broker and a DB, so a clean 403 is positive evidence the guard short-circuits ahead of the handler — not just that the middleware works in isolation.
  • No vi.mock() on any DB or service module; real Hono app, real Requests, real Responses.

Not in this PR (audited, filed separately)

Finding Location Status
API_KEYS.put() sets no expirationTtl — keys never expire, KV as authz source of truth src/middleware/mcp-auth.js:173,203 open, compounds this one
Broker short-circuited by raw env[X] truthiness on Secrets-Store bindings; resolveBinding() at thirdparty.js:890 already solves it and isn't used src/lib/credential-helper.js:68-81,101-121, src/intelligence/context-resolver.js:530-532 code-verified, runtime status unverified
failoverToEnvironment reads arbitrary env vars on broker failure — dead under current config, but CREDENTIAL_FAILOVER_ENABLED="true" in all 3 envs src/services/chittysecrets-connect-client.js:246-249 latent
MCP credential tools 404 (POST /api/credentials/retrieve, /audit — no such routes) src/mcp/tool-registry.js:962,985 drift
Plaintext reveal documented as intended behavior docs/SECRET_MANAGEMENT_ARCHITECTURE.md:81 needs revision
Both governance audit docs dated 2026-03-25, pre-decommission, reference vars absent from live wrangler.jsonc docs/governance/ stale

Refuted during audit, recorded so it isn't re-raised: MCP OAuth tokens do not escalate via the unwrapToken branch at auth.js:114. isOAuthProviderRoute() (index.js:2214) covers only /authorize, /token, /register and two .well-known paths, so /api/* uses app.fetch with raw env where OAUTH_PROVIDER is undefined.

Deploy note

Merging does not deploy via GitHub Actions — only binding-drift-audit.yml runs on push to main, and it's an audit. I could not verify whether a Cloudflare Workers Build is configured to deploy this repo on push (no CF API token in the authoring session). If one is, merging changes production auth behavior immediately. Confirm before merge.

Refs: operator decision 2026-07-25 (decision 5) — standalone remediation, deliberately not folded into the chittycan OAuth work.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added scope-based access controls for credential-reveal endpoints.
    • Added vault-specific permissions for credential access, including Twilio integrations.
    • Requests without the required permissions now receive clear 403 errors.
  • Bug Fixes

    • Unrecognized vaults are denied safely before credential handlers run.
    • Unauthorized requests no longer expose credential payload details.

GET /api/credentials/:vault/:item/:field and GET /api/credentials/twilio
returned broker-resolved secrets as plaintext in the response body, gated
only by `keyInfo.status === "active"` (src/api/middleware/auth.js:142).
There were no scope checks anywhere in either file, so any active API key
could reveal any credential across the infrastructure, services and
integrations vaults.

Adds src/api/middleware/require-scope.js and applies it to both routes:

- Deny-by-default. A key with no `scopes` field, an empty array, or a
  non-array value is rejected. There is no allow-with-warning mode; the
  sensitive-intent contract requires the credential path to fail closed,
  and a warning nobody reads is an open door.
- Per-vault scopes (`credentials:reveal:<vault>`) derived from the same
  vault list the handler validates against, so an unrecognised vault has
  no grantable scope and is denied rather than guessed at.
- Namespaced separately from the MCP OAuth vocabulary. Both land in the
  same `keyInfo.scopes` array, and `mcp:admin` must not satisfy a
  credential-reveal check by vocabulary coincidence.
- The 403 names the required scope so an under-provisioned caller can be
  reissued a correct key without reading this source.

Scope-gating is harm reduction, not the cure: a scoped plaintext reveal is
still a plaintext long-lived secret in a response body. No in-repo consumer
of either route exists (server-side callers use createCredentialBroker in
process and are unaffected), so the intended follow-up is deletion once 403
telemetry confirms no external caller. Tracked separately.

Tests mount the real route module with no env bindings — the handlers need
a broker and a DB, so a clean 403 is positive evidence the guard
short-circuits ahead of the handler.

Refs: operator decision 2026-07-25 (decision 5)
Co-Authored-By: Claude <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 25, 2026 02:56
@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.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 81b660c8-494f-4744-974b-3a5537a6dfde

📥 Commits

Reviewing files that changed from the base of the PR and between a0fd6c5 and 9a90039.

📒 Files selected for processing (3)
  • src/api/middleware/require-scope.js
  • src/api/routes/credentials.js
  • tests/api/require-scope.test.js

📝 Walkthrough

Walkthrough

Credential reveal authorization is added through shared scope middleware. Twilio requires the integrations reveal scope, while generic credential routes derive reveal scopes from validated vault names. Tests cover scope resolution, denial behavior, route wiring, and payload non-disclosure.

Changes

Credential reveal scope enforcement

Layer / File(s) Summary
Scope middleware and reveal scope contracts
src/api/middleware/require-scope.js
Adds reveal-scope construction, fixed and request-resolved middleware factories, scope normalization, audit logging, and structured 403 responses.
Credential route scope gates
src/api/routes/credentials.js
Protects the Twilio route with the integrations reveal scope and derives reveal scopes for recognized vaults on generic credential retrieval.
Middleware and route behavior tests
tests/api/require-scope.test.js
Covers allowed and denied scopes, unresolved vaults, legacy keys, actual credential route wiring, and denial payload contents.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant credentialsRoutes
  participant requireScopeFrom
  participant CredentialHandler
  Client->>credentialsRoutes: request credential endpoint
  credentialsRoutes->>requireScopeFrom: resolve vault reveal scope
  requireScopeFrom-->>Client: return 403 for unresolved or insufficient scope
  requireScopeFrom->>CredentialHandler: continue authorized request
  CredentialHandler-->>Client: return credential response
Loading

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: closing credential-reveal routes by default.
Description check ✅ Passed The description is detailed and on-topic; it covers purpose, scope, access changes, validation, and follow-up work, though it doesn't match the template exactly.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/dreamy-jackson-f7f412

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.

Copilot AI 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.

Pull request overview

Adds an explicit authorization layer (scope checks) to the plaintext credential-reveal endpoints under src/api/routes/credentials.js, so that “active key” is no longer sufficient to retrieve broker-resolved secrets. This fits into the API layer by extending the existing authenticate identity check with a deny-by-default authorization middleware and validating the wiring via real-route Vitest coverage.

Changes:

  • Introduces requireScope / requireScopeFrom middleware to enforce per-route/per-vault scopes (fail-closed).
  • Wires scope enforcement onto GET /api/credentials/twilio and GET /api/credentials/:vault/:item/:field.
  • Adds behavioral tests that exercise both the middleware directly and the real credentials route module mounting.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
tests/api/require-scope.test.js Adds behavioral tests for fixed-scope and derived-scope enforcement, plus real route wiring checks.
src/api/routes/credentials.js Applies scope enforcement middleware to the two plaintext credential-reveal GET routes; centralizes valid vault list.
src/api/middleware/require-scope.js Adds deny-by-default scope enforcement middleware and helper utilities for credential reveal scope vocabulary.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +13 to +15
* allow-with-warning mode — per the sensitive-intent contract
* (~/.ch1tty/canon/system-wide-sensitive-intent-contract-v1.md) the credential
* path fails closed, and a warning nobody reads is an open door.
Comment on lines +28 to +32
/**
* Vaults the credential routes expose. Kept in sync with the `validVaults`
* check in ../routes/credentials.js — a vault absent here has no reachable
* scope and is therefore denied, which is the correct direction to fail.
*/
Comment on lines +110 to +123
async function enforce(c, next, scope) {
const keyInfo = c.get("apiKey") || {};
const granted = scopesOf(c);
const allowed = granted.includes(scope);

console.log(
JSON.stringify({
tag: "scope-audit",
scope,
allowed,
keyType: keyInfo.type || "api-key",
service: keyInfo.service || keyInfo.name || "unknown",
path: c.req.path,
}),
@chitcommit

Copy link
Copy Markdown
Contributor Author

Governance gap found while verifying this PR's own green checks

governance and gates initially failed here, then passed after merging main forward. I checked why rather than accepting the green, and the reason is a gap worth filing.

Both gates classify a change as security-critical by path prefix:

  • .github/workflows/governance-checks.yml:41.github/workflows/, wrangler.toml, etc/chittyos/, src/auth/, public/openapi.json
  • gates.github/workflows/, wrangler., etc/, src/middleware/auth

Neither matches src/api/middleware/auth.js — the file that actually performs API-key authorization for every /api/* route, and whose sole check (keyInfo.status === "active", line 142) is the defect this PR exists to fix. src/auth/ does not match it, and src/middleware/auth does not either — the real path has api/ in the middle.

Also unmatched by both lists:

  • src/api/routes/credentials.js — the plaintext credential-reveal routes
  • src/middleware/mcp-auth.js — API key issuance (generateAPIKey, revokeAPIKey)
  • src/lib/credential-helper.js, src/lib/credential-broker.js — credential resolution

Net effect: a PR that rewrites the credential authorization path requires zero security-approval labels. This PR is the demonstration — it changes exactly that surface and both gates are green.

The two gates also disagree with each other (src/auth/ vs src/middleware/auth, etc/chittyos/ vs etc/), which suggests they drifted rather than being maintained as one policy.

Suggested fix, as a separate PR: consolidate to one shared critical-path list and extend it to cover src/api/middleware/, src/api/routes/credentials.js, src/middleware/mcp-auth.js, and src/lib/credential-*.

I have not applied the security-approved / docs-approved / access-reviewed labels to this PR. Self-applying a security sign-off would be the same class of control-bypass this PR is closing — those need a human.

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.

2 participants