[Detail Bug] Request logs can be forged via control characters in fetch object keys (log injection)

Closed Beginner friendly
#211 0 comments 0 reactions 0 assignees View on GitHub

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
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
rust
Domain
backend, security

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

https://app.detail.dev/org_89d327b3-b883-4365-b6a3-46b6701342a9/bugs/bug_abdb8ddf-9db7-4b0a-a339-2ce8913918cb

Introduced in #84 by @shikhar on Feb 20, 2026

Summary

  • Context: ObjectKey is the user-supplied object key extracted from the path of the public GET /fetch/{kind}/{*object} endpoint of the Cachey caching service, and is rendered into request log lines via its Display implementation.
  • Bug: ObjectKey::new validates only non-empty and byte length <= 1024, and — unlike its sibling types BucketName::new and ObjectKind::new — does not reject control characters, so attacker-controlled keys containing newlines/NUL/etc. flow raw into log output.
  • Actual vs. expected: BucketName and ObjectKind reject control characters (added by commit e9d8acf, "fix(types): reject control characters in bucket names (#84)", which closes issue #71), while ObjectKey, rendered into the very same log statement, silently accepts them; a request to /fetch/kind/%0A%00... produces an ObjectKey containing literal \n/\0 bytes.
  • 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's Path extractor percent-decodes the URL path, so requests like /fetch/kind/%0A%00FAKE produce an ObjectKey containing literal control bytes (\n, \0, etc.).
  • ObjectKey::new only checks empty/length and returns Ok(Self(key)), so those control bytes pass validation.
  • ObjectKey is included in a debug! 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

  1. Operator enables request debugging (e.g., RUST_LOG=debug), allowing the debug!(%kind, %object, ...) event to be emitted.
  2. Attacker sends GET /fetch/images/%0AERROR%20cache%20corruption%20detected (plus required headers). %0A is percent-decoded to \n; ObjectKey::new accepts it.
  3. 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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from s2-streamstore/cachey

All issues in s2-streamstore/cachey

Similar issues

More Rust issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.