Implementing Fuzz Testing in Python: A Practical Example
In this article, I will implement a vulnerable program in Python and demonstrate how to perform fuzz testing using Python. It is important to note that since Python is a memory-safe language, we cannot fully replicate buffer overflow vulnerabilities found in C, but we can simulate similar security issues.
Vulnerable Python Program
# vulnerable_parser.py
import sys
import json
def parse_data(input_data):
"""
A parsing function with security issues
1. Exception-prone handling logic
2. Potential code injection vulnerabilities
3. Possible Regular Expression Denial of Service (ReDoS)
"""
try:
# Simulate unsafe data processing
if input_data.startswith("{"):
# Attempt to parse as JSON - may raise an exception
data = json.loads(input_data)
return f"Parsed JSON: {data}"
elif "system" in input_data:
# Simulate command injection vulnerability
import os
# Dangerous operation: should never do this in production!
result = os.popen(f"echo {input_data}").read()
return f"Command result: {result}"
elif "calc" in input_data:
# Simulate unsafe eval usage
return f"Calculation: {eval(input_data)}"
else:
# Simulate regex ReDoS vulnerability
import re
# Dangerous regex pattern that may cause ReDoS
pattern = r"^(a+)+$"
if re.match(pattern, input_data):
return "Pattern matched"
return f"Processed: {input_data}"
except Exception as e:
return f"Error: {str(e)}"
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python vulnerable_parser.py <input_file>")
sys.exit(1)
with open(sys.argv[1], 'r', encoding='utf-8', errors='ignore') as f:
input_data = f.read().strip()
result = parse_data(input_data)
print(result)
Fuzz Testing with Python
There are several methods to perform fuzz testing on Python programs. Here are a few common approaches:
Method 1: Using AFL++ with Python Wrapper
Although AFL++ is primarily designed for native code, it can be supported through Python extensions:
# Install python-afl
pip install python-afl
# Run Python Fuzz testing with AFL++
PYTHON_AFL_PERSISTENT=1 python -m afl.vulnerable_parser
Method 2: Using a Python-Specific Fuzzer – Atheris
Atheris is a coverage-guided fuzzer developed by Google specifically for Python:
# fuzz_parser.py
import atheris
import sys
with atheris.instrument_imports():
import vulnerable_parser
def TestOneInput(data):
try:
# Convert input data to string
input_str = data.decode("utf-8")
vulnerable_parser.parse_data(input_str)
except (UnicodeDecodeError, ValueError):
# Ignore encoding errors, which is normal Fuzz behavior
pass
except Exception as e:
# Here we can log exceptions, but usually let the Fuzzer handle it
pass
if __name__ == "__main__":
atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz()
Run the above fuzzer:
pip install atheris
python fuzz_parser.py
Method 3: Using a Generative Fuzzer – Hypothesis
Hypothesis is a property-based testing library that can also be used for fuzz testing:
# test_with_hypothesis.py
from hypothesis import given, strategies as st
import vulnerable_parser
@given(st.text())
def test_parse_data_with_random_input(text):
"""Test the parser with randomly generated text"""
try:
result = vulnerable_parser.parse_data(text)
# Here we can add some assertions
assert result is not None
except Exception:
# For fuzz testing, exceptions are acceptable
pass
if __name__ == "__main__":
test_parse_data_with_random_input()
Run the test:
pip install hypothesis
python test_with_hypothesis.py
Method 4: Simple Custom Fuzzer
# simple_fuzzer.py
import random
import string
import subprocess
import os
def generate_random_input(length=100):
"""Generate random input"""
# Generate basic random strings
chars = string.ascii_letters + string.digits + string.punctuation + " "
result = ''.join(random.choice(chars) for _ in range(length))
# Sometimes add special patterns
if random.random() < 0.3:
result = "{" + result + "}" # Simulate JSON
if random.random() < 0.1:
result = "a" * length # May trigger ReDoS
if random.random() < 0.05:
result = "system(" + result + ")" # May trigger command injection
return result
def run_fuzzer():
"""Run the fuzzer"""
crash_count = 0
hang_count = 0
total_tests = 10000
for i in range(total_tests):
# Generate test input
test_input = generate_random_input(random.randint(1, 1000))
# Write to temporary file
with open("temp_input.txt", "w") as f:
f.write(test_input)
try:
# Run target program with timeout
result = subprocess.run(
["python", "vulnerable_parser.py", "temp_input.txt"],
capture_output=True,
text=True,
timeout=5 # 5 seconds timeout
)
# Check result
if result.returncode != 0:
crash_count += 1
print(f"Crash found with input: {test_input[:100]}...")
print(f"Error: {result.stderr}")
# Save crashing input for analysis
with open(f"crash_{crash_count}.txt", "w") as f:
f.write(test_input)
except subprocess.TimeoutExpired:
hang_count += 1
print(f"Timeout with input: {test_input[:100]}...")
# Save input that caused timeout
with open(f"hang_{hang_count}.txt", "w") as f:
f.write(test_input)
# Cleanup
if os.path.exists("temp_input.txt"):
os.remove("temp_input.txt")
print(f"Fuzzing completed. Total tests: {total_tests}")
print(f"Crashes: {crash_count}, Hangs: {hang_count}")
if __name__ == "__main__":
run_fuzzer()
Analysis and Improvement
Through the above fuzz testing, we can identify various potential issues in the Python program:
- Exception Handling – Whether the program can gracefully handle various exceptional inputs
- Code/Command Injection – Whether the program has unsafe uses of eval or os.popen
- Regular Expression Denial of Service (ReDoS) – Whether there are inefficient regex patterns
- Resource Consumption – Whether the program consumes excessive resources due to specific inputs
After discovering vulnerabilities, we should fix the program:
# fixed_parser.py
import sys
import json
import re
def safe_parse_data(input_data):
"""
Fixed safe parsing function
"""
# Limit input size
if len(input_data) > 1000:
return "Error: Input too long"
try:
if input_data.startswith("{"):
# Safely parse JSON
data = json.loads(input_data)
return f"Parsed JSON: {data}"
else:
# Use a safer regex pattern
pattern = r"^a+$" # Simplified pattern to avoid ReDoS
if re.match(pattern, input_data):
return "Pattern matched"
return f"Processed: {input_data}"
except json.JSONDecodeError:
return "Error: Invalid JSON"
except Exception as e:
return f"Error: {str(e)}"
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python fixed_parser.py <input_file>")
sys.exit(1)
try:
with open(sys.argv[1], 'r', encoding='utf-8', errors='ignore') as f:
input_data = f.read().strip()
# Remove dangerous functions (command execution, eval, etc.)
if any(cmd in input_data for cmd in ["system", "eval", "exec", "os.", "subprocess"]):
print("Error: Suspicious input detected")
sys.exit(1)
result = safe_parse_data(input_data)
print(result)
except Exception as e:
print(f"Error: {str(e)}")
Conclusion
Fuzz testing in Python differs from C/C++ mainly focusing on:
- Exception Handling – Ensuring the program does not crash due to exceptional inputs
- Code Injection – Avoiding dangerous functions like eval, exec, etc.
- Regular Expression ReDoS – Using more efficient regex patterns
- Resource Limitation – Preventing excessive memory or CPU time consumption
- Security Practices – Following secure coding guidelines
By combining the use of specialized Python fuzzers (like Atheris) and custom fuzz testing, various security issues in Python applications can be effectively discovered.