Securing a REST API comes down to enforcing four things on every request: who is calling, what they are allowed to do, whether their input is trustworthy, and how often they can call. No single control is enough. An API with flawless authentication still leaks data if it forgets object-level authorization, and a perfectly authorized endpoint still falls over to an unvalidated payload. The OWASP API Security Top 10 exists because teams keep getting one of these layers right and missing another.
This is a practitioner's checklist, ordered roughly by how often each gap shows up in real breaches. The examples are illustrative and defensive; the goal is detection and remediation, not exploitation.
Authentication: prove who is calling
Authentication answers "is this caller who they claim to be?" For most REST APIs this means bearer tokens, and in practice OAuth 2.0 access tokens or signed JWTs.
Two mistakes dominate. The first is accepting the alg: none JWT, where an attacker strips the signature and the server trusts the payload anyway. Always validate the algorithm against an allowlist and reject unsigned tokens. The second is failing to check token expiry and audience, so a token minted for one service works against another.
import jwt
def verify_token(token, public_key):
return jwt.decode(
token,
public_key,
algorithms=["RS256"], # explicit allowlist, never "none"
audience="api.example.com", # reject tokens minted for other services
options={"require": ["exp", "aud"]},
)
Do not build your own token scheme. Use a vetted library, keep signing keys out of source control, and rotate them.
Authorization: the most-missed layer
Authentication tells you the caller is Alice. Authorization tells you Alice may only read her own orders. This is where the most damaging API bugs live, and OWASP ranks Broken Object Level Authorization (BOLA) as the number-one API risk for good reason.
The classic flaw:
GET /api/orders/1001 -> returns Alice's order (she owns it)
GET /api/orders/1002 -> returns Bob's order (nobody checked ownership)
The server authenticated Alice but never verified that order 1002 belongs to her. The fix is to enforce ownership on the object, not just the route:
def get_order(order_id, current_user):
order = db.orders.find(order_id)
if order is None or order.owner_id != current_user.id:
raise NotFound() # return 404, not 403, to avoid leaking existence
return order
Check authorization at the object level for every request that touches a resource, and prefer returning 404 over 403 so you do not confirm that a record exists.
Input validation: never trust the payload
Every field in a request body, query string, or header is attacker-controlled until proven otherwise. Validate against a schema, reject unknown fields, and enforce types and bounds.
Mass assignment is a subtle version of this: a client sends {"role": "admin"} in a profile update and your ORM happily writes it. Bind only the fields you intend to accept:
ALLOWED = {"display_name", "email", "timezone"}
def update_profile(payload, user):
clean = {k: v for k, v in payload.items() if k in ALLOWED}
user.update(clean) # "role" and "is_admin" can never slip through
Injection risks (SQL, NoSQL, command) all trace back to trusting input. Parameterize queries, use an ORM correctly, and never build a query or a shell command by string concatenation.
Rate limiting and resource controls
An API without rate limits is a denial-of-service target and a credential-stuffing target. Limit requests per client, and also cap the cost of individual requests: maximum page sizes, maximum payload bytes, and query timeouts.
# nginx illustrative limit
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
location /api/ {
limit_req zone=api burst=20 nodelay;
client_max_body_size 1m;
}
Watch for unbounded queries too. An endpoint that accepts ?limit= without a ceiling lets a caller request a million rows and exhaust your database.
Transport and headers
Serve the API over TLS only and redirect plaintext. Set Strict-Transport-Security so browsers refuse to downgrade. Return Content-Type: application/json and never reflect user input into error messages verbatim, which turns error handlers into an injection surface. Disable verbose stack traces in production; a 500 should log the detail server-side and return an opaque error id to the client.
The dependency layer you cannot skip
Your API's security is not only your code. The web framework, the JWT library, the JSON parser, and their transitive dependencies all run with your privileges. A known deserialization or resource-exhaustion CVE in a parser is a vulnerability in your API even though you never wrote the vulnerable line. This is why continuous software composition analysis belongs alongside your own controls; an SCA tool can flag a vulnerable transitive dependency before it reaches production. For the running application itself, dynamic testing with DAST exercises endpoints the way an attacker would and surfaces the authorization gaps that code review misses.
FAQ
What is the biggest REST API security risk?
Broken Object Level Authorization (BOLA), where an authenticated user can access another user's data by changing an ID in the request. OWASP ranks it as the top API risk because it is common and directly leaks data. Enforce ownership checks on every object, not just on the route.
Should I use JWTs or opaque tokens?
Both work. JWTs are self-contained and scale well but are hard to revoke before expiry, so keep lifetimes short. Opaque tokens require a lookup but are trivially revocable. Whichever you choose, validate the algorithm, expiry, and audience on every request.
How does input validation prevent injection?
Injection happens when untrusted input is interpreted as code or query structure. Validating against a strict schema and using parameterized queries keeps input as data, never as executable structure, which closes SQL, NoSQL, and command injection paths.
Do dependencies affect my API's security?
Yes. Your framework, parsers, and their transitive dependencies run with your API's privileges. A CVE in any of them is effectively a vulnerability in your API, which is why continuous dependency scanning is part of securing a REST API.