Skip to content

fix(httpapi): refuse config-to-secret for values that embed a reference - #1782

Merged
Dumbris merged 2 commits into
smart-mcp-proxy:mainfrom
rudycelekli:fix/config-to-secret-embedded-ref-20261025-01
Oct 29, 2026
Merged

Dumbris merged 2 commits into
smart-mcp-proxy:mainfrom
rudycelekli:fix/config-to-secret-embedded-ref-20261025-01

Conversation

@rudycelekli

Copy link
Copy Markdown
Contributor

Bug

POST /api/v1/servers/{id}/config-to-secret refuses to convert a value that is already a reference, but the check is strings.HasPrefix(value, "${keyring:") || strings.HasPrefix(value, "${env:"). Two kinds of values get past it:

For these values the handler stores the literal text in the keyring and rewrites the field to ${keyring:<secret_name>}. Secret expansion runs once, so on the next connect the upstream gets the unresolved text, for example Bearer ${keyring:github-token} or ${file:/run/secrets/github-token}, as its credential.

The API masks both values on the read path because IsConfigReference only passes through an exact ${keyring:..} or ${env:..} value. The UI therefore shows them as plain secrets and offers Convert-to-secret.

Trigger

"headers": {"Authorization": "Bearer ${keyring:github-token}"}

POST /api/v1/servers/<name>/config-to-secret with {"scope":"header","key":"Authorization","secret_name":"gh2"} succeeds. After that, the header sent upstream is the literal Bearer ${keyring:github-token}.

Fix

Use secret.IsSecretRef(value), which matches any ${type:name} reference anywhere in the value. The handler then returns the existing 400 "already a reference" error for these values.

Test

Two cases added to TestHandleConvertConfigToSecret_ValidationErrors: an env value ${file:/run/secrets/github-token} and a header Bearer ${keyring:github-token}. Both must be rejected with 400 before the resolver is called.

Commands run

  • CGO_ENABLED=0 go test -count=1 ./internal/httpapi: ok
  • With server.go reverted, both new cases fail (expected: 400, actual: 500). The handler gets past validation and reaches the resolver, which the mock leaves nil.
  • go vet ./internal/httpapi, gofmt -l internal/httpapi/: clean

The guard only rejected values starting with ${keyring: or ${env:. A
${file:...} reference, or a composite such as "Bearer ${keyring:gh}",
was stored in the keyring as literal text and the field was rewritten
to ${keyring:<name>}. Resolution is single-pass, so the upstream then
received the unresolved reference text as its credential.

Use secret.IsSecretRef so any value containing a reference is refused.
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…a reference

Review finding (P2): the broader IsSecretRef guard sends mixed values such
as "postgres://app:hunter2@${env:DB_HOST}/app" to the 400 branch, which
echoed the full value and leaked its literal credential part even with
reveal_secret_headers off. The error now names only the scope and key.

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

Dumbris commented Oct 26, 2026

Copy link
Copy Markdown
Member

Verified the bug on main and the fix; thank you! The review found one gap, so I pushed a small maintainer commit on top of your branch: with the broader IsSecretRef guard, a mixed value like postgres://app:hunter2@${env:DB_HOST}/app reaches the 400 branch, which echoed the whole value and leaked its literal credential. The error now names only the scope and key.

I will merge once CI is green.

@Dumbris Dumbris left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Verified: the bug reproduces on main and the PR fixes it. The review found one gap, closed by the maintainer commit on top (regression test included). Race tests and CI lint for the touched packages are clean on this branch merged with main, and the result was cross-reviewed clean.

@Dumbris
Dumbris merged commit e138559 into smart-mcp-proxy:main Oct 29, 2026
48 checks passed
@Dumbris

Dumbris commented Oct 29, 2026

Copy link
Copy Markdown
Member

Merged. Thank you, @rudycelekli! Config-to-secret now refuses ${file:} and embedded references like Bearer ${keyring:x}, and the 400 no longer echoes the secret value back. I checked both cases on a live instance before merging.

Contributions like this are what keep MCPProxy moving, and they matter a lot to the project.

One small ask, only if you feel like it: a short public note about your change (a tweet, a LinkedIn post, a line in a community you are part of) helps more people discover MCPProxy. No pressure at all, and I would be glad to repost it.

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.

3 participants