Skip to main content
Back to Blog
Security

OWASP Top 10 Automated Testing: A Practical Implementation

February 22, 202613 min read
SecurityOWASPPythonAutomationCI/CDScanning

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.

Want to see this in action?

Check out the projects and case studies behind these articles.