A placeholder in a request body causes a block, but body substitution is off and you cannot set it from the CLI
Summary
microsandbox blocks a request when all of these conditions are true:
- The request body contains the placeholder of a secret.
- The request goes to a host that is not in the allow-list of that secret.
SecretInjection.bodyisfalse.
SecretInjection.body is false by default. You cannot set it from the msb CLI.
When body is false, microsandbox cannot put the real secret into a request body. Thus the placeholder in the body is only text. It is not a secret. The block does not stop a leak, but it stops the request.
The guest does not get the cause of the block. The guest gets only this error:
curl: (56) ... unexpected eof while readingThe effect is small for a tool that sends one request. The effect is large for an AI agent. An agent sends all of the conversation history in each request. If the history contains the placeholder one time, microsandbox blocks all of the requests after that. The user must start a new session.
Environment
msb0.6.8- Host: Zorin OS 18.1, Linux 7.0.0-28-generic, x86_64
- Guest image: Debian trixie (
ghcr.io/afrossard/container-base:0.0.9-agent) - Guest program: an AI agent CLI that sends requests to
api.anthropic.com
This is the launch command in short form:
msb run --net public \
--secret [email protected],api.github.com \
--on-secret-violation block-and-log \
<image> -- <command>api.anthropic.com is not in the allow-list of the secret.
How to repeat the problem
Send two requests to the same host. The two requests are the same, but only the second request contains the placeholder in the body.
# A: control. The body does not contain the placeholder.
curl -sS -o /dev/null -w 'http=%{http_code}\n' --max-time 15 \
-X POST -H 'content-type: application/json' \
--data '{"harmless":"text"}' \
https://api.anthropic.com/v1/messages
# B: the same request. The body contains the placeholder.
# In the guest, $GH_TOKEN holds the placeholder, not the secret.
curl -sS -o /dev/null -w 'http=%{http_code}\n' --max-time 15 \
-X POST -H 'content-type: application/json' \
--data "{\"harmless\":\"$GH_TOKEN\"}" \
https://api.anthropic.com/v1/messagesExpected result: microsandbox sends both requests. Body substitution is off. Thus the placeholder is only text, and no secret can go to the host.
Actual result:
--- the guest env holds the placeholder ---
GH_TOKEN=$MSB_GH_TOKEN
--- A: control. The body does not contain the placeholder. ---
http=401
curl_exit=0
--- B: the same request. The body contains the placeholder. ---
http=000
curl_exit=56
curl: (56) OpenSSL SSL_read: OpenSSL/3.5.6: error:0A000126:SSL routines::unexpected eof while reading, errno 0Request A gets HTTP 401. This is correct, because the request has no API key. It also shows that the connection works.
Request B fails. The body is the only difference between A and B.
This is the host log for request B:
WARN microsandbox_network::secrets::handler: secret violation: placeholder detected for
disallowed host action=block-and-log secret_env_var=GH_TOKEN placeholder=$MSB_GH_TOKEN
protocol=http/1.1 sni=api.anthropic.com host=api.anthropic.com method=POST
path=/v1/messages location=body match_form=raw guest_dst=160.79.104.10:443 http2_stream_id=The log shows location=body.
Why we think the block is not correct
We read these two files at main (cb7568f690b4):
packages/microsandbox-types/rust/lib/domain.rs:2016.SecretInjection.bodyhas#[serde(default)]. Thus the default value isfalse. Onlyheadersandbasic_autharetrueby default.crates/cli/lib/commands/common.rs. The functionparse_secretgives back only(env_var, hosts). The syntaxENV@HOST[,HOST...]has no field for the injection scopes. Thus a secret from the CLI always usesSecretInjection::default().
So for each secret that you make with the CLI, microsandbox cannot put the secret into a request body. But microsandbox finds the placeholder at location=body and blocks the request.
microsandbox looks for the placeholder in more locations than it can write the secret to. The block does not use this difference.
What we tested and removed as causes
- The network is good. Request A goes to the same host, the same port, and the same TLS session. It gets HTTP 401 immediately.
- DNS and egress are good. A and B run in the same sandbox at the same time. Only the body is different.
- TLS interception and CA trust are good. Request A uses the same interception path.
- The problem is not temporary. We saw it in two different agent sessions, some hours apart. We can also cause it at any time with the two commands above.
- The problem does not correct itself. All retries fail while the placeholder stays in the history. The client of the agent tries 10 times and then stops.
- This is not issue #1344. Issue #1344 has the same
curl (56)error, but the cause is the egress of the host. Here the egress is good, and the body causes the failure.
Effect on users
Usually the user does not type the placeholder. An AI agent reads a file that gives information about the --secret mechanism. For example, the notes of a project. The file content then goes into the conversation history of the agent. The client sends the history in each subsequent request. microsandbox then blocks each of these requests.
The agent looks like it stopped. It retries, it uses all of its retries, and then it shows a connection error. The error points to the network, not to a policy decision.
The guest does not get the deny events (see #691). Thus there is no data in the guest to help you find the cause. We examined this problem as a network fault for a long time before we found the warning on the host.
What we suggest
Do not block at a location where the injection is off for that secret. If
injection.bodyisfalse, microsandbox cannot substitute the secret in the body. Thus it is safe to send the request. It is satisfactory to write the event to the log. But it is not necessary to close the connection.If you use the same detection for all locations on purpose, let the guest see the cause of the block. At the moment the guest gets only a connection reset. This is related to issue #691.
A related limit in the CLI
ViolationPolicy.passthrough(hosts=[...]) gives the correct policy. It sends the placeholder to the given hosts and keeps block-and-log for the other hosts. The test fixture of the SDK uses api.anthropic.com as its example.
But --on-secret-violation accepts only four words. The word passthrough becomes Passthrough(vec![HostPattern::Any]). This is the all-hosts form. The Rust builder controls this form with passthrough_all_hosts(i_understand_the_risk).
Thus a CLI user has only two choices: block all hosts, or send the placeholder to all hosts. The user cannot set the scoped policy. If you prefer, we can put this in a different issue.
Source: superradcompany/microsandbox