Skip to main content
Blog

Web

API security: a practical guide to the OWASP API Security Top 10

The API behind a mobile app or SPA is tested separately during the expertise. Each item of the OWASP API Top 10 (2023), how to check it yourself, and how to fix it in Laravel and Node.

· 11 min read

Photo: Syced, CC0 · Wikimedia Commons

A modern website is rarely "just a frontend". A React or Vue app in the browser, a mobile app, partner systems — they all talk to the same API. In the interface a user only sees the buttons they are allowed to press, but no button is needed to send a request straight to the API: curl or a proxy tool is enough. That is why API security has to rely on server-side checks, not on what the interface hides.

OWASP maintains a dedicated list for this area — the OWASP API Security Top 10 (2023). In this article we briefly explain each item, show how to test for it in your own system and how to fix it, using Laravel and Node (Express) as examples.

Why this matters for the expertise

During the official cybersecurity expertise, specialists test the application not only through the browser but at the API level: they intercept requests, swap identifiers, add extra fields, and replay requests without a token or with another user's token. During a mobile app expertise, API addresses are extracted from the app and the backend is tested separately. So an API flaw can show up in both the web report and the mobile report.

In the expertise of our own product, a tax and finance SaaS platform, some of the findings belonged to exactly this class: actions that could be performed without the proper state check (a business logic flaw), and excess data returned in responses (information disclosure).

API1: Broken Object Level Authorization (BOLA)

The first and most common problem. The API returns an object by its identifier but does not check that the object belongs to the user making the request. In web applications this is known as IDOR — we covered it in detail in a separate article.

To test: create two test accounts (A and B). Fetch an object with account A, then request the same ID with B's token:

curl -s https://api.your-domain.uz/v1/invoices/1042 \
  -H "Authorization: Bearer <B_TOKEN>"

The response should be 403 or 404, not A's data. Test not only GET but also PUT, PATCH, DELETE and file download endpoints.

The fix in Laravel is a policy per object or scoping the query by owner:

public function show(Invoice $invoice)
{
    $this->authorize('view', $invoice);
    return new InvoiceResource($invoice);
}

// or scope the query itself
$invoice = $request->user()->invoices()->findOrFail($id);

Same principle in Node/Express: look up by "ID + owner", not by ID alone.

const invoice = await db.invoice.findFirst({
  where: { id: req.params.id, ownerId: req.user.id },
});
if (!invoice) return res.sendStatus(404);

API2: Broken Authentication

Flaws in the authentication mechanism: unlimited password guessing, tokens that never expire, weakly signed tokens, gaps in the password reset flow. We wrote about protecting the login here and about token mistakes in the JWT article.

Questions to check:

  • Are attempts limited on /login, /password/reset, /otp/verify?
  • Do tokens expire, and is a token really invalidated after logout?
  • Is there an unprotected login path in the mobile API left over "for the old version"?

API3: Broken Object Property Level Authorization

This item merges two older problems: excessive data exposure and mass assignment. In the first, the API returns the whole model object and lets the frontend pick the fields it needs — while the response still carries password hashes, internal notes or other users' phone numbers. In the second, the API writes incoming JSON straight into the model, and a user can add "role": "admin" or "balance": 999999.

To test: look at responses in DevTools or in a proxy — are there fields the interface never shows? Add an extra field to a profile update request:

curl -s -X PATCH https://api.your-domain.uz/v1/profile \
  -H "Authorization: Bearer <TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"name":"Test","is_admin":true,"role":"admin"}'

Then read the profile again: did the role change?

The fix in Laravel is an API Resource for output and validated data for input:

// only allowed fields are written
$user->update($request->validated());

class UserResource extends JsonResource
{
    public function toArray($request): array
    {
        return ['id' => $this->id, 'name' => $this->name];
    }
}

Do not use $guarded = [] on models; list $fillable explicitly. In Node, likewise, do not pass req.body directly to update(): pick the needed fields with a schema (zod, joi).

API4: Unrestricted Resource Consumption

There is no limit on how many resources a single request may consume: unpaginated lists, ?limit=1000000, huge file uploads, unlimited SMS or email sending. The server slows down, and bills for paid services (an SMS gateway) grow.

In Laravel, the throttle middleware and a pagination cap:

RateLimiter::for('api', function (Request $request) {
    return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});

$perPage = min((int) $request->query('per_page', 20), 100);

In Express, the express-rate-limit package and a body size limit such as express.json({ limit: '100kb' }). At the web server level you can set client_max_body_size and limit_req — shown in the nginx article.

