Title: 🛡️ CRITICAL Arbitrary File Write: Unsanitized filename in Aether version upload handler
🚨 Severity CRITICAL
💡 Description
The upload_handler function in syscore/src/server/aether.rs contains an Arbitrary File Write vulnerability due to the lack of sanitization on the filename provided in the multipart form data.
In Rust, std::path::PathBuf::join replaces the entire base path if the appended string is an absolute path. The filename extracted from multipart.next_field() is directly joined to version_dir:
// syscore/src/server/aether.rs
let file_path = version_dir.join(&filename);
tokio_fs::write(&file_path, &file_bytes).await.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;Because filename is attacker-controlled and unsanitized, an attacker can provide an absolute path (e.g., /etc/passwd or /root/.ssh/authorized_keys) as the filename. PathBuf::join will discard the version_dir and write the uploaded file contents directly to the attacker-specified absolute path on the host filesystem.
🎯 Potential Impact
An authenticated attacker (even using the weak default AETHER_UPLOAD_KEY of "update_me_please") can overwrite arbitrary files on the system with the permissions of the user running the syscore backend service. This can lead to Remote Code Execution (RCE) by overwriting .ssh/authorized_keys, cron jobs, or system binaries, leading to complete system compromise.
🛠️ Steps to Reproduce
- Start the
syscorebackend service. - Construct a multipart POST request to the
/api/v1/aetherupload endpoint. - Provide the default authentication header:
Authorization: Bearer update_me_please. - Include form fields for
version(e.g.,1.0.0),description, andchangelog. - Include a file upload field with the name
file. Set the filename parameter in the Content-Disposition header to an absolute path, such as/tmp/pwned.txt. - Send the request.
- Observe that the file
pwned.txtis created in/tmpcontaining the uploaded payload, instead of within the intendedstorage/aether/1.0.0/directory.
✅ Recommended Remediation
Implement strict path sanitization for the filename extracted from the multipart request before using it with PathBuf::join.
- Reject any filename containing path separators (
/or\). - Alternatively, extract only the final file component using
std::path::Path::new(&filename).file_name(). - Ensure the resolved path remains within the intended storage directory bounds.
Example fix:
let safe_filename = std::path::Path::new(&filename)
.file_name()
.and_then(|name| name.to_str())
.ok_or((StatusCode::BAD_REQUEST, "Invalid filename".to_string()))?;
let file_path = version_dir.join(safe_filename);🔗 References
- Rust
PathBuf::joindocumentation: https://doc.rust-lang.org/std/path/struct.PathBuf.html#method.join - OWASP Path Traversal / Arbitrary File Write: https://owasp.org/www-community/attacks/Path_Traversal