VAITP Dataset

← Back to the dataset

CVE-2021-43837

Vault-cli < 3.0.0: RCE via templated secrets

  • CVSS 9.1
  • CWE-94
  • Input Validation and Sanitization
  • Local

vault-cli is a configurable command-line interface tool (and python library) to interact with Hashicorp Vault. In versions before 3.0.0 vault-cli features the ability for rendering templated values. When a secret starts with the prefix `!template!`, vault-cli interprets the rest of the contents of the secret as a Jinja2 template. Jinja2 is a powerful templating engine and is not designed to safely render arbitrary templates. An attacker controlling a jinja2 template rendered on a machine can trigger arbitrary code, making this a Remote Code Execution (RCE) risk. If the content of the vault can be completely trusted, then this is not a problem. Otherwise, if your threat model includes cases where an attacker can manipulate a secret value read from the vault using vault-cli, then this vulnerability may impact you. In 3.0.0, the code related to interpreting vault templated secrets has been removed entirely. Users are advised to upgrade as soon as possible. For users unable to upgrade a workaround does exist. Using the environment variable `VAULT_CLI_RENDER=false` or the flag `–no-render` (placed between `vault-cli` and the subcommand, e.g. `vault-cli –no-render get-all`) or adding `render: false` to the vault-cli configuration yaml file disables rendering and removes the vulnerability. Using the python library, you can use: `vault_cli.get_client(render=False)` when creating your client to get a client that will not render templated secrets and thus operates securely.

CVSS base score
9.1
Published
2021-12-16
OWASP
A03 Injection
Orthogonal defect classification
Function
Code defect classification
Incorrect Functionality
Category
Input Validation and Sanitization
Subcategory
Insecure Parsing or Deserialization
Accessibility scope
Local
Impact
Arbitrary Code Execution
Fixed by upgrading
Yes

Solution

Upgrade to vault-cli version 3.0.0 or higher.

Vulnerable code sample

from vault_cli import get_client

def get_secret(secret_path):
    """Vulnerable function that demonstrates template injection in vault-cli."""
    # VULNERABLE: Template rendering enabled by default
    # Allows RCE through templated secrets
    client = get_client(render=True)  # DANGEROUS: Template rendering enabled
    secret = client.get(secret_path)
    return secret

def process_vault_data(path, template_data):
    """Vulnerable function that processes vault data with templates."""
    # VULNERABLE: User-controlled template data with rendering enabled
    client = get_client(render=True)
    
    # DANGEROUS: Template injection possible through path or data
    result = client.get(path, **template_data)
    return result

# Example of vulnerable usage:
# get_secret("secret/{{subprocess.check_output(['whoami'])}}")  # RCE through template!
# process_vault_data("secret/data", {"malicious": "{{__import__('os').system('rm -rf /')}}})

Patched code sample

from vault_cli import get_client
import re
from typing import Dict, Any, Optional

