DEV Community

Alex Spinov
Alex Spinov

Posted on

I Built a Free API Vulnerability Scanner — It Found 23 Issues in My Own Code

Last month, I ran a security audit on a side project I'd been working on for 6 months. I thought the code was clean — proper auth headers, HTTPS everywhere, input validation on forms.

Then I pointed my scanner at it and found 23 vulnerabilities. Not theoretical ones. Real, exploitable issues.

Here's what happened, what the scanner found, and how you can build one yourself.

The Wake-Up Call

I was building an API aggregation service — something that pulls data from multiple free APIs and normalizes it. I had:

  • API key rotation ✅
  • Rate limiting ✅
  • HTTPS only ✅
  • Input sanitization ✅

So what could go wrong? A lot.

The 5 Categories of API Vulnerabilities

After researching OWASP API Security Top 10 (2023 edition), I built a scanner that checks for these categories:

1. Broken Object Level Authorization (BOLA)

The #1 API vulnerability. Your endpoint /api/users/123/data\ works for user 123 — but does it also work when user 456 requests it?

import requests

def check_bola(base_url, endpoints, auth_token):
    """Test if changing object IDs exposes other users' data"""
    vulnerabilities = []

    for endpoint in endpoints:
        # Try accessing with incremented/decremented IDs
        for id_offset in [-1, 1, 100, -100]:
            modified_url = modify_id_in_url(endpoint, id_offset)

            resp = requests.get(
                modified_url,
                headers={'Authorization': f'Bearer {auth_token}'}
            )

            if resp.status_code == 200:
                vulnerabilities.append({
                    'type': 'BOLA',
                    'severity': 'HIGH',
                    'endpoint': modified_url,
                    'detail': f'Accessible with modified ID (offset {id_offset})'
                })

    return vulnerabilities

def modify_id_in_url(url, offset):
    """Find numeric IDs in URL and modify them"""
    import re
    def replace_id(match):
        return str(int(match.group()) + offset)
    return re.sub(r'\b\d{1,10}\b', replace_id, url, count=1)
Enter fullscreen mode Exit fullscreen mode

My scanner found: 4 endpoints where changing the user ID returned other users' data. I had authentication but no authorization check.

2. Broken Authentication

Not just "is there a token?" — but "is the token properly validated?"

def check_auth_issues(base_url, endpoints):
    """Test various authentication bypass techniques"""
    issues = []

    bypass_tokens = [
        '',                          # Empty token
        'null',                      # Literal null
        'undefined',                 # Literal undefined
        'Bearer ',                   # Empty bearer
        'Bearer null',               # Null bearer
        'Basic ' + b64encode(b':'), # Empty basic auth
    ]

    for endpoint in endpoints:
        for token in bypass_tokens:
            resp = requests.get(
                f'{base_url}{endpoint}',
                headers={'Authorization': token}
            )

            if resp.status_code != 401:
                issues.append({
                    'type': 'AUTH_BYPASS',
                    'severity': 'CRITICAL',
                    'endpoint': endpoint,
                    'detail': f'Accessible with token: {repr(token)}'
                })

    return issues
Enter fullscreen mode Exit fullscreen mode

My scanner found: 2 endpoints that returned data with an empty Bearer token. My middleware was checking if the header existed, not if it had a valid value.

3. Excessive Data Exposure

APIs that return entire database objects when the client only needs 2 fields.

def check_data_exposure(base_url, endpoint, auth_headers):
    """Check if API returns more data than needed"""
    resp = requests.get(f'{base_url}{endpoint}', headers=auth_headers)

    if resp.status_code != 200:
        return None

    data = resp.json()

    sensitive_fields = [
        'password', 'hash', 'salt', 'secret', 'token',
        'ssn', 'social_security', 'credit_card', 'card_number',
        'private_key', 'api_key', 'internal_id', 'email',
        'phone', 'address', 'ip_address', 'session'
    ]

    found = find_sensitive_fields(data, sensitive_fields)

    if found:
        return {
            'type': 'DATA_EXPOSURE',
            'severity': 'HIGH',
            'endpoint': endpoint,
            'detail': f'Exposes sensitive fields: {found}'
        }

