fix(store): a blob id never reaches outside the blob store
check / check (push) Canceled after 0s

Max-testing pass on ledger-store (security property, rank 2). Blob ids
come from the journal, which is untrusted input on disk. BlobStore built
paths from any string: an id under four bytes (or with multi-byte
characters) panicked on slicing, and exists() would report on any path
an id like "../../x" reached. get() never served such bytes (it serves
only content whose SHA-256 equals the id), but it read them.

Now only a 64-hex-digit id names a blob; anything else is an error from
get() and false from exists(), touching nothing. Test
a_blob_id_never_reaches_outside_the_blob_store (empty, short, unicode,
traversal, absolute, non-hex, 63/65 digits; uppercase hex still works).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
This commit is contained in:
swaitsandClaude Opus 5.5 committed 2026-10-13 17:10:01 -06:00
1 parent d0490bf106
commit 725b9beaac
2 files changed
+60 -8

No files matched your search

+34
View File
@@ -1114,3 +1114,37 @@ fn a_second_writer_is_refused_while_the_first_has_the_books() {
reopened.verify().unwrap();
assert_eq!(reopened.ledger().chart().len(), 1);
}
/// Security (rank 2; max-testing pass on the store, 2026-10-13): a blob id
/// comes from the journal, which is untrusted input on disk. Whatever the
/// id, the blob store never panics and never touches a path outside its
/// own directory: only a 64-hex-digit id names a blob.
#[test]
fn a_blob_id_never_reaches_outside_the_blob_store() {
let dir = test_dir("blob-ids");
let blobs = ledger_store::BlobStore::open(&dir.join("blobs")).unwrap();
let outside = dir.join("secret.txt");
std::fs::write(&outside, b"not a blob").unwrap();
for id in [
"",
"a",
"ab",
"abc",
"é",
"日本語",
"../secret.txt",
"../../secret.txt",
"ab/../../secret.txt",
"/etc/passwd",
"zz",
&"g".repeat(64),
&"a".repeat(63),
&"a".repeat(65),
] {
assert!(blobs.get(id).is_err(), "{id:?} was served");
assert!(!blobs.exists(id), "{id:?} probed a path");
}
let (sha, _) = blobs.put(b"receipt").unwrap();
assert_eq!(blobs.get(&sha).unwrap(), b"receipt");
assert_eq!(blobs.get(&sha.to_uppercase()).unwrap(), b"receipt");
}