From eval() to ast.literal_eval(): Closing a Code Injection Door in Slack Data Processing
Introduction
There's a function in Python that experienced developers have learned to fear: eval(). It's powerful, it's flexible, and in the wrong hands — or in the wrong context — it's a loaded weapon pointed at your own infrastructure. This week, we're examining a real-world fix that replaced a dangerous eval() call with its safe counterpart, ast.literal_eval(), in a Slack data processing component.
The vulnerability, catalogued under CVE-2025-3933, is classified as a Regular Expression Denial of Service (ReDoS) issue in the huggingface/transformers ecosystem, but the specific code fix addresses something arguably more dangerous: unsafe code execution via eval(). Whether you're a junior developer writing your first Python script or a seasoned engineer reviewing production code, this is a pattern you need to recognize and eliminate.
The Vulnerability Explained
What Is eval() and Why Is It Dangerous?
Python's eval() function takes a string and executes it as Python code. It's often used as a shortcut to parse data structures from strings — like turning "{'key': 'value'}" into an actual Python dictionary. On the surface, this seems harmless. But consider what happens when the string being evaluated isn't under your control.
# What the developer intended:
eval("{'error': 'not_found'}")
# Returns: {'error': 'not_found'} ✅
# What an attacker could inject:
eval("__import__('os').system('rm -rf /')")
# Executes: Deletes your entire filesystem ❌
eval() doesn't distinguish between "safe data" and "executable code." If an attacker can influence the string passed to eval(), they can execute any Python expression in the context of your application — with all the permissions and access your application has.
Where Was the Vulnerability?
The vulnerable code lived in apps/slack_data/slack_mcp_reader.py, specifically in the error handling logic of a SlackMCPReader class. When the Slack API returned an error, the code attempted to parse the error payload from the exception message using a regular expression, then passed the matched string directly to eval():
# BEFORE (Vulnerable Code)
match = re.search(r"'error':\s*(\{[^}]+\})", str(e))
if match:
try:
error_dict = eval(match.group(1)) # ⚠️ DANGEROUS
except (ValueError, SyntaxError, NameError):
pass
And again, a few lines below:
# BEFORE (Second Vulnerable Instance)
match = re.search(r"Failed to fetch messages:\s*(\{[^}]+\})", str(e))
if match:
try:
error_dict = eval(match.group(1)) # ⚠️ DANGEROUS
except (ValueError, SyntaxError, NameError):
pass
How Could This Be Exploited?
The attack surface here is the Slack API error response. If an attacker could influence what error messages the Slack API returns — or if the application connects to a malicious or compromised Slack-like endpoint — they could craft an error payload that, when parsed by the regex and passed to eval(), executes arbitrary code on the server.
Attack Scenario:
- An attacker identifies that the application uses a Slack integration and processes error messages.
- The attacker crafts a malicious error response that embeds Python code within the error dictionary string:
Failed to fetch messages: {'error': __import__('subprocess').check_output(['whoami'])} - The regex extracts the "dictionary" portion of the string.
eval()executes the injected code in the application's runtime context.- The attacker now has Remote Code Execution (RCE) on the server.
What's the Real-World Impact?
The consequences of successful exploitation depend on the application's runtime permissions, but could include:
- Data exfiltration: Reading sensitive files, environment variables, API keys, or database credentials.
- Lateral movement: Using the compromised server as a pivot point to attack internal infrastructure.
- Service disruption: Crashing the application or consuming resources.
- Persistent backdoors: Writing malicious files or modifying existing code.
- Supply chain compromise: In an ML pipeline context (this is a
transformers-adjacent project), corrupting model outputs or training data.
The Fix
Replacing eval() with ast.literal_eval()
The fix is elegant in its simplicity: replace eval() with ast.literal_eval() from Python's standard library.
# AFTER (Safe Code)
import ast # Added at the top of the file
match = re.search(r"'error':\s*(\{[^}]+\})", str(e))
if match:
try:
error_dict = ast.literal_eval(match.group(1)) # ✅ SAFE
except (ValueError, SyntaxError, NameError):
pass
# AFTER (Second Instance Fixed)
match = re.search(r"Failed to fetch messages:\s*(\{[^}]+\})", str(e))
if match:
try:
error_dict = ast.literal_eval(match.group(1)) # ✅ SAFE
except (ValueError, SyntaxError, NameError):
pass
Why Is ast.literal_eval() Safe?
ast.literal_eval() is part of Python's Abstract Syntax Tree (AST) module. Unlike eval(), it only evaluates Python literals — strings, bytes, numbers, tuples, lists, dicts, sets, booleans, and None. It explicitly refuses to evaluate any expression that contains function calls, attribute access, or other executable constructs.
import ast
# Safe usage — parses data structures only
ast.literal_eval("{'error': 'not_found'}")
# Returns: {'error': 'not_found'} ✅
# Blocked — cannot execute function calls
ast.literal_eval("__import__('os').system('rm -rf /')")
# Raises: ValueError: malformed node or string ✅ (Attack neutralized)
# Blocked — cannot access attributes
ast.literal_eval("os.environ")
# Raises: ValueError ✅
The function works by parsing the string into an AST first, then inspecting the tree to ensure it contains only safe literal nodes before evaluating them. If anything unsafe is detected, it raises a ValueError — which is already caught by the existing exception handler in the code.
Before vs. After: Side-by-Side Comparison
| Aspect | eval() |
ast.literal_eval() |
|---|---|---|
| Executes arbitrary Python | ✅ Yes | ❌ No |
| Parses dictionaries | ✅ Yes | ✅ Yes |
| Parses lists/tuples | ✅ Yes | ✅ Yes |
| Allows function calls | ✅ Yes (dangerous) | ❌ No |
| Allows imports | ✅ Yes (dangerous) | ❌ No |
| Safe for untrusted input | ❌ Never | ✅ Yes |
The change is a drop-in replacement for this use case — the functionality is identical for safe inputs, but the attack surface is completely eliminated.
Prevention & Best Practices
1. Never Use eval() on Untrusted Input
This is the cardinal rule. If the string being evaluated comes from anywhere outside your direct control — user input, API responses, file contents, environment variables, network data — eval() is off the table. Full stop.
# ❌ NEVER do this with external data
result = eval(user_input)
result = eval(api_response_string)
result = eval(file.read())
# ✅ Use safer alternatives
result = ast.literal_eval(data_string) # For Python literals
result = json.loads(json_string) # For JSON data
2. Prefer Structured Data Formats
If you're parsing error messages or structured data, consider using JSON instead of Python dictionary syntax. JSON has well-defined, safe parsers:
import json
# Safe JSON parsing
error_dict = json.loads(error_json_string)
For this specific use case, if the Slack API returns structured error objects, parse them at the API response level rather than extracting them from exception message strings.
3. Use Static Analysis Tools
Tools like Semgrep (which actually detected this vulnerability, as noted in the commit message: semgrep_python.lang.security.audit.eval-detected) can automatically flag dangerous eval() usage in your codebase:
# Semgrep rule that catches this pattern
rules:
- id: eval-detected
pattern: eval(...)
message: "Dangerous use of eval() detected"
severity: WARNING
Other tools to consider:
- Bandit: Python security linter that flags eval() as a high-severity issue (B307)
- PyLint: Can be configured to warn on eval() usage
- SonarQube: Enterprise-grade static analysis with security rules
4. Apply the Principle of Least Privilege
Even if eval() is used safely elsewhere, ensure your application runs with the minimum permissions necessary. If code injection does occur, limited OS permissions reduce the blast radius:
- Run application processes as non-root users
- Use containerization (Docker) with restricted capabilities
- Apply network egress filtering to prevent data exfiltration
5. Validate and Sanitize at Boundaries
Treat all external data as untrusted. Validate the structure and content of API responses before processing them:
# Validate the structure before processing
def parse_slack_error(error_string: str) -> dict:
"""Safely parse a Slack error dictionary from a string."""
match = re.search(r"'error':\s*(\{[^}]+\})", error_string)
if not match:
return {}
try:
# Use ast.literal_eval for safe parsing
result = ast.literal_eval(match.group(1))
# Validate the result is actually a dict
if not isinstance(result, dict):
return {}
return result
except (ValueError, SyntaxError, NameError):
return {}
6. Relevant Security Standards
This vulnerability maps to several well-known security standards:
- CWE-95: Improper Neutralization of Directives in Dynamically Evaluated Code ('Eval Injection')
- CWE-94: Improper Control of Generation of Code ('Code Injection')
- OWASP A03:2021: Injection — one of the OWASP Top 10, covering all forms of injection including code injection
- OWASP ASVS V5: Validation, Sanitization and Encoding requirements
Conclusion
This fix is a perfect example of how a single function substitution can eliminate a serious security vulnerability. The developer who originally wrote eval(match.group(1)) was almost certainly focused on functionality — "I need to parse this dictionary string" — rather than security implications. This is completely understandable, and it's exactly why automated security scanning tools like Semgrep are invaluable in modern development workflows.
The key takeaways from this vulnerability:
eval()is almost never the right answer for parsing data. Usejson.loads()for JSON,ast.literal_eval()for Python literals, or purpose-built parsers for structured formats.- Error handling code is attack surface too. It's easy to overlook security in exception handlers, but attackers specifically target edge cases and error paths.
- Automated scanning catches what code review misses. This fix was triggered by Semgrep detection, demonstrating the value of integrating security tools into your CI/CD pipeline.
- The fix was simple, safe, and non-breaking.
ast.literal_eval()is a drop-in replacement foreval()when parsing literals — there's no excuse not to use it.
Security isn't about writing perfect code the first time. It's about building systems — including tooling, processes, and culture — that catch and fix these issues before they can be exploited. This fix is that system working exactly as intended.
Stay curious, stay secure, and audit your eval() calls today.
This vulnerability was automatically detected and fixed as part of an ongoing security improvement initiative. The fix was validated and is now live in production.