Skip to main content
Back to blog
April 28, 2026|9 min read

API Security Testing Checklist: 12 Concrete Checks for REST APIs

12 concrete API security checks every developer should run, with Express and FastAPI code examples — from authentication to rate limiting to error handling.

ZeriFlow Team

1,668 words

API Security Testing Checklist: 12 Concrete Checks for REST APIs

API security testing is the part of application security most teams underinvest in. A typical SaaS exposes 5-10x more endpoints than visible web pages — and every one of them is a target. The OWASP API Security Top 10 (2023) confirms what pentesters have known for years: APIs leak data, skip authorization, and expose internal logic in ways traditional web app testing rarely catches.

This article gives you a concrete, runnable checklist of 12 API security checks. Each one includes what to test, why it matters, and code examples in Express (Node.js) and FastAPI (Python) where relevant. By the end, you'll have a repeatable testing pipeline you can run on every release.

Check your site right now: Free ZeriFlow scan in 60 seconds →

The 12-Point API Security Testing Checklist

1. Authentication on Every Endpoint

The #1 API breach pattern: an endpoint that *should* require authentication but doesn't. This is shockingly common in microservice architectures where one service trusts another and forgets the front door.

How to test:

bash
# Hit every endpoint without an Authorization header
curl -i https://api.example.com/v1/users/me
curl -i https://api.example.com/v1/admin/settings
# Should return 401 Unauthorized

Automate by exporting your OpenAPI spec and looping every path:

bash
jq -r '.paths | keys[]' openapi.json | while read path; do
  status=$(curl -s -o /dev/null -w "%{http_code}" "https://api.example.com$path")
  echo "$path: $status"
done | grep -v "401"

Anything that's not 401 needs review.

2. Authorization Per Resource (BOLA / IDOR)

Broken Object Level Authorization is OWASP API #1. The endpoint requires a token, but doesn't verify the token's user owns the requested resource.

Vulnerable code:

javascript
// Express - vulnerable
app.get('/api/orders/:id', authenticate, async (req, res) => {
  const order = await db.orders.findById(req.params.id);
  res.json(order); // Returns ANY order to ANY authenticated user
});

Fixed:

javascript
app.get('/api/orders/:id', authenticate, async (req, res) => {
  const order = await db.orders.findOne({ 
    id: req.params.id, 
    userId: req.user.id 
  });
  if (!order) return res.status(404).json({ error: 'Not found' });
  res.json(order);
});

How to test: create two users, enumerate their resource IDs, try to access user A's resources with user B's token.

3. Rate Limiting

Without rate limiting, your /login endpoint is a credential-stuffing playground and your /api/expensive-report endpoint is a DoS vector.

Express with `express-rate-limit`:

javascript
import rateLimit from 'express-rate-limit';

app.use('/api/auth/login', rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  standardHeaders: true,
  message: { error: 'Too many login attempts' }
}));

FastAPI with `slowapi`:

python
from slowapi import Limiter
from slowapi.util import get_remote_address

limiter = Limiter(key_func=get_remote_address)

@app.post("/auth/login")
@limiter.limit("5/15minutes")
async def login(request: Request, ...):
    ...

How to test: loop a request 100 times, confirm 429 kicks in:

bash
for i in {1..100}; do
  curl -s -o /dev/null -w "%{http_code}\n" -X POST https://api.example.com/auth/login \
    -d '{"email":"x@x.com","password":"x"}'
done | sort | uniq -c

4. Input Validation and Schema Enforcement

Mass assignment, type confusion, oversized payloads — all preventable with schema validation.

Express with `zod`:

javascript
import { z } from 'zod';

const CreateUserSchema = z.object({
  email: z.string().email().max(255),
  password: z.string().min(12).max(128),
  // role NOT accepted from client
}).strict();

app.post('/api/users', (req, res) => {
  const result = CreateUserSchema.safeParse(req.body);
  if (!result.success) return res.status(400).json(result.error);
  // ...
});

FastAPI does this natively via Pydantic — just type your endpoint parameters and unknown fields are rejected if you set model_config = {"extra": "forbid"}.

How to test: POST oversized payloads, unexpected types, additional fields (role: "admin"), nested objects with prototype pollution payloads (__proto__: {...}).

5. Output Filtering (No Mass Data Exposure)

OWASP API #3: returning more data than the client needs. The classic example: GET /api/users/me returns the entire user record including password_hash, mfa_secret, and internal_notes.

Always whitelist output fields:

javascript
function publicUser(user) {
  return {
    id: user.id,
    email: user.email,
    displayName: user.displayName,
    createdAt: user.createdAt,
  };
}

Never return user; directly from a database query.

6. Strict CORS Configuration

