APIs Break in Different Ways Than Websites
A decade ago, most web application security assessments focused on HTML-rendered pages: form inputs, session cookies, URL parameters. The OWASP Top 10 reflected that world. The shift to API-first architecture — React and Vue frontends consuming JSON APIs, mobile apps talking directly to backend services, third-party integrations via REST and GraphQL — has changed the attack surface fundamentally.
APIs break in ways that differ from traditional web applications. The classic signs of compromise (defacement, phishing page injection, suspicious redirects) do not apply to an API that returns JSON to a mobile client. Exploitation is silent. A misconfigured authorisation check on a single endpoint can expose every user record in the database, and the only evidence in your logs might be a slightly elevated request rate from an unusual IP.
OWASP maintains a separate Top 10 for APIs specifically. The categories it highlights — broken object level authorisation, broken authentication, excessive data exposure — are the vulnerabilities that appear most consistently in real API penetration test reports.
Authentication: Getting the Basics Right
**Use established standards, not roll-your-own schemes.** OAuth 2.0 with PKCE for user-delegated access, API keys for service-to-service communication, JWT (JSON Web Tokens) for stateless authentication. Building a custom token scheme is almost always a mistake — the attack surface of authentication is well-understood, and the standards encode that understanding.
**JWT implementation mistakes** are among the most common authentication findings. Specific issues that appear regularly:
The "none" algorithm attack: JWT headers specify the signing algorithm. If your server accepts `"alg": "none"`, an attacker can forge tokens by removing the signature entirely and setting the algorithm to none. Validate that the algorithm is what you expect; reject anything else.
```javascript // Vulnerable: accepts whatever algorithm the token claims jwt.verify(token, secret);
// Safer: explicitly specify the expected algorithm jwt.verify(token, secret, { algorithms: ['HS256'] }); ```
Weak secrets: JWTs signed with HMAC (HS256, HS384, HS512) are only as secure as the secret key. A secret of "secret", "password", or the application name can be brute-forced offline in seconds using tools like Hashcat with the JWT-cracking module. Use a cryptographically random secret of at least 256 bits.
Not validating expiry: check `exp` (expiration) and `iat` (issued at) claims server-side. A JWT without an expiry, or with an expiry your server does not enforce, is a long-lived credential that cannot be revoked if compromised.
Broken Object Level Authorisation (BOLA)
This is the OWASP API Security Top 10's most critical category, and the one most likely to produce a critical finding in your pen test report.
The pattern: your API exposes resources via numeric or sequential identifiers.
``` GET /api/orders/10472 GET /api/users/8821/profile GET /api/invoices/3301 ```
If your code retrieves the resource but does not verify that the authenticated user is authorised to access *that specific resource*, an attacker can enumerate IDs and access any object in the database.
```python # Vulnerable: fetches order without checking ownership @app.route('/api/orders/<int:order_id>') @jwt_required def get_order(order_id): order = db.query(Order).filter_by(id=order_id).first() return jsonify(order)
# Correct: verifies the order belongs to the authenticated user @app.route('/api/orders/<int:order_id>') @jwt_required def get_order(order_id): user_id = get_jwt_identity() order = db.query(Order).filter_by(id=order_id, user_id=user_id).first() if not order: return jsonify({'error': 'Not found'}), 404 return jsonify(order) ```
Use UUIDs rather than sequential integers for resource IDs where possible — they are harder to enumerate. But authorisation checks must still be in place; UUIDs are not a substitute for access control.
Excessive Data Exposure
APIs commonly return full object representations and rely on the client to filter what it displays. The result: fields that serve no purpose for the client (password hashes, internal flags, other users' data, system metadata) are transmitted and exposed.
```json // What the API returns { "id": 8821, "email": "user@example.com", "name": "Alex Singh", "password_hash": "$2b$12$KIXdDk...", "admin_flag": false, "internal_notes": "VIP customer, do not restrict", "stripe_customer_id": "cus_Np..." }
// What the client needed { "id": 8821, "email": "user@example.com", "name": "Alex Singh" } ```
Define explicit response schemas for every endpoint. Return only what the client needs. Use a serialisation layer (Django REST Framework serializers, Pydantic response models, custom DTOs) that explicitly whitelists output fields rather than serialising entire ORM objects.
Rate Limiting and Abuse Prevention
Unauthenticated and authenticated endpoints both need rate limiting, but for different reasons.
Unauthenticated endpoints — login, password reset, account creation — need rate limiting to prevent brute force and credential stuffing. Limits on login attempts per IP, per username, and globally, combined with CAPTCHA or proof-of-work challenges after threshold events.
Authenticated endpoints need rate limiting to prevent data scraping. An authenticated user who makes 10,000 requests to `GET /api/users/{id}` in an hour is enumerating your user database. Without rate limiting and anomaly detection, that enumeration completes silently.
HTTP response headers should communicate rate limit state:
``` X-RateLimit-Limit: 100 X-RateLimit-Remaining: 47 X-RateLimit-Reset: 1727820000 Retry-After: 60 ```
Return HTTP 429 (Too Many Requests) when limits are exceeded, not a generic 400 or 500.
API Keys: Management and Rotation
If your API issues API keys to third-party integrators or internal services, key management is a security domain in itself.
Keys should be hashed before storage — store the hash, not the plaintext key. An attacker who reads your keys database gets useless hashes. The plaintext key is shown once, at generation time, and never stored.
Keys should have minimal scope — a key that can only read order data should not be able to create or delete orders. Implement key scopes and enforce them server-side.
Keys should be rotatable without downtime — integrators need a way to rotate keys without service interruption. This typically means a brief overlap period during rotation.
Audit key usage — log which key made which request. When a key is compromised, the audit log tells you what was accessed.
HTTPS and Transport Security
APIs must be HTTPS-only. Return HTTP 301 redirects from HTTP to HTTPS, and consider HSTS to prevent protocol downgrade attacks:
``` Strict-Transport-Security: max-age=31536000; includeSubDomains ```
Verify TLS configuration: disable TLS 1.0 and 1.1 (both deprecated), ensure cipher suite selection avoids known-weak options (RC4, DES, export ciphers). Test with tools like testssl.sh or SSL Labs' API scanner.
What UK API Pen Tests Find Most Often
Based on the vulnerability patterns that appear consistently in UK web application and API pen test reports: BOLA on resource endpoints (present in the majority of first-time assessments), missing rate limiting on authentication endpoints, JWT implementation weaknesses (weak secrets, missing expiry validation), excessive data exposure in user profile and search endpoints, and CORS misconfiguration allowing arbitrary origins to make credentialed requests.
Yrzo AI's automated scan covers the API security surface accessible via your web application — authentication endpoint behaviour, response data exposure, security header configuration, CORS policy, and TLS configuration. It won't replace a manual API pen test for complex authorisation logic, but it catches the low-hanging fruit before a human tester (or an attacker) does. From £99 at yrzoai.dev.
Find out if your website has these vulnerabilities
Yrzo AI runs 44 automated security checks and delivers a full report in under 20 minutes. Starting from £399.
Scan your website →