Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ edition = "2021"
# the ROOT manifest (`[workspace.package].version`), so it MUST be set here for a
# release to fire (§3.6). The library crates (dig-node-core/dig-runtime/dig-wallet)
# keep their own independent versions — only the released binary tracks the workspace version.
version = "0.72.5"
version = "0.72.6"

# Release hardening, matching digstore: keep integer-overflow checks ON in release.
# The node parses untrusted serialized input and does offset/length arithmetic over
Expand Down
44 changes: 40 additions & 4 deletions crates/dig-node-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4323,17 +4323,53 @@ mod tests {
}

#[tokio::test]
async fn remove_cached_rejects_path_traversal() {
// A non-hex store id that tries to escape the cache dir is refused and
// never deletes anything outside it.
async fn remove_cached_rejects_a_non_canonical_key_at_the_validator() {
// A non-hex store id is refused by the 64-hex validator (`CapsuleKey::parse`) BEFORE any path
// is built — so it never reaches the containment guard below. This pins the validator gate;
// `remove_cached_containment_guard_refuses_a_symlink_escape` pins the guard that follows it.
let (node, _td) = test_node(None);
let err = node
.cache_remove_cached("../../etc", &"33".repeat(32))
.await
.unwrap_err();
assert!(
err.contains("invalid") || err.contains("hex"),
"traversal attempt rejected as invalid input, got: {err}"
"a non-canonical key is rejected as invalid input, got: {err}"
);
}

#[cfg(unix)]
#[tokio::test]
async fn remove_cached_containment_guard_refuses_a_symlink_escape() {
// The 64-hex validator can only pass keys whose bytes contain no `.`/`/`, so a valid key can
// never escape `<cache>/modules` on its own — the canonicalize + `starts_with(cache)` guard is
// defense-in-depth against a compromised cache LAYOUT. Exercise it directly: plant a symlink
// inside the cache whose target is a real file OUTSIDE the cache, then a well-formed remove
// must REFUSE it and leave the outside file intact. Delete the guard and this unlinks the
// outside file instead — so the assertions below fail without it (the test pins the guard).
let (node, _td) = test_node(None);
let store = "aa".repeat(32);
let root = "bb".repeat(32);

// A real file outside the cache dir that must survive the refused remove.
let outside = tempfile::tempdir().unwrap();
let protected = outside.path().join(format!("{root}.module"));
std::fs::write(&protected, b"must-not-be-deleted").unwrap();

// <cache>/modules/<store> -> <outside>, so <cache>/modules/<store>/<root>.module resolves,
// through the symlink, to the protected file above.
let modules = node.cache_dir_path().join("modules");
std::fs::create_dir_all(&modules).unwrap();
std::os::unix::fs::symlink(outside.path(), modules.join(&store)).unwrap();

let err = node.cache_remove_cached(&store, &root).await.unwrap_err();
assert!(
err.contains("outside the cache"),
"a symlink escape is refused by the containment guard, got: {err}"
);
assert!(
protected.exists(),
"the containment guard must leave the out-of-cache file intact"
);
}

Expand Down
Loading