
Understanding OWASP
OWASP API Security Top 10
What is OWASP?
The Open Web Application Security Project (OWASP) is a non-profit foundation focused on improving software security. It provides open-source tools, documentation, methodologies, and community-driven standards to help organizations build and maintain secure applications.
OWASP is widely used as a baseline reference for application security engineering and auditing across the industry.
OWASP History and Background
OWASP originated in the early 2000s as a response to the increasing number of web-based vulnerabilities emerging from poorly secured applications.
Key Points
-
Founded in 2001
-
Community-driven and vendor-neutral organization
-
Focused on application security (AppSec) rather than network security
-
Produces widely adopted standards such as:
- OWASP Top 10 Web
- OWASP API Security Top 10
- ASVS: Application Security Verification Standard
- Testing Guide and Cheat Sheets
The organization operates under an open contribution model, meaning its standards evolve based on real-world security research and industry feedback.
Rapid Rise of APIs
Modern software architecture has shifted significantly.
Major Changes
-
Transition from monolithic systems to microservices
-
Increased adoption of:
- REST APIs
- GraphQL APIs
- gRPC services
-
Mobile applications and frontend frameworks now rely heavily on backend APIs
Implication
Applications are no longer single attack surfaces.
Instead, they are distributed systems exposed through multiple endpoints.
This expansion dramatically increases the attack surface area.
The Gap in API Security
Despite heavy API adoption, security practices often lag behind.
Common Issues
- APIs are treated as internal components, not public-facing assets
- Lack of consistent authentication and authorization enforcement
- Weak input validation at endpoint level
- Incomplete API inventory, including unknown or undocumented endpoints
- Overexposed data models, also known as excessive object exposure
Critical Gap
Traditional web security controls focused on browsers do not fully protect API-first architectures.
The Leading Attack Vector
APIs have become one of the primary attack vectors in modern systems.
Why Attackers Target APIs
- Direct access to backend logic and data
- Ability to bypass UI-level protections
- High likelihood of misconfiguration
- Exposure of sensitive business logic, not only data
Typical Exploitation Outcomes
- Data exfiltration, including PII, credentials, and financial data
- Account takeover through broken authentication
- Business logic abuse, such as fraud or workflow bypass
- Mass data scraping
OWASP API Security Top 10
The OWASP API Security Top 10 identifies the most critical API vulnerabilities observed in real-world systems.
Each section below includes:
- A description
- A vulnerable code example
- A fixed code example
- Key mitigations
API1: Broken Object Level Authorization
Description
The API fetches a resource by ID taken directly from the request without checking whether the requesting user actually owns or has permission to access that object.
Vulnerable Code
// No ownership check at all — any authenticated user can access any order
app.get('/api/orders/:orderId', async (req, res) => {
const order = await db.query(
'SELECT * FROM orders WHERE id = ?',
[req.params.orderId] // attacker supplies any orderId
);
res.json(order); // returns whoever's order matches the ID
});
Fixed Code
// Scope the query to the authenticated user — both conditions must match
app.get('/api/orders/:orderId', authenticate, async (req, res) => {
const order = await db.query(
'SELECT * FROM orders WHERE id = ? AND user_id = ?',
[req.params.orderId, req.user.id] // user_id ties the record to the caller
);
if (!order) return res.status(403).json({ error: 'Forbidden' });
res.json(order);
});
Key Mitigations
- Always add the authenticated user's ID as a second
WHEREcondition on every object query. - Implement a reusable authorization helper and use it consistently across all resource endpoints.
- Use UUIDs instead of sequential IDs to slow enumeration attacks.
- Write integration tests that assert cross-user access returns
403.
API2: Broken Authentication
Description
Authentication mechanisms are weak, improperly implemented, or missing entirely, allowing attackers to impersonate legitimate users.
Vulnerable Code
// JWT secret is trivially brute-forceable and algorithm is not pinned,
// which opens the door to algorithm confusion attacks (e.g., 'none' algorithm)
const token = jwt.sign(
{ userId: user.id, role: user.role },
'secret', // weak, hardcoded secret
{ expiresIn: '30d' }
);
// Verification does not lock down the algorithm
jwt.verify(token, 'secret'); // accepts any algorithm including 'none'
Fixed Code
// Strong secret loaded from environment, algorithm pinned, short TTL applied
const JWT_SECRET = process.env.JWT_SECRET; // must be a >=256-bit random value
if (!JWT_SECRET) throw new Error('JWT_SECRET environment variable is not set');
const token = jwt.sign(
{ userId: user.id, role: user.role },
JWT_SECRET,
{ algorithm: 'HS256', expiresIn: '15m' } // short-lived access token
);
// Pin the algorithm on verify to prevent confusion attacks
jwt.verify(token, JWT_SECRET, { algorithms: ['HS256'] });
Key Mitigations
- Store secrets in environment variables; never commit them to source control.
- Pin the JWT algorithm explicitly on both sign and verify.
- Keep access token TTL short, such as 15 minutes.
- Use refresh tokens for long sessions.
- Rate-limit and lock accounts after repeated failed authentication attempts.
API3: Broken Object Property Level Authorization
Description
The API blindly binds all incoming request fields to the model. This is also called mass assignment.
Attackers can modify properties they should never control, such as their own role or account balance.
Vulnerable Code
// Spreads the entire request body onto the UPDATE — attacker sends { role: 'admin' }
app.put('/api/users/:id', authenticate, async (req, res) => {
await db.query(
'UPDATE users SET ? WHERE id = ?',
[req.body, req.params.id] // no field filtering whatsoever
);
res.json({ updated: true });
});
Fixed Code
// Explicitly whitelist the fields a user is allowed to modify
const ALLOWED_USER_FIELDS = ['name', 'email', 'bio', 'avatarUrl'];
app.put('/api/users/:id', authenticate, async (req, res) => {
// Build a patch object from whitelisted keys only; discard everything else
const patch = Object.fromEntries(
Object.entries(req.body).filter(([key]) => ALLOWED_USER_FIELDS.includes(key))
);
if (Object.keys(patch).length === 0) {
return res.status(400).json({ error: 'No valid fields provided' });
}
await db.query('UPDATE users SET ? WHERE id = ?', [patch, req.params.id]);
res.json({ updated: true });
});
Key Mitigations
- Define an explicit allowlist of writable fields per operation, not per model.
- Use a DTO or schema validation library such as Zod or Joi.
- Never use an ORM's auto-mapping features without an explicit field whitelist.
- Apply read-side authorization as well: return only the fields the caller is permitted to see.
API4: Unrestricted Resource Consumption
Description
No rate limiting, payload size limits, or query complexity constraints allow attackers to exhaust server CPU, memory, database connections, or third-party API quotas.
This can lead to denial-of-service conditions.
Vulnerable Code
// No rate limiting, no payload cap, no pagination — open to DoS
app.post('/api/search', async (req, res) => {
// Attacker can POST gigabyte payloads or issue thousands of requests per second
const results = await db.query(
'SELECT * FROM products WHERE name LIKE ?',
[`%${req.body.keyword}%`]
);
res.json(results); // returns unbounded result set
});
Fixed Code
import rateLimit from 'express-rate-limit';
import { json } from 'express';
// Global 100 KB payload cap applied to all body parsers
app.use(json({ limit: '100kb' }));
// Per-IP rate limiter scoped to expensive endpoints
const searchLimiter = rateLimit({
windowMs: 60_000, // 1-minute sliding window
max: 30, // maximum 30 requests per window per IP
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Too many requests — please try again later.' }
});
app.post('/api/search', searchLimiter, async (req, res) => {
const { keyword, page = 1, pageSize = 20 } = req.body;
// Clamp pagination values server-side regardless of what the client sends
const safePage = Math.max(1, parseInt(page, 10));
const safeSize = Math.min(100, Math.max(1, parseInt(pageSize, 10)));
const results = await db.query(
'SELECT * FROM products WHERE name LIKE ? LIMIT ? OFFSET ?',
[`%${keyword}%`, safeSize, (safePage - 1) * safeSize]
);
res.json(results);
});
Key Mitigations
- Apply per-IP and per-user rate limits at the gateway or middleware layer.
- Enforce maximum payload sizes on all body parsers.
- Paginate every list endpoint.
- Enforce a server-side maximum page size cap.
- Set read and write timeouts on all database queries and outbound HTTP calls.
API5: Broken Function Level Authorization
Description
Admin or privileged operations are exposed through the same API but rely only on the client to hide them.
Attackers can enumerate hidden endpoints and invoke them without any server-side role check.
Vulnerable Code
// Admin endpoint is reachable by any authenticated user — no role check at all
app.delete('/api/admin/users/:id', authenticate, async (req, res) => {
// Any valid token — regardless of role — can delete any account
await db.query('DELETE FROM users WHERE id = ?', [req.params.id]);
res.json({ deleted: true });
});
Fixed Code
// Reusable middleware that enforces a minimum required role
function requireRole(...roles) {
return (req, res, next) => {
if (!req.user || !roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
// Authenticate first, then enforce the 'admin' role before the handler runs
app.delete(
'/api/admin/users/:id',
authenticate,
requireRole('admin'), // non-admins receive 403 immediately
async (req, res) => {
await db.query('DELETE FROM users WHERE id = ?', [req.params.id]);
res.json({ deleted: true });
}
);
Key Mitigations
- Define roles at the data model level.
- Enforce roles in middleware, not in the client UI.
- Group admin routes under a dedicated prefix and protect the entire prefix with a single middleware.
- Audit every route and document the minimum required role as part of the route definition.
- Write automated tests that assert non-admin tokens receive
403on every privileged route.
API6: Unrestricted Access to Sensitive Business Flows
Description
Business-critical workflows such as checkout, voting, or coupon redemption lack bot detection or per-user limits.
This enables automated abuse at scale, such as bulk inventory hoarding or vote stuffing.
Vulnerable Code
// Coupon can be redeemed unlimited times by the same user — no usage tracking
app.post('/api/checkout/apply-coupon', authenticate, async (req, res) => {
const coupon = await db.query(
'SELECT * FROM coupons WHERE code = ? AND active = 1',
[req.body.code]
);
if (!coupon) return res.status(400).json({ error: 'Invalid coupon' });
// No check: has this user already used this coupon?
await applyDiscount(req.user.id, coupon.discount);
res.json({ applied: true });
});
Fixed Code
app.post(
'/api/checkout/apply-coupon',
authenticate,
couponRateLimiter, // max N attempts per IP per hour at the gateway
async (req, res) => {
const coupon = await db.query(
'SELECT * FROM coupons WHERE code = ? AND active = 1',
[req.body.code]
);
if (!coupon) return res.status(400).json({ error: 'Invalid coupon' });
// Enforce single-use constraint per user
const alreadyUsed = await db.query(
'SELECT 1 FROM coupon_usage WHERE coupon_id = ? AND user_id = ?',
[coupon.id, req.user.id]
);
if (alreadyUsed) {
return res.status(409).json({ error: 'Coupon already used' });
}
// Record usage atomically before applying discount to prevent race conditions
await db.transaction(async (trx) => {
await trx.query(
'INSERT INTO coupon_usage (coupon_id, user_id) VALUES (?, ?)',
[coupon.id, req.user.id]
);
await applyDiscount(req.user.id, coupon.discount, trx);
});
res.json({ applied: true });
}
);
Key Mitigations
- Record business-flow consumption per user.
- Enforce hard limits at the database level.
- Use database transactions to prevent race conditions in concurrent redemptions.
- Add CAPTCHA or device fingerprinting on high-value flows exposed to anonymous users.
- Monitor for statistical anomalies such as redemption spikes from a single IP or account.
API7: Server-Side Request Forgery
Description
Server-Side Request Forgery, or SSRF, happens when an API accepts a user-supplied URL and fetches it on the server's behalf.
Attackers can use this to reach internal services that are not publicly accessible, such as:
- Metadata APIs
- Databases
- Admin panels
- Internal dashboards
- Private network services
Vulnerable Code
// Fetches any URL the user provides without any validation
app.post('/api/fetch-preview', authenticate, async (req, res) => {
// Attacker sends: { url: 'http://169.254.169.254/latest/meta-data/' }
// The server fetches cloud instance metadata and returns it directly to the attacker
const response = await fetch(req.body.url);
const html = await response.text();
res.json({ preview: html });
});
Fixed Code
import { URL } from 'url';
import dns from 'dns/promises';
// Only HTTPS is permitted; HTTP is blocked
const ALLOWED_SCHEMES = new Set(['https:']);
// Block private, loopback, link-local, and RFC-1918 ranges
const BLOCKED_HOSTS = /^(localhost|127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|169\.254\.)/;
async function isSafeUrl(rawUrl) {
let parsed;
try {
parsed = new URL(rawUrl);
} catch {
return false;
}
if (!ALLOWED_SCHEMES.has(parsed.protocol)) return false;
// Resolve DNS and check every resolved IP against the blocklist
const addresses = await dns.resolve4(parsed.hostname).catch(() => []);
return addresses.length > 0 && !addresses.some((ip) => BLOCKED_HOSTS.test(ip));
}
app.post('/api/fetch-preview', authenticate, async (req, res) => {
if (!(await isSafeUrl(req.body.url))) {
return res.status(400).json({ error: 'URL not allowed' });
}
// Disable redirects — each redirect target must be independently validated
const response = await fetch(req.body.url, { redirect: 'error' });
res.json({ preview: await response.text() });
});
Key Mitigations
- Validate and resolve URLs to IPs.
- Block private, loopback, and link-local ranges after DNS resolution.
- Disable automatic redirects or re-validate every redirect target independently.
- Use an allowlist of permitted domains if the use case is constrained enough.
- Run the fetch worker in a sandboxed network namespace with no internal routing access.
API8: Security Misconfiguration
Description
Security misconfiguration happens when default configurations, missing security headers, exposed debug endpoints, verbose error messages, or permissive CORS settings reveal internal details and expand the attack surface.
Vulnerable Code
// CORS is open to all origins — credentials will leak to any website
app.use(cors());
// Debug endpoint dumps the entire process environment including secrets
app.get('/debug/config', (req, res) => {
res.json(process.env); // leaks DB passwords, JWT secrets, API keys
});
// Stack traces are returned to the client in production
app.use((err, req, res, next) => {
res.status(500).json({ error: err.stack }); // reveals file paths and library versions
});
Fixed Code
import helmet from 'helmet';
// Apply security headers: CSP, HSTS, X-Frame-Options, X-Content-Type-Options, etc.
app.use(helmet());
// Restrict CORS to an explicit list of trusted origins
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') ?? [],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true
}));
// Debug endpoint is only mounted outside production, and is role-gated
if (process.env.NODE_ENV !== 'production') {
app.get('/debug/config', requireRole('admin'), (req, res) => {
res.json({ env: process.env.NODE_ENV }); // never dump the full env object
});
}
// Generic error handler — no internal details reach the client
app.use((err, req, res, next) => {
console.error(err); // log internally with a correlation ID for tracing
res.status(err.status ?? 500).json({
error: 'An unexpected error occurred.'
});
});
Key Mitigations
- Use a security header middleware such as Helmet on every Express or Fastify application.
- Restrict CORS to an explicit allowlist.
- Never use a wildcard origin with
credentials: true. - Remove or gate all
/debug,/swagger, and/metricsendpoints in production builds. - Return generic error messages to clients.
- Log full stack traces server-side with a trace ID.
API9: Improper Inventory Management
Description
Old, shadow, or undocumented API versions remain accessible long after they have been superseded.
Attackers can discover them through enumeration and exploit missing security controls that exist only in newer versions.
Vulnerable Code
// v1 is still mounted — it predates authentication middleware and rate limiting
app.use('/api/v1', v1Router); // forgotten endpoint; no auth, no rate limits
app.use('/api/v2', v2Router); // current version with full security controls
// A v1 endpoint with no authorization that exposes raw PII
v1Router.get('/users/:id', async (req, res) => {
const user = await db.query(
'SELECT * FROM users WHERE id = ?',
[req.params.id]
);
res.json(user); // returns SSN, date of birth, address — all unprotected
});
Fixed Code
// Maintain an explicit registry of every mounted API version and its lifecycle status
const API_VERSIONS = {
v1: { deprecated: true, sunsetDate: '2024-06-01', router: v1Router },
v2: { deprecated: false, sunsetDate: null, router: v2Router }
};
// Middleware that rejects deprecated versions past their sunset date,
// or adds warning headers during the grace period
function versionGuard(versionInfo) {
return (req, res, next) => {
if (versionInfo.deprecated) {
const pastSunset = new Date() > new Date(versionInfo.sunsetDate);
if (pastSunset) {
// Return 410 Gone — do not silently serve old traffic
return res.status(410).json({
error: 'This API version has been retired.'
});
}
// Within grace period — warn the caller but allow the request through
res.setHeader('Deprecation', versionInfo.sunsetDate);
res.setHeader('Sunset', versionInfo.sunsetDate);
}
next();
};
}
for (const [version, info] of Object.entries(API_VERSIONS)) {
app.use(`/api/${version}`, versionGuard(info), info.router);
}
Key Mitigations
- Maintain a living inventory of every API version, endpoint, and its current exposure status.
- Return
DeprecationandSunsetheaders throughout the grace period to notify API consumers. - Return HTTP
410 Goneafter the sunset date. - Never silently keep old routes alive.
- Automate route discovery in CI to detect newly added endpoints that lack documentation.
API10: Unsafe Consumption of APIs
Description
The application blindly trusts and forwards data from third-party APIs without validation, sanitization, or error handling.
This allows malicious or malformed upstream data to propagate into the system.
Vulnerable Code
// Blindly trusts and persists whatever the third-party API returns
app.post('/api/enrich-profile', authenticate, async (req, res) => {
const response = await fetch(
`https://api.third-party.com/enrich?email=${req.body.email}`
);
// No schema validation, no timeout, no error handling
const enrichedData = await response.json();
// Raw upstream data goes directly into the database UPDATE
await db.query('UPDATE users SET ? WHERE id = ?', [enrichedData, req.user.id]);
res.json({ updated: true });
});
Fixed Code
import { z } from 'zod';
// Define exactly what the third-party response is allowed to contain
const EnrichmentSchema = z.object({
company: z.string().max(200).optional(),
jobTitle: z.string().max(200).optional(),
linkedIn: z.string().url().optional()
}).strict(); // .strict() rejects any unexpected extra fields entirely
app.post('/api/enrich-profile', authenticate, async (req, res) => {
let raw;
try {
const response = await fetch('https://api.third-party.com/enrich', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: req.body.email }),
signal: AbortSignal.timeout(5000) // hard 5-second timeout
});
if (!response.ok) {
throw new Error(`Upstream returned ${response.status}`);
}
raw = await response.json();
} catch (err) {
console.error('Third-party API error:', err.message);
return res.status(502).json({
error: 'Enrichment service unavailable.'
});
}
// Validate and strip unknown fields before any database interaction
const result = EnrichmentSchema.safeParse(raw);
if (!result.success) {
console.warn('Upstream schema mismatch:', result.error.flatten());
return res.status(502).json({
error: 'Unexpected upstream response format.'
});
}
// Only the validated, whitelisted fields reach the database
await db.query('UPDATE users SET ? WHERE id = ?', [result.data, req.user.id]);
res.json({ updated: true });
});
Key Mitigations
- Validate every third-party response against a strict schema before using any of its data.
- Set both connection and read timeouts on all outbound HTTP calls.
- Never pass raw upstream data directly to database queries or template engines.
- Log and alert on upstream schema mismatches because they often signal breaking supply-chain changes.
References
- OWASP API Security Project
- Tutorial:
https://www.youtube.com/watch?v=YYe0FdfdgDU