Tools: Breaking: API Security Hardening Checklist: 15 Points Every API Must Pass

Tools: Breaking: API Security Hardening Checklist: 15 Points Every API Must Pass

API Security Hardening Checklist for Production: 15 Points Every API Must Pass

Authentication & Authorization

1. JWT Validation Is Complete

2. Authorization Checks on Every Endpoint

3. API Key Rotation and Scoping

Input Validation

4. Request Size Limits

5. Schema Validation on Every Input

6. SQL Injection and NoSQL Injection Prevention

Rate Limiting & Abuse Prevention

7. Rate Limiting by Identity, Not Just IP

8. Endpoint-Specific Limits

9. Request Throttling for Expensive Operations

Response Security

10. Never Expose Stack Traces

11. Security Headers on Every Response

12. Response Filtering — Return Only What's Needed

Infrastructure

13. TLS Everywhere — No Exceptions

14. Audit Logging for Sensitive Operations

15. Dependency Scanning in CI/CD

Real-World Impact: What Happens When You Skip This

How to Use This Checklist We audited over 40 production APIs last year. Every single one failed at least 3 items on this checklist. The median was 5 failures. Two of them had critical vulnerabilities that could have led to full database exposure. The uncomfortable truth? Most API security failures aren't sophisticated attacks. They're basic hygiene items that teams skip because they're "boring" or "we'll get to it later." Later never comes until after the breach. This checklist is ordered by severity. If you can only fix five things today, fix the first five. Don't just check if the token is present. Validate: The most common failure: accepting tokens signed with alg: none. Your JWT library should reject this by default, but verify it does. Authentication tells you who they are. Authorization tells you what they can do. We've seen APIs where users could access any resource by changing the ID in the URL: This is Insecure Direct Object Reference (IDOR) — OWASP API #1. Every endpoint must verify that the authenticated user has permission to access the requested resource. If you use API keys: they must be scoped (read-only vs read-write), they must rotate automatically (90 days max), and revoked keys must be rejected immediately — not after a cache TTL expires. Without size limits, an attacker can send a 10GB JSON body and crash your server. Set explicit limits: Also limit: array lengths, string lengths, nested object depth, and number of request parameters. Every request body, query parameter, and path parameter should be validated against a schema before reaching your business logic. Use OpenAPI schemas or JSON Schema validators. Don't just check types — check patterns, ranges, and allowed values: Use parameterized queries. Always. No exceptions. For MongoDB, watch out for query operator injection: {"username": {"$gt": ""}} matches everything. Validate that input fields are strings, not objects. IP-based rate limiting fails against distributed attacks and punishes legitimate users behind NAT/VPN. Rate limit by: Return proper 429 Too Many Requests with Retry-After header. Your login endpoint should have much stricter limits than your product listing endpoint. Set per-endpoint limits: Search, report generation, export, and analytics endpoints should have dedicated throttling. A single user running 50 concurrent export requests can bring down your database. A 500 response with a full Python traceback tells an attacker your framework, database, file paths, and sometimes credentials. In production, return generic error messages: Log the full trace server-side with the request ID for debugging. For APIs that return sensitive data, add Cache-Control: no-store to prevent proxy caching of personal information. Your internal user object has 40 fields. Your API response should have 8. Never serialize your entire database model to JSON. Use explicit response schemas that whitelist returned fields. All API traffic must be encrypted. No HTTP fallback. No self-signed certs in production. No TLS 1.0/1.1. Minimum TLS 1.2, prefer 1.3. Test with: nmap --script ssl-enum-ciphers -p 443 your-api.com Log every: authentication attempt (success and failure), authorization failure, data access, data modification, and admin action. Include: timestamp, user ID, IP, action, resource, and result. These logs are your forensic trail. When (not if) you investigate an incident, they tell you exactly what happened. Store them in append-only storage for at least 12 months. Your code might be secure, but your dependencies might not be. Run automated vulnerability scans on every build: Block merges with critical vulnerabilities. No exceptions. We worked with a fintech startup that shipped their payment API without checking items 2, 7, and 10 on this list. Within three months, an attacker discovered the IDOR vulnerability — they could enumerate other users' transaction histories by incrementing the user ID in the URL. The missing rate limiting meant the attacker could scrape thousands of records per minute. And the exposed stack traces in error responses gave them the exact database schema they needed to understand what they were looking at. The breach affected 12,000 users. The regulatory fine was six figures. The engineering time to fix, audit, and rebuild trust took four months. The actual security fixes? Three hours. The same three hours they could have spent before launch. This isn't unusual. The LiteLLM supply chain attack that hit HackerNews this week (362 points) is another reminder: security isn't something you bolt on after launch. It's either built into your development process or it's a ticking clock. The goal isn't a perfect score — it's knowing where your gaps are and having a plan to close them. Every item you fix today is one less vulnerability an attacker can exploit tomorrow. TechSaaS offers comprehensive API security audits. We run your APIs through this checklist (and more), identify vulnerabilities, and help you fix them before attackers find them. If your last security audit was "never," that's exactly why you need one. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