def find_sensitive_fields(obj, sensitive, path=''):
    """Recursively search JSON for sensitive field names"""
    found = []
    if isinstance(obj, dict):
        for key, val in obj.items():
            current = f'{path}.{key}' if path else key
            if any(s in key.lower() for s in sensitive):
                found.append(current)
            found.extend(find_sensitive_fields(val, sensitive, current))
    elif isinstance(obj, list) and obj:
        found.extend(find_sensitive_fields(obj[0], sensitive, f'{path}[0]'))
    return found
Enter fullscreen mode Exit fullscreen mode

My scanner found: 8 fields being returned unnecessarily, including email addresses and internal IDs in a public endpoint.

4. Rate Limiting Gaps

import time
from concurrent.futures import ThreadPoolExecutor

def check_rate_limiting(url, auth_headers, num_requests=100):
    """Test if rate limiting is properly enforced"""

    def make_request(_):
        return requests.get(url, headers=auth_headers).status_code

    start = time.time()
    with ThreadPoolExecutor(max_workers=20) as executor:
        results = list(executor.map(make_request, range(num_requests)))
    elapsed = time.time() - start

    success_count = results.count(200)
    rate_limited = results.count(429)

    if rate_limited == 0 and success_count > 50:
        return {
            'type': 'NO_RATE_LIMIT',
            'severity': 'MEDIUM',
            'endpoint': url,
            'detail': f'{success_count}/{num_requests} succeeded in {elapsed:.1f}s, 0 rate-limited'
        }
Enter fullscreen mode Exit fullscreen mode

My scanner found: 5 endpoints with no rate limiting at all. An attacker could have hammered them at thousands of requests per second.

5. Security Header Analysis

def check_security_headers(url):
    """Verify security headers are present"""
    resp = requests.get(url)
    headers = resp.headers

    required_headers = {
        'X-Content-Type-Options': 'nosniff',
        'X-Frame-Options': ['DENY', 'SAMEORIGIN'],
        'Strict-Transport-Security': None,  # Just needs to exist
        'Content-Security-Policy': None,
        'X-XSS-Protection': '1; mode=block',
    }

    missing = []
    for header, expected in required_headers.items():
        if header not in headers:
            missing.append({'header': header, 'status': 'MISSING'})
        elif expected and headers[header] not in (expected if isinstance(expected, list) else [expected]):
            missing.append({'header': header, 'status': 'MISCONFIGURED', 'value': headers[header]})

    return missing
Enter fullscreen mode Exit fullscreen mode

My scanner found: 4 missing security headers across different endpoints.

The Full Results

Category Issues Found Severity
BOLA 4 🔴 High
Auth Bypass 2 🔴 Critical
Data Exposure 8 🟡 Medium-High
No Rate Limit 5 🟡 Medium
Missing Headers 4 🟢 Low
Total 23

What I Did About It

  1. BOLA fixes — Added middleware that checks req.user.id === resource.owner_id\ on every data endpoint
  2. Auth fixes — Rewrote auth middleware to validate token content, not just presence
  3. Data exposure — Created response serializers that whitelist fields per endpoint
  4. Rate limiting — Added express-rate-limit with tiered limits (100/min for reads, 10/min for writes)
  5. Headers — Added helmet.js with strict CSP

Run It On Your Own APIs

I've open-sourced the scanner. To use it:

git clone https://github.com/spinov001-art/api-vulnerability-scanner
cd api-vulnerability-scanner
pip install -r requirements.txt

# Quick scan
python scanner.py --url https://your-api.com --endpoints endpoints.json

# Full scan with auth
python scanner.py --url https://your-api.com --token YOUR_TOKEN --full
Enter fullscreen mode Exit fullscreen mode

The endpoints.json\ format:

{
  "endpoints": [
    {"method": "GET", "path": "/api/users/{id}"},
    {"method": "GET", "path": "/api/orders/{id}"},
    {"method": "POST", "path": "/api/data"}
  ]
}
Enter fullscreen mode Exit fullscreen mode

Key Lessons

  1. Auth ≠ Authorization — Having a login doesn't mean your data is protected
  2. Never trust the client — Server must validate every request independently
  3. Return minimum data — If the UI needs 3 fields, return 3 fields, not 30
  4. Automate security checks — Run this in CI/CD, not once a quarter
  5. OWASP API Top 10 is your checklist — Read it. Seriously.

Have you run a security scanner on your APIs recently? I'm curious what people find — drop your experience in the comments.

If you want to explore free APIs for your projects, check out my curated list of 300+ free APIs.

Found this useful? Follow me for more security + API content.

Top comments (0)