[Detail Bug] Request logs can be forged via control characters in fetch object keys (log injection)
Maintainers usually reply within 1 day
A pull request for this has already been merged.
- #214 by @shikhar — merged
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 82/100
Research direction
Start in src/types.rs by comparing ObjectKey::new with the control-character checks in BucketName::new and ObjectKind::new. Add a unit test there for an object key containing a control character, then run the relevant types tests. Done means ObjectKey::new rejects control characters while preserving its existing empty and length validation.
Written by the indexing model from the issue text.
Description
Detail Bug Report
Introduced in #84 by @shikhar on Feb 20, 2026
Summary
- Context:
ObjectKeyis the user-supplied object key extracted from the path of the publicGET /fetch/{kind}/{*object}endpoint of the Cachey caching service, and is rendered into request log lines via itsDisplayimplementation. - Bug:
ObjectKey::newvalidates only non-empty and byte length <= 1024, and — unlike its sibling typesBucketName::newandObjectKind::new— does not reject control characters, so attacker-controlled keys containing newlines/NUL/etc. flow raw into log output. - Actual vs. expected:
BucketNameandObjectKindreject control characters (added by commite9d8acf, "fix(types): reject control characters in bucket names (#84)", which closes issue #71), whileObjectKey, rendered into the very same log statement, silently accepts them; a request to/fetch/kind/%0A%00...produces anObjectKeycontaining literal\n/\0bytes. - Impact: Log injection / log-line forgery in the server's request log: an attacker can inject arbitrary newlines and control characters into log lines, spoofing or corrupting log entries whenever request debugging is enabled.
Code with Bug
src/types.rs:
pub fn new(key: impl Into<CompactString>) -> Result<Self, &'static str> {
let key = key.into();
if key.is_empty() {
return Err("Object key cannot be empty");
}
if key.len() > Self::MAX_LEN {
return Err("Object key too long");
}
Ok(Self(key)) // <-- BUG 🔴 accepts control chars (e.g., '\n', '\0') that will be logged via Display
}
src/service/routes.rs:
Path((kind, object)): Path<(ObjectKind, ObjectKey)>, // <-- axum Path percent-decodes the URI path
...
debug!(%kind, %object, ?buckets, ?byterange, "processing"); // <-- BUG 🔴 attacker-controlled ObjectKey logged unescaped
Explanation
axum'sPathextractor percent-decodes the URL path, so requests like/fetch/kind/%0A%00FAKEproduce anObjectKeycontaining literal control bytes (\n,\0, etc.).ObjectKey::newonly checks empty/length and returnsOk(Self(key)), so those control bytes pass validation.ObjectKeyis included in adebug!log event via%object(Display), and the server uses the plain-text formatter (tracing_subscriber::fmt::init()), so control characters are emitted directly into logs. A newline in the key splits one logical log event into multiple physical log lines, enabling log-line forgery/spoofing.
Codebase Inconsistency
BucketName::new and ObjectKind::new already reject control characters, but ObjectKey::new does not, despite ObjectKind and ObjectKey being logged side-by-side in the same debug! statement.
Exploit Scenario
- Operator enables request debugging (e.g.,
RUST_LOG=debug), allowing thedebug!(%kind, %object, ...)event to be emitted. - Attacker sends
GET /fetch/images/%0AERROR%20cache%20corruption%20detected(plus required headers).%0Ais percent-decoded to\n;ObjectKey::newaccepts it. - The request log output contains an injected newline, creating an additional physical log line with attacker-chosen content, which can spoof/corrupt log entries and confuse log-based alerting/forensics.
Recommended Fix
In src/types.rs, add control-character rejection to ObjectKey::new to match BucketName::new / ObjectKind::new, and add a unit test case ensuring ObjectKey rejects strings containing control characters.
if key.chars().any(char::is_control) {
return Err("Object key cannot contain control characters");
}
History
This bug was created by commit e9d8acf ("fix(types): reject control characters in bucket names (#84)", closes issue #71, Feb 20 2026) — issue #71 concerned a DoS panic in c0_status (src/service/routes.rs:394-409), reached from the URL path via From<ObjectKind> for BucketName (src/types.rs:95-99), so the fix added char::is_control rejection (plus tests) to the two types that reach that header, BucketName::new and ObjectKind::new, and left the third sibling ObjectKey::new (rendered into the very same debug!(%kind, %object, ...) request-log line but not flowing into c0_status) completely untouched. ObjectKey had carried the same weak empty/length-only validation since the initial commit (2c1010e, with the custom Deserialize impl that routes Path extraction through new() added by 7f42e36, "fix: validate ObjectKind/ObjectKey during deserialization (#62)"), but before e9d8acf that was merely weak-by-default; this commit is what created the bug as it exists now — the inconsistent, incomplete hardening in which control characters are rejected for BucketName and ObjectKind but still accepted by ObjectKey, with no note, TODO, or test acknowledging the omission.
- Dominant language
- Rust
- Stars
- 622
- Forks
- 18
- Avg merge
- 17h 8m
- Merged PRs (30d)
- 35
Getting set up
- Ships a Dockerfile or Docker Compose file
- No pull request template
- No contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from s2-streamstore/cachey
-
Difficulty 4/5 3-5 days Newbie friendliness 15/100
s2-streamstore/cachey#6 · 2 comments ·
Maintainers usually reply within 1 day
-
export metrics around tokio schedulingMay be free again A pull request for this issue was closed without being merged. Open
Difficulty 3/5 1-2 days Newbie friendliness 35/100
s2-streamstore/cachey#1 ·
Maintainers usually reply within 1 day
All issues in s2-streamstore/cachey
Similar issues
-
area:frontend kind:bug priority:p2 ready
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Maintainers usually reply within 1 day
-
bug priority: medium
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
storytold/vectorcraft#6143 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
rubys/roundhouse#1917 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 2 days
-
good first issue
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
Maintainers usually reply within 1 day