Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 424e4a7

Browse files
WaylandYangclaude
andcommitted
test(controller): add failing tests for unsafe snapshot_tag in create_sandbox
Reproduces the path-traversal gap in `create_sandbox`: it skipped `is_safe_tag` on `req.snapshot_tag`, unlike `delete_snapshot` and `branch_sandbox` which both validate. A tag like `../../etc/passwd` falls through to disk-fallback where `snapshot_root.join(tag)` produces a path whose `../` segments std::fs syscalls resolve outside snapshot_root. The vmstate-existence check partially gates impact, but the unvalidated tag also persists into SandboxInfo.snapshot_tag and later flows into `read_snapshot_volumes`, which parses attacker- chosen JSON files as forkd_vmm::Snapshot and inherits their volumes into branches — i.e. attacker controls volume mounts of grandchild VMs (post-auth, but the K8s manifest's placeholder bearer token makes that gate brittle in practice). Two tests added: - create_sandbox_rejects_unsafe_snapshot_tag_traversal: `../` path - create_sandbox_rejects_unsafe_snapshot_tag_chars: space char Both currently FAIL on this commit (returns 404, not 400). The next commit will add the validation and turn them green. CI logs are the verification harness — keeping the failing-test commit separate so the bug-reproduction is recorded in the PR history. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
1 parent 112d17e commit 424e4a7

1 file changed

Lines changed: 47 additions & 0 deletions

File tree

crates/forkd-controller/src/http.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -871,6 +871,53 @@ mod tests {
871871
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
872872
}
873873

874+
#[tokio::test]
875+
async fn create_sandbox_rejects_unsafe_snapshot_tag_traversal() {
876+
// Regression: `create_sandbox` previously skipped `is_safe_tag` on the
877+
// request body's `snapshot_tag`. A traversing value like
878+
// `../../etc/passwd` would fall through to the disk-fallback branch
879+
// where `snapshot_root.join(tag)` produces a path that std::fs syscalls
880+
// resolve outside snapshot_root. The 404-from-vmstate-existence-check
881+
// partially limited impact, but the unvalidated tag also got persisted
882+
// into SandboxInfo.snapshot_tag and later flowed into
883+
// `read_snapshot_volumes`, where it would parse attacker-chosen JSON
884+
// files as forkd_vmm::Snapshot and inherit their volumes into branches.
885+
//
886+
// Expect 400 (input validation), not 404 (file-existence oracle).
887+
let app = router(test_state());
888+
let resp = app
889+
.oneshot(
890+
Request::builder()
891+
.method("POST")
892+
.uri("/v1/sandboxes")
893+
.header("content-type", "application/json")
894+
.body(Body::from(r#"{"snapshot_tag":"../../etc/passwd","n":1}"#))
895+
.unwrap(),
896+
)
897+
.await
898+
.unwrap();
899+
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
900+
}
901+
902+
#[tokio::test]
903+
async fn create_sandbox_rejects_unsafe_snapshot_tag_chars() {
904+
// Defense in depth: also reject tags containing characters that aren't
905+
// ASCII alnum / dash / underscore (matches `is_safe_tag`'s contract).
906+
let app = router(test_state());
907+
let resp = app
908+
.oneshot(
909+
Request::builder()
910+
.method("POST")
911+
.uri("/v1/sandboxes")
912+
.header("content-type", "application/json")
913+
.body(Body::from(r#"{"snapshot_tag":"tag with space","n":1}"#))
914+
.unwrap(),
915+
)
916+
.await
917+
.unwrap();
918+
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
919+
}
920+
874921
#[tokio::test]
875922
async fn create_sandbox_rejects_zero_n() {
876923
let app = router(test_state());

0 commit comments

Comments
 (0)