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.
- CWE
- CWE-94
- 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 processingCite 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 ::
