OWASP Top 10 Automated Testing: A Practical Implementation
Security testing shouldn't be a quarterly audit. It should run on every pull request. Here's how I built an automated OWASP Top 10 scanner.
The Approach
Each OWASP category gets its own test module with specific payloads and detection logic:
class OWASPScanner:
def __init__(self, target_url):
self.target = target_url
self.findings = []
def scan_all(self):
self.test_injection() # A03:2021
self.test_broken_auth() # A07:2021
self.test_xss() # A03:2021
self.test_security_misconfig() # A05:2021
self.test_sensitive_data() # A02:2021
return self.findings
SQL Injection Detection
I don't just send ' OR 1=1. I use a payload library with error-based, blind, and time-based techniques:
SQL_PAYLOADS = [
"' OR '1'='1",
"' UNION SELECT NULL--",
"'; WAITFOR DELAY '0:0:5'--", # Time-based blind
"' AND 1=CONVERT(int, @@version)--", # Error-based
]
SQL_ERROR_PATTERNS = [
r"SQL syntax.*MySQL",
r"ORA-\d{5}",
r"Microsoft.*SQL.*Server",
r"PostgreSQL.*ERROR",
r"Unclosed quotation mark",
]
def test_sql_injection(self, endpoint, params):
for param_name in params:
for payload in SQL_PAYLOADS:
test_params = {**params, param_name: payload}
response = requests.get(endpoint, params=test_params)
# Check for database error messages in response
for pattern in SQL_ERROR_PATTERNS:
if re.search(pattern, response.text, re.IGNORECASE):
self.findings.append(Finding(
category="A03:Injection",
severity="CRITICAL",
cwe_id="CWE-89",
endpoint=endpoint,
parameter=param_name,
payload=payload,
evidence=f"Database error pattern matched: {pattern}"
))
XSS Detection
For reflected XSS, inject a unique marker and check if it appears unescaped:
XSS_PAYLOADS = [
'<script>alert("XSS")</script>',
'<img src=x onerror=alert(1)>',
'"><script>alert(document.cookie)</script>',
"javascript:alert('XSS')",
]
def test_xss(self, endpoint, params):
for param_name in params:
for payload in XSS_PAYLOADS:
test_params = {**params, param_name: payload}
response = requests.get(endpoint, params=test_params)
# If the exact payload appears in the response unescaped
if payload in response.text:
self.findings.append(Finding(
category="A03:Injection",
severity="HIGH",
cwe_id="CWE-79",
endpoint=endpoint,
parameter=param_name,
payload=payload,
evidence="Payload reflected unescaped in response"
))
Secrets Detection
Scan source code for hardcoded credentials:
SECRET_PATTERNS = {
'AWS Access Key': r'AKIA[0-9A-Z]{16}',
'AWS Secret Key': r'[0-9a-zA-Z/+]{40}',
'GitHub Token': r'ghp_[0-9a-zA-Z]{36}',
'Generic API Key': r'api[_-]?key["\'\s]*[:=]\s*["\'\s]*[a-zA-Z0-9]{20,}',
'JWT Token': r'eyJ[A-Za-z0-9-_]+\.eyJ[A-Za-z0-9-_]+',
'Private Key': r'-----BEGIN (RSA |EC )?PRIVATE KEY-----',
}
def scan_secrets(self, file_path):
with open(file_path) as f:
content = f.read()
for secret_type, pattern in SECRET_PATTERNS.items():
matches = re.findall(pattern, content)
if matches:
# Filter false positives (test files, examples)
if not self.is_test_file(file_path):
self.findings.append(Finding(
category="A02:Sensitive Data Exposure",
severity="CRITICAL",
cwe_id="CWE-798",
file=file_path,
evidence=f"Found {secret_type} pattern"
))
CI Integration
Run the scanner on every PR:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run OWASP scanner
run: python run_security_scan.py --target http://localhost:8000
- name: Fail on critical findings
run: |
if grep -q '"severity": "CRITICAL"' scan_results.json; then
echo "Critical vulnerabilities found!"
cat scan_results.json | python -m json.tool
exit 1
fi
The Reality Check
Automated scanning catches the low-hanging fruit — obvious injection points, exposed secrets, misconfigured headers. It does NOT replace:
- Manual code review for business logic flaws
- Penetration testing for complex attack chains
- Threat modeling for architectural vulnerabilities
But catching the obvious stuff automatically means your security team (or your manual reviews) can focus on the hard problems.