REST API Security
Behind almost every mobile app and modern website, there is an API: a set of doors that programs use to ask for data and actions. Unlike a web page, an API has no friendly interface protecting it; it is just logic, directly exposed. This makes APIs a favorite target and a great topic for a young security professional. In this lesson you will learn the main risks, the OWASP API Security Top 10 mindset and the defenses that matter.
Where to practice
Test APIs only if you own them or are explicitly authorized. For practice, use OWASP Juice Shop, which exposes a REST API, DVWA, the PortSwigger Web Security Academy, or machines from TryHackMe and HackTheBox. Probing someone else's API without permission can be prosecuted under articles 323-1 to 323-3-1 of the Code pénal in France, even if "the data was just there".
What is a REST API?
REST is a common style of web API. Resources are identified by addresses and manipulated with standard HTTP methods:
GET /orders/42: read order number 42.POST /orders: create an order.PUTorPATCH /orders/42: modify it.DELETE /orders/42: remove it.
Data usually travels as JSON, and the client proves who it is with a token sent in a header. If you followed the web pentest course, you already know HTTP; an API is the same protocol without the decorations.
Why APIs are risky
- Direct access to logic and data. Each endpoint is a function that someone can call, in any order, with any parameter.
- Clients cannot be trusted. The mobile app may hide a button, but the API itself still accepts the request. Security rules must live on the server.
- They are numerous and change often. Old versions and forgotten endpoints remain online.
- They are automated. Anyone can send thousands of requests per minute with a short script, like those of chapter 2.
The OWASP API Security Top 10
The OWASP project (owasp.org) maintains a list of the most common API risks, in addition to its well-known web Top Ten. The list is revised periodically, so check the current edition. Recurring themes include:
- Broken object-level authorization: the API checks that you are signed in, but not that this object is yours. Changing an identifier in a request, from your order 42 to someone else's 43, reveals another customer's data. This is the most common and most damaging API flaw.
- Broken authentication: weak or missing token validation, tokens that never expire, no protection against password guessing, tokens exposed in addresses.
- Broken object property-level authorization: the API returns, or accepts, more fields than it should. A profile endpoint that returns the internal role field, or accepts a role field from the client.
- Unrestricted resource consumption: no limits on request rate, size or cost, enabling abuse and denial of service, and financial damage with pay-per-use services.
- Broken function-level authorization: administrative functions reachable by ordinary users because only the interface hid them.
- Unrestricted access to sensitive business flows: legitimate features abused in bulk, such as ticket purchase or account creation.
- Server-side request forgery: an endpoint that fetches a URL supplied by the user, which can be made to reach internal systems.
- Security misconfiguration: verbose error messages, permissive cross-origin settings, missing headers, debug features left on.
- Improper inventory management: forgotten old versions and undocumented endpoints.
- Unsafe consumption of other APIs: trusting data from third-party services too much.
Understanding object-level authorization with a story
A hotel gives each guest a keycard that opens the main door and the guest's own room. If the card also opened every room, the hotel would have a "broken object-level authorization" problem. The card (authentication) proves you are a guest; the lock on each door (authorization) checks which guest. In an API, each request for an object must pass a check like "does this object belong to this user?".
Here is a defensive code illustration, in a generic Python style, of the check that must exist on the server:
def get_order(current_user, order_id):
order = database.find_order(order_id)
if order is None:
return error(404)
if order.owner_id != current_user.id and not current_user.is_support:
return error(404) # same answer as "does not exist"
return ok(order)
Notice two points. The ownership comparison is done on the server for each request. And the response for "not yours" is the same as for "does not exist", so that nobody can discover which identifiers are valid. Using random, hard-to-guess identifiers instead of sequential numbers is a helpful extra layer, but it never replaces the check.
Authentication: tokens done well
- Always use HTTPS, so tokens and data are encrypted in transit.
- Validate every token on every request: signature, expiry, intended audience and issuer. Never accept unsigned tokens.
- Short lifetimes for access tokens, with a controlled way to renew and revoke.
- Do not put tokens in addresses, which end up in logs and browser history. Use headers.
- Protect login endpoints with rate limits and monitoring, and offer multi-factor authentication.
- Use proven libraries and standards instead of designing your own scheme.
Input and output: trust nothing
- Validate input: types, lengths, allowed values and formats, against a schema. Reject what does not fit.
- Use an allow-list of fields for updates, to avoid mass assignment, where a client sends extra fields such as
"role": "admin"that the server blindly saves. - Filter output: return only the fields the client needs, not the entire database record.
- Handle errors carefully: a generic message to the client, the details in internal logs.
- Limit sizes and rates: maximum body size, pagination limits, rate limiting per user and per address.
Inventory and monitoring
You cannot protect an API you do not know about. Keep a living inventory: every endpoint, version, owner, who consumes it and what data it touches. Retire old versions. Log requests (without storing secrets) and alert on abnormal behavior, such as one user requesting thousands of different object identifiers, which is a pattern of enumeration.
How testers approach an API, at a high level
- Learn the contract: documentation, specification files and the traffic of the application, within the scope.
- Map roles and objects: which users exist, which objects they own, which functions are privileged.
- Test authorization systematically: can user A reach user B's object? Can an ordinary user call an admin function? Do this using two test accounts you control, in a lab.
- Review business rules and limits: rate limits, quantity limits, sequence of steps.
- Report with evidence: request, response and impact, with sensitive data redacted.
Notice that this has nothing to do with "hacking tricks". It is careful, methodical comparison of what each role should and should not be able to do.
Your turn: audit a design on paper
A fictional food-delivery API has these endpoints. Identify at least four design problems and propose a fix for each.
GET /api/v1/orders/{id} returns any order if the token is valid
POST /api/v1/users accepts any JSON field, including "role"
GET /api/v1/users returns all users with email, phone and role
POST /api/v1/login no limit on attempts
GET /api/v0/orders/{id} old version, no authentication, still online
See a solution
- Orders by identifier: missing object-level authorization. Check that the order belongs to the signed-in user (or that the user has a defined support role).
- User creation with "role": mass assignment. Accept only an allow-list of fields and set the role on the server.
- Listing all users: excessive exposure and missing function-level authorization. Restrict to administrators, paginate and return only necessary fields.
- Login without limit: unrestricted attempts allow password guessing. Add rate limiting per account and address, delays, monitoring and multi-factor authentication.
- Old version without authentication: improper inventory management. Retire it or apply the same controls as the current version.
Portfolio step
Add api-security-review.md to 05-cloud-modern-apps with your solution and a second fictional API of your own design that has three different weaknesses, together with their fixes. If you want to extend your Python skills, you could write, for your own lab API, a small script that checks with two test accounts that user A cannot read user B's objects: a safe, defensive authorization test.
Key takeaways
- APIs expose logic and data directly; security must be enforced on the server for every request.
- The OWASP API Security Top 10 lists recurring risks, led by broken object-level authorization.
- Validate tokens fully, use HTTPS, limit rates and sizes, validate input and filter output.
- Use allow-lists to avoid mass assignment, and keep a living inventory of endpoints and versions.
- Test only your own APIs or authorized targets, using at least two test accounts to check authorization.
Next, we look at how modern applications are packaged and run: containers.