Skip to content

Security: replace forgeable Hashids owner_token with server-side UUID#325

Open
ZigZagT wants to merge 2 commits intoszabodanika:masterfrom
ZigZagT:fix/forgeable-owner-token
Open

Security: replace forgeable Hashids owner_token with server-side UUID#325
ZigZagT wants to merge 2 commits intoszabodanika:masterfrom
ZigZagT:fix/forgeable-owner-token

Conversation

@ZigZagT
Copy link
Copy Markdown

@ZigZagT ZigZagT commented Mar 8, 2026

Summary

Fixes #323 — the owner_token cookie used to skip burn-after-read counting was generated using unsalted Hashids encoding of two public values (expiry timestamp + pasta ID). Any reader could forge it and read burn-after-read pastas indefinitely.

  • Replace Hashids token with uuid::Uuid::new_v4() stored in a server-side HashMap<u64, (String, u64)>
  • Verify by comparing cookie value against server state instead of self-validating decode
  • Function signature verify_owner_token(token: &str, id: &str) -> bool unchanged

Design choices

  • In-memory map, not Pasta struct field: token is only relevant for 15-second post-creation window. No DB schema change needed.
  • std::sync::Mutex: map access is brief insert/lookup, no .await held across lock.
  • Adds uuid = { version = "1", features = ["v4"] } dependency.

Backwards compatibility

Zero breaking changes. No Pasta struct changes, no DB migration, no URL changes. Existing pastas have no map entry, so verify_owner_token returns false (same as expired token).

Test plan

Test is designed to fail without the fix and pass with the fix using identical test code:

#[test]
fn test_hashids_forged_token_rejected() {
    let pasta_id: u64 = 12345;
    let id_str = to_animal_names(pasta_id);
    let timenow = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
    let forged_token = HARSH.encode(&[timenow + 3600, pasta_id]);
    assert!(!verify_owner_token(&forged_token, &id_str),
        "Forged Hashids token was accepted - owner_token is forgeable!");
}
  • Test fails at commit 1 (old Hashids verification accepts forged token) — verified via cargo test
  • Test passes at commit 2 (UUID map rejects forged token) — verified via cargo test
  • Build and test via: docker run --rm -v $(pwd):/src -w /src deaddev/ubuntu:rust cargo test --bin microbin test_hashids_forged_token_rejected

🤖 Generated with Claude Code

ZigZagT and others added 2 commits March 8, 2026 01:17
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security: Burn-after-read bypass via forgeable owner_token cookie

1 participant