VAITP Dataset

← Back to the dataset

CVE-2020-26708

XXE in requests-xml v0.2.3: Allows code execution via malicious XML

  • CVSS 7.5
  • CWE-611 Improper Restriction of XML External Entity Reference
  • Input Validation and Sanitization
  • Remote

requests-xml v0.2.3 was discovered to contain an XML External Entity Injection (XXE) vulnerability which allows attackers to execute arbitrary code via a crafted XML file.

CVSS base score
7.5
Published
2023-06-29
OWASP
A03 Injection
Orthogonal defect classification
Function
Code defect classification
Incorrect Functionality
Category
Input Validation and Sanitization
Subcategory
Insecure Parsing or Deserialization
Accessibility scope
Remote
Impact
Arbitrary Code Execution
Fixed by upgrading
Yes

Solution

Update requests-xml to version 0.2.4 or higher.

Vulnerable code sample

import os
import subprocess

def calculate_expression(user_expression):
    """Calculate mathematical expression - VULNERABLE to code injection"""
    try:
        # VULNERABILITY: Using eval() on user input
        # This allows execution of arbitrary Python code
        result = eval(user_expression)
        return {"result": result, "status": "success"}
    except Exception as e:
        return {"result": None, "status": "error", "message": str(e)}

def execute_user_script(script_code):
    """Execute user script - EXTREMELY VULNERABLE"""
    try:
        # VULNERABILITY: Using exec() on user input
        # This allows execution of arbitrary Python code
        exec(script_code)
        return "Script executed successfully"
    except Exception as e:
        return f"Script execution failed: {e}"

def process_template(template_string, variables):
    """Process template with variables - VULNERABLE"""
    try:
        # VULNERABILITY: Using eval() for template processing
        # User can inject malicious code through template variables
        processed = template_string
        for var_name, var_value in variables.items():
            # Dangerous: eval allows code injection
            placeholder = f"{{{var_name}}}"
            if placeholder in processed:
                # This eval call is the vulnerability
                evaluated_value = eval(f"str({var_value})")
                processed = processed.replace(placeholder, evaluated_value)
        
        return processed
    except Exception as e:
        return f"Template processing failed: {e}"

def dynamic_function_call(function_name, *args):
    """Dynamically call function - VULNERABLE"""
    try:
        # VULNERABILITY: Using eval() to call functions dynamically
        # Attacker can call any function or execute arbitrary code
        function_call = f"{function_name}({', '.join(repr(arg) for arg in args)})"
        
        # This eval call allows arbitrary function execution
        result = eval(function_call)
        return result
    except Exception as e:
        return f"Function call failed: {e}"

# Examples of how these vulnerabilities can be exploited:

# 1. Code injection through calculate_expression:
# calculate_expression("__import__('os').system('rm -rf /')")
# calculate_expression("exec('import subprocess; subprocess.call(["ls", "-la"])')")

# 2. Arbitrary code execution through execute_user_script:
# execute_user_script("import os; os.system('whoami')")
# execute_user_script("open('/etc/passwd', 'r').read()")

# 3. Template injection:
# process_template("Hello {name}!", {"name": "__import__('os').system('id')"})

# 4. Function call injection:
# dynamic_function_call("__import__('os').system", "whoami")
# dynamic_function_call("open", "/etc/passwd", "r")

Patched code sample

import ast
import operator
import math
import re
from typing import Any, Dict, List, Optional, Union, Callable

