fix(api): reject newline/CR/NUL in secret key to prevent secrets.env line injection#6300
Conversation
houko
left a comment
There was a problem hiding this comment.
One mechanical fix pushed: the 5-line comment block before the new key check was collapsed to a single line per the project comment rule ("never write multi-line comment blocks — one short line max"; CLAUDE.md).
Missing integration test (CLAUDE.md §Integration Testing)
CLAUDE.md requires a #[tokio::test] against TestServer for any route / wiring change:
Add a
#[tokio::test]againstTestServerin the matchingtests/*.rsfile. Pattern: spawn router viastart_test_server(), hit the endpoint withreqwest, assert status and response shape; for write endpoints, follow up with a read and assert the side effect.
The three tests added here are unit tests on upsert_secret directly (inside the source file's #[cfg(test)] block), not integration tests through the HTTP layer.
A test in crates/librefang-api/tests/ that calls PUT /api/secrets/env/FOO%0ABAR and expects a 400 / 422 would cover the route-level wiring (parameter extraction, handler dispatch, error serialisation) that the unit tests do not reach.
The unit tests are a good complement but cannot substitute for it per the project policy.
Generated by Claude Code
409d7ed to
0f14401
Compare
…line injection
routes::secrets_env::upsert_secret validated the secret value for newline/CR/NUL but the key check only rejected `=`, edge whitespace, and empty.
A key with an interior newline (e.g. `FOO\nBAR`) passed all checks, and the `format!("{key}={value}\n")` write emitted `FOO\nBAR=value`, injecting an extra `BAR=value` line into secrets.env — which is loaded into the process environment at boot and inherited by sidecar children.
The key is now rejected for newline/CR/NUL, mirroring the already-hardened routes::skills::write_secret_env path. Adds unit tests for the rejection and the well-formed happy path.
0f14401 to
a8739a0
Compare
Summary
routes::secrets_env::upsert_secretvalidated the secret value for\n/\r/\0(lines 24-33) but the key check (line 44) only rejected=, leading/trailing whitespace, and empty.A key containing an interior newline (e.g.
FOO\nBAR) passes every check —key.trim() != keyis false because the newline is not at an edge, andkey.contains('=')is false — so theout.push_str(&format!("{key}={value}\n"))write emits:injecting an extra
BAR=valueline intosecrets.env. That file is loaded into the process environment at daemon boot (librefang_extensions::dotenv::load_dotenv) and inherited by sidecar child processes, so the injected line becomes a live environment variable.This is a direct inconsistency with the sibling helper
routes::skills::write_secret_env, which already rejectskey.contains('\n') || key.contains('\r').Fix
Reject
\n/\r/\0in the key before the existing checks, mirroring the hardenedwrite_secret_envpath, so a key can never break theKEY=VALUEline framing.Tests
Adds an in-crate
#[cfg(test)]module:key_with_newline_is_rejected_and_does_not_inject_a_linekey_with_carriage_return_or_nul_is_rejectedwell_formed_key_still_writes_a_single_line(positive case)Verification
This host has no native Rust toolchain and no working container runtime, so local
cargoverification was not possible — CI is the gate. The change is a few lines of input validation plus self-contained unit tests. Reviewers / CI:cargo test -p librefang-api.Scope
One of several independent findings from a repo audit; filed as its own PR per the one-PR-one-domain policy.