Implementing Fuzz Testing in Python: A Practical Example

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:

  1. Exception Handling – Whether the program can gracefully handle various exceptional inputs
  2. Code/Command Injection – Whether the program has unsafe uses of eval or os.popen
  3. Regular Expression Denial of Service (ReDoS) – Whether there are inefficient regex patterns
  4. 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:

  1. Exception Handling – Ensuring the program does not crash due to exceptional inputs
  2. Code Injection – Avoiding dangerous functions like eval, exec, etc.
  3. Regular Expression ReDoS – Using more efficient regex patterns
  4. Resource Limitation – Preventing excessive memory or CPU time consumption
  5. 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.

Leave a Comment