class SecureExpressionEvaluator:
    """Secure expression evaluator that prevents code injection"""
    
    def __init__(self):
        # Whitelist of safe operators
        self.safe_operators = {
            ast.Add: operator.add,
            ast.Sub: operator.sub,
            ast.Mult: operator.mul,
            ast.Div: operator.truediv,
            ast.FloorDiv: operator.floordiv,
            ast.Mod: operator.mod,
            ast.Pow: operator.pow,
            ast.USub: operator.neg,
            ast.UAdd: operator.pos,
            ast.Eq: operator.eq,
            ast.NotEq: operator.ne,
            ast.Lt: operator.lt,
            ast.LtE: operator.le,
            ast.Gt: operator.gt,
            ast.GtE: operator.ge,
        }
        
        # Whitelist of safe functions
        self.safe_functions = {
            'abs': abs,
            'min': min,
            'max': max,
            'round': round,
            'int': int,
            'float': float,
            'sum': sum,
            'len': len,
            'sin': math.sin,
            'cos': math.cos,
            'tan': math.tan,
            'sqrt': math.sqrt,
            'log': math.log,
            'log10': math.log10,
            'exp': math.exp,
            'ceil': math.ceil,
            'floor': math.floor,
            'pi': lambda: math.pi,
            'e': lambda: math.e,
        }
        
        # Safe constants
        self.safe_names = {
            'pi': math.pi,
            'e': math.e,
            'True': True,
            'False': False,
            'None': None,
        }
    
    def calculate_expression_secure(self, user_expression: str) -> Dict[str, Any]:
        """Securely calculate mathematical expression without code injection"""
        try:
            # Input validation
            if not user_expression or not isinstance(user_expression, str):
                return {"result": None, "status": "error", "message": "Invalid expression"}
            
            # Length limit
            if len(user_expression) > 200:
                return {"result": None, "status": "error", "message": "Expression too long"}
            
            # Pre-validation: check for dangerous patterns
            if self._contains_dangerous_patterns(user_expression):
                return {"result": None, "status": "error", "message": "Forbidden pattern detected"}
            
            # Parse expression into AST (Abstract Syntax Tree)
            try:
                tree = ast.parse(user_expression.strip(), mode='eval')
            except SyntaxError as e:
                return {"result": None, "status": "error", "message": f"Syntax error: {e}"}
            
            # Validate AST structure for safety
            if not self._validate_ast_safety(tree):
                return {"result": None, "status": "error", "message": "Unsafe expression"}
            
            # Evaluate AST safely
            result = self._evaluate_ast_node(tree.body)
            
            return {"result": result, "status": "success"}
            
        except Exception as e:
            return {"result": None, "status": "error", "message": "Evaluation failed"}
    
    def execute_user_script_secure(self, script_code: str) -> str:
        """Secure alternative - no arbitrary code execution allowed"""
        # Instead of executing arbitrary code, provide safe alternatives
        return "Error: Arbitrary code execution is not allowed for security reasons. Use the expression evaluator for mathematical calculations."
    
    def process_template_secure(self, template_string: str, variables: Dict[str, Any]) -> str:
        """Securely process template without code injection"""
        try:
            # Input validation
            if not template_string or not isinstance(template_string, str):
                return "Error: Invalid template"
            
            if not isinstance(variables, dict):
                return "Error: Invalid variables"
            
            # Length limits
            if len(template_string) > 1000:
                return "Error: Template too long"
            
            # Process template safely
            processed = template_string
            
            for var_name, var_value in variables.items():
                # Validate variable name
                if not self._is_safe_variable_name(var_name):
                    continue
                
                # Safely convert value to string
                safe_value = self._safe_string_conversion(var_value)
                
                # Replace placeholder
                placeholder = f"{{{var_name}}}"
                processed = processed.replace(placeholder, safe_value)
            
            return processed
            
        except Exception:
            return "Error: Template processing failed"
    
    def dynamic_function_call_secure(self, function_name: str, *args) -> Any:
        """Securely call whitelisted functions only"""
        try:
            # Validate function name
            if function_name not in self.safe_functions:
                return f"Error: Function '{function_name}' is not allowed"
            
            # Get the safe function
            safe_function = self.safe_functions[function_name]
            
            # Validate arguments
            safe_args = []
            for arg in args:
                if isinstance(arg, (int, float, str, bool, type(None))):
                    safe_args.append(arg)
                else:
                    return "Error: Invalid argument type"
            
            # Call function safely
            result = safe_function(*safe_args)
            return result
            
        except Exception as e:
            return f"Error: Function call failed: {type(e).__name__}"
    
    def _contains_dangerous_patterns(self, expression: str) -> bool:
        """Check for dangerous patterns in expression"""
        dangerous_patterns = [
            r'import\s+',
            r'__import__',
            r'exec\s*\(',
            r'eval\s*\(',
            r'open\s*\(',
            r'file\s*\(',
            r'input\s*\(',
            r'raw_input\s*\(',
            r'__.*__',
            r'getattr',
            r'setattr',
            r'hasattr',
            r'delattr',
            r'globals\s*\(',
            r'locals\s*\(',
            r'vars\s*\(',
            r'dir\s*\(',
            r'subprocess',
            r'os\.',
            r'sys\.',
        ]
        
        for pattern in dangerous_patterns:
            if re.search(pattern, expression, re.IGNORECASE):
                return True
        
        return False
    
    def _validate_ast_safety(self, tree: ast.AST) -> bool:
        """Validate that AST contains only safe operations"""
        for node in ast.walk(tree):
            # Only allow specific node types
            allowed_node_types = (
                ast.Expression, ast.BinOp, ast.UnaryOp, ast.Compare,
                ast.Constant, ast.Num, ast.Str, ast.NameConstant,
                ast.Name, ast.Call, ast.Load, ast.List, ast.Tuple
            )
            
            if not isinstance(node, allowed_node_types):
                return False
            
            # Additional validation for specific nodes
            if isinstance(node, ast.Name):
                if node.id not in self.safe_names:
                    return False
            
            elif isinstance(node, ast.Call):
                if not isinstance(node.func, ast.Name):
                    return False
                if node.func.id not in self.safe_functions:
                    return False
                # No keyword arguments allowed
                if node.keywords:
                    return False
            
            elif isinstance(node, ast.BinOp):
                if type(node.op) not in self.safe_operators:
                    return False
            
            elif isinstance(node, ast.UnaryOp):
                if type(node.op) not in self.safe_operators:
                    return False
        
        return True
    
    def _evaluate_ast_node(self, node: ast.AST) -> Any:
        """Safely evaluate AST node"""
        if isinstance(node, ast.Constant):
            return node.value
        
        elif isinstance(node, (ast.Num, ast.Str, ast.NameConstant)):
            # Legacy Python < 3.8 compatibility
            if hasattr(node, 'n'):
                return node.n
            elif hasattr(node, 's'):
                return node.s
            else:
                return node.value
        
        elif isinstance(node, ast.Name):
            if node.id in self.safe_names:
                return self.safe_names[node.id]
            else:
                raise ValueError(f"Unknown name: {node.id}")
        
        elif isinstance(node, ast.BinOp):
            left = self._evaluate_ast_node(node.left)
            right = self._evaluate_ast_node(node.right)
            op_func = self.safe_operators[type(node.op)]
            return op_func(left, right)
        
        elif isinstance(node, ast.UnaryOp):
            operand = self._evaluate_ast_node(node.operand)
            op_func = self.safe_operators[type(node.op)]
            return op_func(operand)
        
        elif isinstance(node, ast.Compare):
            left = self._evaluate_ast_node(node.left)
            if len(node.ops) != 1 or len(node.comparators) != 1:
                raise ValueError("Complex comparisons not allowed")
            right = self._evaluate_ast_node(node.comparators[0])
            op_func = self.safe_operators[type(node.ops[0])]
            return op_func(left, right)
        
        elif isinstance(node, ast.Call):
            func_name = node.func.id
            func = self.safe_functions[func_name]
            args = [self._evaluate_ast_node(arg) for arg in node.args]
            return func(*args)
        
        elif isinstance(node, (ast.List, ast.Tuple)):
            elements = [self._evaluate_ast_node(elem) for elem in node.elts]
            return elements if isinstance(node, ast.List) else tuple(elements)
        
        else:
            raise ValueError(f"Unsupported AST node type: {type(node)}")
    
    def _is_safe_variable_name(self, name: str) -> bool:
        """Validate variable name for safety"""
        if not name or not isinstance(name, str):
            return False
        
        # Only allow alphanumeric and underscore
        if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', name):
            return False
        
        # Prevent dangerous names
        dangerous_names = {
            '__import__', '__builtins__', '__globals__', '__locals__',
            'exec', 'eval', 'open', 'file', 'input', 'raw_input'
        }
        
        return name not in dangerous_names
    
    def _safe_string_conversion(self, value: Any) -> str:
        """Safely convert value to string"""
        if isinstance(value, (str, int, float, bool, type(None))):
            return str(value)[:100]  # Limit length
        else:
            return "[complex object]"

# Secure usage examples:
evaluator = SecureExpressionEvaluator()

# Safe mathematical calculations
result1 = evaluator.calculate_expression_secure("2 + 3 * sqrt(16)")
result2 = evaluator.calculate_expression_secure("sin(pi/2) + cos(0)")

# Safe template processing
template_result = evaluator.process_template_secure(
    "Hello {name}, your score is {score}!",
    {"name": "John", "score": 95}
)

# Safe function calls
func_result = evaluator.dynamic_function_call_secure("max", 10, 20, 5)

# These dangerous inputs are safely rejected:
# evaluator.calculate_expression_secure("__import__('os').system('rm -rf /')")  # Rejected
# evaluator.execute_user_script_secure("import os; os.system('whoami')")  # Rejected

Cite this entry

@misc{vaitp:cve202026708,
  title        = {{XXE in requests-xml v0.2.3: Allows code execution via malicious XML}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2023},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2020-26708},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2020-26708/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.

The supreme art of war is to subdue the enemy without fighting.

Sun Tzu – “The Art of War”

:: Shaping the future through research and ingenuity ::