class SecureVaultManager:
    """Secure vault client management without template injection vulnerabilities."""
    
    def __init__(self):
        self.max_path_length = 200
        self.allowed_path_pattern = re.compile(r'^[a-zA-Z0-9/_-]+$')
        self.dangerous_template_patterns = [
            r'\{\{.*?\}\}',  # Template expressions
            r'subprocess', r'__import__', r'eval', r'os\.system',
            r'open\s*\(', r'file\s*\('
        ]
    
    def get_secret(self, secret_path: str) -> Dict[str, Any]:
        """Secure function that gets secrets without template injection risks."""
        
        # Input validation
        if not isinstance(secret_path, str):
            raise ValueError("Secret path must be a string")
        
        if len(secret_path.strip()) == 0:
            raise ValueError("Secret path cannot be empty")
        
        if len(secret_path) > self.max_path_length:
            raise ValueError("Secret path too long")
        
        # Sanitize and validate path
        clean_path = secret_path.strip()
        
        if not self.allowed_path_pattern.match(clean_path):
            raise ValueError("Secret path contains invalid characters")
        
        # Check for template injection attempts
        if self._contains_template_injection(clean_path):
            raise ValueError("Template injection attempt detected in path")
        
        try:
            # SECURE: Disable template rendering to prevent RCE
            client = get_client(render=False)  # CRITICAL: Template rendering disabled
            
            # Get secret safely without template processing
            secret = client.get(clean_path)
            
            # Additional validation of returned data
            if secret and isinstance(secret, dict):
                validated_secret = self._validate_secret_data(secret)
                return {
                    'success': True,
                    'secret': validated_secret,
                    'path': clean_path,
                    'message': 'Secret retrieved securely'
                }
            else:
                return {
                    'success': False,
                    'error': 'Invalid secret format',
                    'path': clean_path,
                    'message': 'Secret validation failed'
                }
                
        except Exception as e:
            return {
                'success': False,
                'error': str(e),
                'path': clean_path,
                'message': 'Secret retrieval failed'
            }
    
    def process_vault_data(self, path: str, template_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
        """Secure function that processes vault data without template injection."""
        
        # Input validation
        if not isinstance(path, str):
            raise ValueError("Path must be a string")
        
        # Validate path
        clean_path = path.strip()
        if not self.allowed_path_pattern.match(clean_path):
            raise ValueError("Path contains invalid characters")
        
        # Check for template injection in path
        if self._contains_template_injection(clean_path):
            raise ValueError("Template injection detected in path")
        
        # Validate template data if provided
        if template_data is not None:
            if not isinstance(template_data, dict):
                raise ValueError("Template data must be a dictionary")
            
            # Check template data for injection attempts
            for key, value in template_data.items():
                if isinstance(value, str) and self._contains_template_injection(value):
                    raise ValueError(f"Template injection detected in data key: {key}")
        
        try:
            # SECURE: Always disable template rendering
            client = get_client(render=False)
            
            # Process data without template rendering
            if template_data:
                # Filter and sanitize template data
                safe_data = self._sanitize_template_data(template_data)
                result = client.get(clean_path, **safe_data)
            else:
                result = client.get(clean_path)
            
            return {
                'success': True,
                'data': result,
                'path': clean_path,
                'message': 'Vault data processed securely'
            }
            
        except Exception as e:
            return {
                'success': False,
                'error': str(e),
                'path': clean_path,
                'message': 'Vault data processing failed'
            }
    
    def _contains_template_injection(self, content: str) -> bool:
        """Check for template injection patterns."""
        
        for pattern in self.dangerous_template_patterns:
            if re.search(pattern, content, re.IGNORECASE):
                return True
        return False
    
    def _validate_secret_data(self, secret: Dict[str, Any]) -> Dict[str, Any]:
        """Validate and sanitize secret data."""
        
        validated = {}
        
        for key, value in secret.items():
            # Validate key
            if isinstance(key, str) and len(key) <= 100:
                # Validate value
                if isinstance(value, (str, int, float, bool)):
                    if isinstance(value, str) and len(value) <= 1000:
                        validated[key] = value
                    elif not isinstance(value, str):
                        validated[key] = value
        
        return validated
    
    def _sanitize_template_data(self, template_data: Dict[str, Any]) -> Dict[str, Any]:
        """Sanitize template data to remove dangerous content."""
        
        sanitized = {}
        
        for key, value in template_data.items():
            # Only allow safe string keys
            if isinstance(key, str) and len(key) <= 50:
                clean_key = re.sub(r'[^a-zA-Z0-9_]', '', key)
                
                # Only allow safe values
                if isinstance(value, (str, int, float, bool)):
                    if isinstance(value, str):
                        # Remove template patterns from string values
                        clean_value = re.sub(r'\{\{.*?\}\}', '', value)
                        if len(clean_value) <= 200:
                            sanitized[clean_key] = clean_value
                    else:
                        sanitized[clean_key] = value
        
        return sanitized

# Example of secure usage:
vault_manager = SecureVaultManager()
# result = vault_manager.get_secret("secret/myapp/config")  # Secure secret retrieval
# data = vault_manager.process_vault_data("secret/data", {"env": "production"})  # Safe processing

Cite this entry

@misc{vaitp:cve202143837,
  title        = {{Vault-cli < 3.0.0: RCE via templated secrets}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2021},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2021-43837},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2021-43837/}}
}
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 ::