API5: Broken Function Level Authorization

Where BOLA is about objects, this item is about functions: a regular user can call an admin endpoint. Admin paths are often only hidden in the interface, while the server never checks the role.

To test: search the frontend JavaScript or the mobile app for paths like /admin, /internal, /manage and call them with a regular user's token. Where GET is allowed, try DELETE too.

In Laravel, group admin routes and protect them with middleware or a Gate:

Route::middleware(['auth:sanctum', 'can:admin'])
    ->prefix('admin')
    ->group(function () { /* ... */ });

API6: Unrestricted Access to Sensitive Business Flows

Technically everything is correct, but a business flow can be automated and abused: harvesting promo codes in bulk, buying limited stock with bots, automating sign-ups. The fix depends on context: per-user and per-device limits, server-side checks of the action sequence, monitoring of suspicious activity.

A related issue came up in our own expertise: a "payment successful" page was shown before the payment provider had confirmed the payment. There is a separate article about it. The core rule: accept a state change (paid, confirmed, shipped) only after a server-side check.

API7: Server Side Request Forgery (SSRF)

If the API fetches a URL supplied by the user (image import, webhooks, link previews), an attacker can use the server to reach the internal network or a cloud metadata service. Defences: an allowlist of domains, blocking internal IP ranges, limiting redirects.

API8: Security Misconfiguration

Wrong settings: debug mode in production, errors that return stack traces, unnecessary HTTP methods, missing security headers, misconfigured CORS. We have separate articles on each: debug mode, security headers, CORS.

A quick check — call a non-existent endpoint and send malformed JSON:

curl -s https://api.your-domain.uz/v1/nonexistent
curl -s -X POST https://api.your-domain.uz/v1/login \
  -H "Content-Type: application/json" -d '{bad json'

The response must not contain file paths, SQL queries or the framework version.

API9: Improper Inventory Management

Do you know which APIs you are running? Often v1 gets updated while v0 is kept for old mobile apps and forgotten. Or a staging.api subdomain sits open with production data. The expertise can find such "forgotten" doors.

We had a related finding ourselves: the full list of the application's routes was sent to the browser. That hands an attacker a ready-made map of the API. If you use Ziggy in Laravel, expose only the routes you need:

// config/ziggy.php
return [
    'only' => ['dashboard', 'invoices.*', 'profile.*'],
];

Practical steps: list all API versions and hosts, switch off the unused ones, and do not leave Swagger/OpenAPI docs open in production — or put them behind authentication.

API10: Unsafe Consumption of APIs

Your API consumes other APIs (payment systems, government services, maps). If you blindly trust an external response — writing it to the database or rendering it without checks — a problem in that external system becomes yours. Validate external responses too, connect only over HTTPS, set a timeout and do not follow redirects automatically.

With Laravel's HTTP client:

$response = Http::timeout(10)
    ->withOptions(['allow_redirects' => false])
    ->get('https://partner.example/api/status');

$data = validator($response->json(), [
    'status' => 'required|in:paid,pending,failed',
])->validate();

A short self-check plan

  1. List all API endpoints (php artisan route:list --path=api or your Express routers).
  2. Prepare two test accounts and one admin account.
  3. Call every endpoint without a token, with another user's token, and call admin functions with a regular user's token.
  4. Send extra fields to write endpoints.
  5. Compare responses with what the interface actually shows.
  6. Test rate limiting on login, OTP and SMS endpoints.
  7. Record results in a table and retest each finding after it is fixed.

Do this only on systems you own and with test data.

In an API, the security boundary is the server. A button hidden in the interface is not protection.

Checklist

  • Every object endpoint checks ownership (a policy or a scoped query).
  • Admin and internal functions are protected by role on the server.
  • Responses go through API Resources or DTOs, with only the needed fields.
  • Write requests accept only validated fields.
  • Login, OTP, SMS and heavy endpoints have rate limits and a pagination cap.
  • Debug is off; errors return a generic message.
  • Old API versions, test hosts and public docs are inventoried.
  • External API responses are validated and timeouts are set.

Conclusion

The OWASP API Top 10 is not theory; it is a list of concrete questions that get tested during the expertise. Most of them come down to one rule: for every request, the server must answer "is this user allowed to perform this action, on this object, at this volume?"

Want to review your API against this list before the expertise? Request a security scan — the Claude AI agent quickly goes through your endpoints and code, and our specialist confirms the findings and helps you close the gaps.