Skip to content

fix(security): path-traversal guard on 5 sensing-server endpoints (closes #615)#616

Merged
ruvnet merged 1 commit into
mainfrom
fix/issue-615-path-traversal
May 17, 2026
Merged

fix(security): path-traversal guard on 5 sensing-server endpoints (closes #615)#616
ruvnet merged 1 commit into
mainfrom
fix/issue-615-path-traversal

Conversation

@ruvnet

@ruvnet ruvnet commented May 17, 2026

Copy link
Copy Markdown
Owner

Closes #615 — credit @bannned-bit for the precise exploit catalogue and the fix template.

Five vulnerable endpoints, all patched

File Endpoint Param What an attacker could do pre-fix
recording.rs POST /api/v1/recording/start session_name Write JSONL to arbitrary location via ../../-prefixed name
recording.rs GET /api/v1/recording/download/:id id Read any file the server process can access (../../.env, /etc/passwd)
recording.rs DELETE /api/v1/recording/delete/:id id Delete arbitrary files (config, recordings)
model_manager.rs POST /api/v1/models/load model_id Load attacker-controlled .rvf file from arbitrary path
training_api.rs load_recording_frames (internal, exposed via /api/v1/train/start) dataset_ids[] Read arbitrary files as training input

All five built filesystem paths via format!("...{user_input}...") with no validation. The replace(' ', "_") in recording.rs only normalised whitespace — it did nothing against .., /, null bytes, etc.

Fix shape

New wifi_densepose_sensing_server::path_safety module exports:

pub fn safe_id(input: &str) -> Result<&str, PathSafetyError>

Rejection envelope:

  • Allowed characters: [A-Za-z0-9._-]
  • Reject leading . (rules out ., .., .env, hidden files)
  • Reject empty strings
  • Reject anything > 64 bytes (MAX_ID_LEN)
  • Reject all whitespace, path separators (/, \), null bytes, non-ASCII (including the fullwidth slash U+FF0F which can normalise to / on some filesystems)

Applied at all 5 sites before any user input reaches a format!() that builds a path. Errors return 400 Bad Request (download) or status: "error" JSON (others) — not panics. Stay consistent with the existing error response shapes.

Test coverage

9 unit tests in path_safety::tests:

test path_safety::tests::accepts_simple_alphanumeric ... ok
test path_safety::tests::boundary_max_len ... ok
test path_safety::tests::rejects_parent_directory_traversal ... ok
test path_safety::tests::rejects_path_separators ... ok
test path_safety::tests::rejects_too_long ... ok
test path_safety::tests::rejects_whitespace_and_specials ... ok
test path_safety::tests::rejects_non_ascii ... ok
test path_safety::tests::rejects_null_byte ... ok
test path_safety::tests::rejects_empty ... ok
test result: ok. 9 passed; 0 failed

cargo build -p wifi-densepose-sensing-server --no-default-features — 33s, no errors (one pre-existing unused-import warning unchanged).

Regression guard

Fix-marker RuView#615 in scripts/fix-markers.json requires both path_safety::safe_id (called sites) and pub fn safe_id (the helper itself). Removing the guard at any of the 5 call sites or deleting the module would fail CI.

$ python scripts/check_fix_markers.py
…
[PASS] RuView#615   path_safety::safe_id gates user-controlled IDs at filesystem boundaries
…
All 19 fix markers present.

Why this is severity-critical

All 5 endpoints are unauthenticated when RUVIEW_API_TOKEN is unset (the LAN-mode default from #443). Combined with the recent #580 (host-header allowlist for DNS rebinding) — both classes of attack land same-origin from any page the user visits during a sensing session. This PR closes the filesystem-side hole; #580 already closed the cross-origin reach.

Audit completeness

grep -rn 'format!("[^"]*\\{[^}]*_id[^}]*\\}' v2/crates/wifi-densepose-sensing-server/src/ — the 5 sites in the PR are the only matches that touch user-controlled identifiers AND build a filesystem path. Other format! calls in that crate either build URLs (host_validation), log messages, or use server-internal identifiers.

🤖 Generated with claude-flow

…oses #615)

Reported by @bannned-bit. Five endpoints in
v2/crates/wifi-densepose-sensing-server embedded user-controlled
identifiers in format!() paths with no sanitization:

  recording.rs       POST   /api/v1/recording/start       (session_name)
  recording.rs       GET    /api/v1/recording/download/:id (id)
  recording.rs       DELETE /api/v1/recording/delete/:id   (id)
  model_manager.rs   POST   /api/v1/models/load           (model_id)
  training_api.rs    load_recording_frames                (dataset_ids[])

Each unauthenticated caller could:
- READ arbitrary files via ../../etc/passwd, ../../.env, etc.
- WRITE attacker-controlled JSONL via recording/start
- LOAD attacker-controlled .rvf model files
- DELETE arbitrary files the server process can touch

New `path_safety` module exports `safe_id(&str) -> Result<&str, PathSafetyError>`
that enforces the rejection envelope BEFORE any user input reaches a
format!() that builds a path:

  - Allowed character set: [A-Za-z0-9._-]
  - Reject leading '.' (rules out '.', '..', '.env', hidden files)
  - Reject empty strings
  - Reject anything > 64 bytes
  - Reject all whitespace, path separators, null bytes, non-ASCII

Applied at all 5 sites. Errors return 400 Bad Request (download) /
status:"error" JSON (others) — not panics.

9 unit tests in path_safety::tests cover:
  - accepts simple alphanumeric / hyphen / underscore / dot
  - rejects empty, leading dot, path separators ('/', '\'),
    null byte, whitespace, shell specials, non-ASCII (including
    fullwidth slash U+FF0F), too-long, boundary at MAX_ID_LEN

  test result: ok. 9 passed; 0 failed
  cargo build -p wifi-densepose-sensing-server --no-default-features: 33s

Fix-marker RuView#615 in scripts/fix-markers.json prevents removing the
guard at any of the 5 call sites. CHANGELOG entry under [Unreleased] /
Security documents the patched endpoints and the rejection envelope.

Severity: critical per reporter — five remotely-reachable paths to read,
write, or delete arbitrary files. Hot per-request paths, not edge cases.

Co-Authored-By: claude-flow <ruv@ruv.net>
@ruvnet
ruvnet merged commit 72bbd25 into main May 17, 2026
13 checks passed
@ruvnet
ruvnet deleted the fix/issue-615-path-traversal branch May 17, 2026 23:59
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.

Path traversal in 3 endpoints — training_api.rs, model_manager.rs, recording.rs

1 participant