CORS misconfiguration enables credentialed cross-origin attacks. The two killer mistakes:

  • Access-Control-Allow-Origin: * combined with Access-Control-Allow-Credentials: true (browsers reject this, but reflected origin variants don't)
  • Reflecting the Origin request header without an allowlist

Express done right:

javascript
import cors from 'cors';

app.use(cors({
  origin: ['https://app.example.com', 'https://admin.example.com'],
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
}));

How to test: send Origin: https://evil.com and check the response. ZeriFlow flags CORS misconfigurations automatically — see our broader testing guide for context.

7. HTTPS Enforcement and HSTS

API responses over HTTP leak tokens. Always:

  • Redirect HTTP → HTTPS (or, better, refuse HTTP entirely on API hosts)
  • Set HSTS with max-age=31536000; includeSubDomains; preload
  • Reject TLS < 1.2 at the load balancer

How to test:

bash
# Should return 301/308 to https
curl -i http://api.example.com/health

# Check HSTS header
curl -sI https://api.example.com/health | grep -i strict

A ZeriFlow scan tests TLS version, HSTS configuration, certificate validity, and 80+ other config signals in one shot.

8. Generic Error Messages

Verbose errors leak stack traces, ORM details, and SQL syntax — gold for attackers.

Bad:

json
{
  "error": "PostgresError: column \"emial\" does not exist at line 1: SELECT * FROM users WHERE emial = $1"
}

Good:

json
{
  "error": "Invalid request",
  "code": "VALIDATION_ERROR",
  "requestId": "req_abc123"
}

Log the full error server-side keyed by requestId so you can debug without leaking.

Express pattern:

javascript
app.use((err, req, res, next) => {
  const requestId = req.id || crypto.randomUUID();
  logger.error({ err, requestId, path: req.path });
  res.status(err.status || 500).json({
    error: err.publicMessage || 'Internal server error',
    requestId,
  });
});

9. Secret Management (No Tokens in URLs or Logs)

API keys, JWTs, and session tokens belong in Authorization headers — never in query strings (they get logged by every proxy and CDN), never in URL paths (same), and never echoed in error responses.

How to test: grep -r "Bearer\|api_key" /var/log/nginx on your prod servers. If you find tokens in access logs, you have leaks.

10. Versioning and Deprecation Strategy

Old API versions accumulate vulnerabilities and rarely get the same security attention as /v3. Track every version in production:

  • Document deprecation timelines (Sunset HTTP header per RFC 8594)
  • Monitor traffic per version — turn off versions with zero callers
  • Apply security patches across all supported versions

11. Webhook Signature Verification

If your API receives webhooks (Stripe, GitHub, custom), verify signatures. Forgetting this lets anyone trigger your "verified" handlers.

Stripe-style HMAC verification:

javascript
import crypto from 'crypto';

function verifyWebhook(req) {
  const signature = req.headers['x-webhook-signature'];
  const expected = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(req.rawBody)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

Note timingSafeEqual — string === is vulnerable to timing attacks.

12. Logging and Monitoring (Detect, Don't Just Prevent)

Even with all 11 checks above, something will eventually slip. Your last line of defense is detection:

  • Log every authentication failure with IP, user, endpoint
  • Alert on spikes (5xx errors, 401s, 429s)
  • Detect impossible travel (login from US then Russia in 5 minutes)
  • Alert on access to sensitive endpoints (/admin/*, mass exports)

Tools: Datadog, Grafana, ELK stack, or simpler hosted options like Better Stack.

Tools to Automate API Security Testing

A practical stack:

ToolUse case
Postman / BrunoManual exploration, building test suites
OWASP ZAP API scanAutomated DAST against OpenAPI spec
SchemathesisProperty-based testing from OpenAPI
NucleiTemplate-based, CI-friendly
ZeriFlowConfiguration audit (HTTPS, CORS, headers)
Burp Suite ProManual deep-dive testing

Schemathesis is particularly underrated — it generates thousands of test cases from your OpenAPI spec and finds edge cases manual testing misses:

bash
schemathesis run https://api.example.com/openapi.json \
  --checks all --hypothesis-max-examples 1000

FAQ

### Q: How is API security testing different from web app security testing? A: APIs have a wider attack surface (every endpoint is its own target), often skip authorization checks at the object level, and rarely have UI-level rate limiting. They also lack the browser-enforced defenses (same-origin policy, CSP) that protect traditional web apps. Testing must be more systematic and endpoint-by-endpoint.

### Q: What's the OWASP API Security Top 10? A: A list of the most critical API vulnerabilities, updated by OWASP in 2023. Top issues include Broken Object Level Authorization (BOLA), broken authentication, broken object property level authorization, unrestricted resource consumption, and broken function level authorization.

### Q: Should I rate-limit GET endpoints? A: Yes, especially expensive ones (search, reports, exports). Rate limits protect against scraping, DoS, and credential enumeration. Apply different limits per endpoint based on cost and sensitivity.

### Q: How do I test for BOLA / IDOR vulnerabilities? A: Create two test accounts (User A and User B), enumerate every resource ID in User A's account, then try to access each one using User B's token. Any 200 response is a bug. Automate this with Schemathesis or a custom script against your OpenAPI spec.

### Q: Does ZeriFlow test API endpoints? A: ZeriFlow audits the configuration layer of your API host — HTTPS enforcement, HSTS, CORS, security headers, TLS version, server fingerprint, exposed metadata. For endpoint-level checks (BOLA, input validation, business logic), pair ZeriFlow with ZAP, Schemathesis, or a manual review.

Conclusion

API security testing isn't one tool, it's a checklist. Authentication on every endpoint. Authorization per resource. Rate limiting. Input validation. Output filtering. CORS. HTTPS. Generic errors. Secret hygiene. Versioning. Webhook signatures. Logging. Twelve checks, each one preventable, each one catastrophic when missed.

Start by running the configuration-layer checks today (HTTPS, HSTS, CORS, headers) — those are the easiest to fix and the first an attacker probes. Then work through the application-layer checks systematically, ideally with automation from Schemathesis and Nuclei in CI.

Start your free security scan on ZeriFlow → — 60 seconds, 80+ checks covering the configuration layer of your API, free plan available.

Ready to check your site?

Run a free security scan in 30 seconds.

Related articles

Keep reading