# BAD — accepts any algorithm decoded = jwt.decode(token, secret) # GOOD — explicitly specify allowed algorithms decoded = jwt.decode(token, public_key, algorithms=["RS256"]) # BAD — accepts any algorithm decoded = jwt.decode(token, secret) # GOOD — explicitly specify allowed algorithms decoded = jwt.decode(token, public_key, algorithms=["RS256"]) # BAD — accepts any algorithm decoded = jwt.decode(token, secret) # GOOD — explicitly specify allowed algorithms decoded = jwt.decode(token, public_key, algorithms=["RS256"]) GET /api/users/123/invoices → returns YOUR invoices GET /api/users/456/invoices → returns SOMEONE ELSE'S invoices GET /api/users/123/invoices → returns YOUR invoices GET /api/users/456/invoices → returns SOMEONE ELSE'S invoices GET /api/users/123/invoices → returns YOUR invoices GET /api/users/456/invoices → returns SOMEONE ELSE'S invoices # Nginx client_max_body_size 1m; # Express.js app.use(express.json({ limit: '1mb' })); # Nginx client_max_body_size 1m; # Express.js app.use(express.json({ limit: '1mb' })); # Nginx client_max_body_size 1m; # Express.js app.use(express.json({ limit: '1mb' })); { "email": { "type": "string", "format": "email", "maxLength": 254 }, "age": { "type": "integer", "minimum": 0, "maximum": 150 }, "role": { "type": "string", "enum": ["user", "admin"] } } { "email": { "type": "string", "format": "email", "maxLength": 254 }, "age": { "type": "integer", "minimum": 0, "maximum": 150 }, "role": { "type": "string", "enum": ["user", "admin"] } } { "email": { "type": "string", "format": "email", "maxLength": 254 }, "age": { "type": "integer", "minimum": 0, "maximum": 150 }, "role": { "type": "string", "enum": ["user", "admin"] } } # BAD — SQL injection cursor.execute(f"SELECT * FROM users WHERE id = {user_id}") # GOOD — parameterized cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,)) # BAD — SQL injection cursor.execute(f"SELECT * FROM users WHERE id = {user_id}") # GOOD — parameterized cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,)) # BAD — SQL injection cursor.execute(f"SELECT * FROM users WHERE id = {user_id}") # GOOD — parameterized cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,)) { "error": "internal_server_error", "message": "An unexpected error occurred", "request_id": "req_abc123" } { "error": "internal_server_error", "message": "An unexpected error occurred", "request_id": "req_abc123" } { "error": "internal_server_error", "message": "An unexpected error occurred", "request_id": "req_abc123" } X-Content-Type-Options: nosniff X-Frame-Options: DENY Strict-Transport-Security: max-age=31536000; includeSubDomains Content-Security-Policy: default-src 'none' Cache-Control: no-store X-Content-Type-Options: nosniff X-Frame-Options: DENY Strict-Transport-Security: max-age=31536000; includeSubDomains Content-Security-Policy: default-src 'none' Cache-Control: no-store X-Content-Type-Options: nosniff X-Frame-Options: DENY Strict-Transport-Security: max-age=31536000; includeSubDomains Content-Security-Policy: default-src 'none' Cache-Control: no-store # GitHub Actions example - name: Security scan run: | -weight: 500;">pip -weight: 500;">install safety && safety check -weight: 500;">npm audit --audit-level=high trivy fs --severity HIGH,CRITICAL . # GitHub Actions example - name: Security scan run: | -weight: 500;">pip -weight: 500;">install safety && safety check -weight: 500;">npm audit --audit-level=high trivy fs --severity HIGH,CRITICAL . # GitHub Actions example - name: Security scan run: | -weight: 500;">pip -weight: 500;">install safety && safety check -weight: 500;">npm audit --audit-level=high trivy fs --severity HIGH,CRITICAL . - Signature using the correct algorithm (RS256, not HS256 with a guessable secret) - Expiration (exp claim) — tokens should expire in minutes, not days - Issuer (iss claim) — reject tokens from unknown issuers - Audience (aud claim) — reject tokens meant for other services - API key or user ID (primary) - IP address (secondary) - Combination with sliding window counters - Score your API: Go through all 15 points. Mark pass/fail for each. Be honest — the only person you're fooling is yourself. - Fix critical first: Items 1-6 are critical. If you fail any of these, -weight: 500;">stop everything and fix them now. These are the vulnerabilities that lead to data breaches. - Automate checks: Add items 4, 5, 6, 13, and 15 to your CI/CD pipeline so they can't regress. Security that depends on humans remembering to check is security that will fail. - Schedule quarterly audits: Run through the full checklist every quarter. New code introduces new attack surface. New dependencies introduce new vulnerabilities. - Test adversarially: Don't just check the box — try to break your own API. Use tools like OWASP ZAP, Burp Suite, or sqlmap against your staging environment. If you're not attacking your own API, someone else will.