API Security Hardening in 2026: OWASP API Security Top 10 implementation guide
APIs are the attack surface of 2026. Implementing the OWASP API Security Top 10 is no longer optional—it's business hygiene.
Executive summary
APIs are the attack surface of 2026. Implementing the OWASP API Security Top 10 is no longer optional—it's business hygiene.
Last updated: 3/20/2026
Executive summary
In 2026, APIs represent the largest attack surface for most organizations. As applications decompose into microservices, adopt event-driven architectures, and expose functionality through APIs, the traditional security perimeter has dissolved. API security is no longer an add-on—it's a foundational requirement for production systems.
The OWASP API Security Top 10 provides a framework for securing APIs against the most critical vulnerabilities. This guide translates those risks into actionable implementation patterns, code examples, and operational practices for production APIs.
API1:2023 – Broken Object Level Authorization
The vulnerability
APIs tend to expose endpoints that handle object identifiers (UUIDs, auto-incrementing IDs). Without proper authorization checks, attackers can manipulate these identifiers to access resources belonging to other users.
Implementation pattern
typescript// BAD: No authorization check
app.get('/api/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
res.json(user);
});
// GOOD: Explicit authorization check
app.get('/api/users/:id', async (req, res) => {
const requestingUserId = req.user.id; // From JWT/session
const requestedUserId = req.params.id;
// Authorization check: users can only access their own data
if (requestingUserId !== requestedUserId) {
return res.status(403).json({
error: 'Forbidden',
message: 'You do not have permission to access this resource'
});
}
const user = await db.users.findById(requestedUserId);
res.json(user);
});
// BETTER: Authorization abstraction
interface AuthorizationPolicy {
canAccess(userId: string, resource: any): boolean;
}
class OwnResourceOnlyPolicy implements AuthorizationPolicy {
canAccess(userId: string, resource: any): boolean {
return resource.userId === userId;
}
}
class AdminOrOwnResourcePolicy implements AuthorizationPolicy {
canAccess(userId: string, resource: any): boolean {
const user = resource;
return user.isAdmin || user.userId === userId;
}
}
// Usage with middleware
function authorizeResource(policy: AuthorizationPolicy) {
return async (req: Request, res: Response, next: NextFunction) => {
const userId = req.user.id;
const resource = await getResourceFromRequest(req);
if (!policy.canAccess(userId, resource)) {
return res.status(403).json({
error: 'Forbidden',
message: 'You do not have permission to access this resource'
});
}
req.resource = resource;
next();
};
}
app.get('/api/users/:id',
authenticate,
authorizeResource(new OwnResourceOnlyPolicy()),
(req, res) => {
res.json(req.resource);
}
);###防止BOLA的最佳实践
| Practice | Implementation |
|---|---|
| Use UUIDs instead of sequential IDs | Prevents enumeration attacks |
| Implement access control checks | Check every request against permissions |
| Scope API tokens | Limit tokens to specific resources/operations |
| Audit access patterns | Monitor for unusual access patterns |
API2:2023 – Broken Authentication
The vulnerability
Authentication mechanisms often have implementation flaws that allow attackers to compromise authentication tokens, passwords, or session identifiers.
Implementation pattern
typescript// JWT-based authentication with security best practices
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';
import crypto from 'crypto';
interface AuthConfig {
jwtSecret: string;
jwtExpiresIn: string;
refreshTokenExpiresIn: number; // days
bcryptRounds: number;
maxFailedAttempts: number;
lockoutDuration: number; // minutes
}
class AuthenticationService {
private failedAttempts: Map<string, { count: number; lockUntil: Date }> = new Map();
constructor(private config: AuthConfig) {}
async hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, this.config.bcryptRounds);
}
async verifyPassword(password: string, hash: string): Promise<boolean> {
// Check if account is locked
const attempt = this.failedAttempts.get(password);
if (attempt && attempt.lockUntil > new Date()) {
throw new Error('Account temporarily locked');
}
const isValid = await bcrypt.compare(password, hash);
if (!isValid) {
await this.recordFailedAttempt(password);
throw new Error('Invalid credentials');
}
// Reset failed attempts on successful login
this.failedAttempts.delete(password);
return true;
}
private async recordFailedAttempt(identifier: string): Promise<void> {
const attempt = this.failedAttempts.get(identifier) || { count: 0, lockUntil: new Date(0) };
attempt.count++;
if (attempt.count >= this.config.maxFailedAttempts) {
attempt.lockUntil = new Date(Date.now() + this.config.lockoutDuration * 60 * 1000);
}
this.failedAttempts.set(identifier, attempt);
}
generateTokens(userId: string, userRole: string): { accessToken: string; refreshToken: string } {
const accessToken = jwt.sign(
{ userId, role: userRole },
this.config.jwtSecret,
{ expiresIn: this.config.jwtExpiresIn }
);
const refreshToken = crypto.randomBytes(32).toString('hex');
return { accessToken, refreshToken };
}
verifyAccessToken(token: string): any {
try {
return jwt.verify(token, this.config.jwtSecret);
} catch (error) {
throw new Error('Invalid or expired token');
}
}
}
// Refresh token rotation
class RefreshTokenService {
private tokens = new Map<string, { userId: string; expires: Date; previousToken?: string }>();
async createRefreshToken(userId: string): Promise<string> {
const token = crypto.randomBytes(32).toString('hex');
const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
this.tokens.set(token, { userId, expires });
return token;
}
async rotateRefreshToken(oldToken: string): Promise<string> {
const tokenData = this.tokens.get(oldToken);
if (!tokenData || tokenData.expires < new Date()) {
throw new Error('Invalid or expired refresh token');
}
// Invalidate old token
this.tokens.delete(oldToken);
// Create new token
return this.createRefreshToken(tokenData.userId);
}
async revokeToken(token: string): Promise<void> {
this.tokens.delete(token);
}
}
// Authentication middleware
function authenticate(authService: AuthenticationService) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
error: 'Unauthorized',
message: 'Missing or invalid authorization header'
});
}
const token = authHeader.substring(7);
const decoded = authService.verifyAccessToken(token);
req.user = {
id: decoded.userId,
role: decoded.role
};
next();
} catch (error) {
return res.status(401).json({
error: 'Unauthorized',
message: 'Invalid or expired token'
});
}
};
}###防止Broken Authentication的最佳实践
| Practice | Implementation |
|---|---|
| Implement rate limiting on auth endpoints | Prevent brute force attacks |
| Use secure password hashing | bcrypt with appropriate work factor |
| Implement account lockout | Temporary lockout after failed attempts |
| Rotate refresh tokens | Single-use refresh tokens |
| Implement token revocation | Ability to revoke compromised tokens |
API3:2023 – Broken Object Property Level Authorization
The vulnerability
APIs that automatically include all object properties in responses can leak sensitive data or allow unauthorized modifications.
Implementation pattern
typescript// Data Transfer Object (DTO) pattern for response filtering
interface UserDTO {
id: string;
email: string;
name: string;
createdAt: Date;
}
interface SensitiveUserDTO extends UserDTO {
phoneNumber: string;
address: string;
lastLogin: Date;
}
class UserResponseFactory {
toPublicDTO(user: User): UserDTO {
return {
id: user.id,
email: user.email,
name: user.name,
createdAt: user.createdAt
};
}
toSensitiveDTO(user: User, requester: User): SensitiveUserDTO | UserDTO {
// Only include sensitive fields for admins or the user themselves
if (requester.isAdmin || requester.id === user.id) {
return {
...this.toPublicDTO(user),
phoneNumber: user.phoneNumber,
address: user.address,
lastLogin: user.lastLogin
};
}
return this.toPublicDTO(user);
}
}
// Request validation with allowed fields
class RequestFieldValidator {
private allowedFieldsByRole: Map<string, string[]> = new Map();
constructor() {
this.allowedFieldsByRole.set('user', ['name', 'email']);
this.allowedFieldsByRole.set('admin', ['name', 'email', 'role', 'phoneNumber']);
}
validateUpdateRequest(userRole: string, requestedFields: string[]): void {
const allowedFields = this.allowedFieldsByRole.get(userRole) || [];
const invalidFields = requestedFields.filter(
field => !allowedFields.includes(field)
);
if (invalidFields.length > 0) {
throw new Error(
`Unauthorized fields: ${invalidFields.join(', ')}`
);
}
}
}
// Usage in API endpoint
app.patch('/api/users/:id',
authenticate,
async (req, res) => {
const user = await db.users.findById(req.params.id);
const responseFactory = new UserResponseFactory();
const fieldValidator = new RequestFieldValidator();
try {
// Validate requested fields
fieldValidator.validateUpdateRequest(
req.user.role,
Object.keys(req.body)
);
// Update only allowed fields
const allowedFields = fieldValidator.getAllowedFields(req.user.role);
const updates = pick(req.body, allowedFields);
const updatedUser = await db.users.update(user.id, updates);
const dto = responseFactory.toSensitiveDTO(updatedUser, req.user);
res.json(dto);
} catch (error) {
res.status(403).json({
error: 'Forbidden',
message: error.message
});
}
}
);API4:2023 – Unrestricted Resource Consumption
The vulnerability
APIs that don't limit resource consumption (requests, data size, computation) are vulnerable to DoS attacks and cost exploitation.
Implementation pattern
typescript// Rate limiting implementation
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
// Rate limiter configuration
interface RateLimitConfig {
windowMs: number;
maxRequests: number;
keyPrefix: string;
}
function createRateLimiter(config: RateLimitConfig, redis?: any) {
const store = redis ? new RedisStore({
client: redis,
prefix: config.keyPrefix
}) : undefined;
return rateLimit({
store,
windowMs: config.windowMs,
max: config.maxRequests,
standardHeaders: true,
legacyHeaders: false,
handler: (req, res) => {
res.status(429).json({
error: 'Too Many Requests',
message: 'Rate limit exceeded',
retryAfter: Math.ceil(config.windowMs / 1000)
});
}
});
}
// Different rate limits for different endpoints
const apiLimiter = createRateLimiter({
windowMs: 60 * 1000, // 1 minute
maxRequests: 100,
keyPrefix: 'api_limit'
});
const authLimiter = createRateLimiter({
windowMs: 15 * 60 * 1000, // 15 minutes
maxRequests: 5,
keyPrefix: 'auth_limit'
});
const expensiveOperationLimiter = createRateLimiter({
windowMs: 60 * 1000,
maxRequests: 10,
keyPrefix: 'expensive_limit'
});
// Apply rate limiters
app.use('/api/', apiLimiter);
app.use('/api/auth/', authLimiter);
app.use('/api/export/', expensiveOperationLimiter);
// Request size limiting
app.use(express.json({
limit: '1mb' // Limit request body size
}));
// Query complexity limiting
interface QueryCostCalculator {
calculateCost(query: any): number;
}
class GraphQLQueryCostCalculator implements QueryCostCalculator {
calculateCost(query: any): number {
// Simple cost calculation based on depth and fields
let cost = 0;
let depth = 0;
const traverse = (node: any, currentDepth: number) => {
depth = Math.max(depth, currentDepth);
cost += 1; // Base cost per node
if (node.selectionSet) {
node.selectionSet.selections.forEach((selection: any) => {
traverse(selection, currentDepth + 1);
});
}
};
traverse(query, 0);
// Penalize deep queries
if (depth > 5) {
cost *= Math.pow(2, depth - 5);
}
return cost;
}
}
// Timeout for expensive operations
const timeoutMiddleware = (timeoutMs: number) => {
return (req: Request, res: Response, next: NextFunction) => {
const timeout = setTimeout(() => {
if (!res.headersSent) {
res.status(504).json({
error: 'Gateway Timeout',
message: 'Request exceeded time limit'
});
}
}, timeoutMs);
res.on('finish', () => clearTimeout(timeout));
next();
};
};
app.get('/api/expensive-report',
timeoutMiddleware(30000), // 30 second timeout
expensiveOperationLimiter,
async (req, res) => {
// ... expensive operation
}
);API5:2023 – Broken Function Level Authorization
The vulnerability
APIs that hide admin functions behind obscurity rather than proper authorization allow attackers to access privileged functionality.
Implementation pattern
typescript// Role-based access control (RBAC)
enum Role {
USER = 'user',
MODERATOR = 'moderator',
ADMIN = 'admin'
}
interface Permission {
resource: string;
action: string;
}
interface RolePermissions {
role: Role;
permissions: Permission[];
}
class AuthorizationService {
private rolePermissions: Map<Role, Permission[]> = new Map();
constructor() {
this.initializePermissions();
}
private initializePermissions(): void {
this.rolePermissions.set(Role.USER, [
{ resource: 'users', action: 'read' },
{ resource: 'posts', action: 'create' },
{ resource: 'posts', action: 'read' }
]);
this.rolePermissions.set(Role.MODERATOR, [
{ resource: 'users', action: 'read' },
{ resource: 'posts', action: 'create' },
{ resource: 'posts', action: 'read' },
{ resource: 'posts', action: 'update' },
{ resource: 'posts', action: 'delete' }
]);
this.rolePermissions.set(Role.ADMIN, [
{ resource: 'users', action: 'read' },
{ resource: 'users', action: 'update' },
{ resource: 'users', action: 'delete' },
{ resource: 'posts', action: 'create' },
{ resource: 'posts', action: 'read' },
{ resource: 'posts', action: 'update' },
{ resource: 'posts', action: 'delete' },
{ resource: 'settings', action: 'update' }
]);
}
hasPermission(role: Role, resource: string, action: string): boolean {
const permissions = this.rolePermissions.get(role);
if (!permissions) {
return false;
}
return permissions.some(
permission => permission.resource === resource && permission.action === action
);
}
}
// Authorization middleware factory
function requirePermission(authService: AuthorizationService, resource: string, action: string) {
return (req: Request, res: Response, next: NextFunction) => {
const userRole = req.user.role as Role;
if (!authService.hasPermission(userRole, resource, action)) {
return res.status(403).json({
error: 'Forbidden',
message: 'You do not have permission to perform this action'
});
}
next();
};
}
// Usage in API routes
app.delete('/api/users/:id',
authenticate,
requirePermission(new AuthorizationService(), 'users', 'delete'),
async (req, res) => {
await db.users.delete(req.params.id);
res.status(204).send();
}
);
app.patch('/api/settings',
authenticate,
requirePermission(new AuthorizationService(), 'settings', 'update'),
async (req, res) => {
const settings = await db.settings.update(req.body);
res.json(settings);
}
);API6:2023 – Unrestricted Access to Sensitive Business Flows
The vulnerability
APIs that expose critical business flows (password reset, payment processing) without additional protections allow automation and exploitation.
Implementation pattern
typescript// Bot detection and challenge-response
import { createHash, randomBytes } from 'crypto';
class BotProtectionService {
private challenges = new Map<string, { answer: string; expires: Date }>();
generateChallenge(): string {
const challengeId = randomBytes(16).toString('hex');
const answer = Math.floor(Math.random() * 10).toString();
const expires = new Date(Date.now() + 5 * 60 * 1000); // 5 minutes
this.challenges.set(challengeId, { answer, expires });
return challengeId;
}
verifyChallenge(challengeId: string, userAnswer: string): boolean {
const challenge = this.challenges.get(challengeId);
if (!challenge || challenge.expires < new Date()) {
return false;
}
this.challenges.delete(challengeId);
return challenge.answer === userAnswer;
}
}
// Rate limiting for sensitive flows
class SensitiveFlowRateLimiter {
private attempts = new Map<string, { count: number; resetAt: Date }>();
checkLimit(identifier: string, maxAttempts: number, windowMs: number): boolean {
const record = this.attempts.get(identifier);
const now = new Date();
if (!record || record.resetAt < now) {
this.attempts.set(identifier, {
count: 1,
resetAt: new Date(now.getTime() + windowMs)
});
return true;
}
if (record.count >= maxAttempts) {
return false;
}
record.count++;
return true;
}
}
// Sensitive flow protection middleware
function protectSensitiveFlow(botProtection: BotProtectionService, rateLimiter: SensitiveFlowRateLimiter) {
return async (req: Request, res: Response, next: NextFunction) => {
const identifier = req.ip || req.socket.remoteAddress || 'unknown';
// Check rate limit
if (!rateLimiter.checkLimit(identifier, 3, 60 * 60 * 1000)) {
return res.status(429).json({
error: 'Too Many Attempts',
message: 'Maximum attempts exceeded. Please try again later.'
});
}
// Check for bot challenge
const challengeId = req.headers['x-challenge-id'];
const challengeAnswer = req.headers['x-challenge-answer'];
if (!challengeId || !challengeAnswer || !botProtection.verifyChallenge(challengeId, challengeAnswer.toString())) {
const newChallengeId = botProtection.generateChallenge();
return res.status(403).json({
error: 'Challenge Required',
message: 'Please complete the challenge to proceed',
challengeId: newChallengeId,
// In production, you'd return a puzzle instead of the answer
challenge: 'What is 2 + 2? Enter a single digit.'
});
}
next();
};
}
// Usage on sensitive endpoints
app.post('/api/password-reset',
protectSensitiveFlow(new BotProtectionService(), new SensitiveFlowRateLimiter()),
async (req, res) => {
// ... password reset logic
}
);API7:2023 – Server-Side Request Forgery (SSRF)
The vulnerability
APIs that fetch user-provided URLs can be exploited to access internal resources or perform attacks on behalf of the server.
Implementation pattern
typescriptimport { URL } from 'url';
import dns from 'dns';
import net from 'net';
class SSRFProtectionService {
private blockedDomains: Set<string>;
private blockedIPRanges: string[];
private allowedDomains: Set<string>;
constructor(config: {
blockedDomains?: string[];
blockedIPRanges?: string[];
allowedDomains?: string[];
}) {
this.blockedDomains = new Set(config.blockedDomains || [
'localhost',
'127.0.0.1',
'metadata.google.internal'
]);
this.blockedIPRanges = config.blockedIPRanges || [
'127.0.0.0/8', // Loopback
'10.0.0.0/8', // Private network
'172.16.0.0/12', // Private network
'192.168.0.0/16', // Private network
'169.254.0.0/16' // Link-local
];
this.allowedDomains = new Set(config.allowedDomains || []);
}
async validateURL(userURL: string): Promise<boolean> {
try {
const url = new URL(userURL);
// Only allow HTTP/HTTPS
if (!['http:', 'https:'].includes(url.protocol)) {
throw new Error('Invalid protocol');
}
// Check against blocked domains
if (this.blockedDomains.has(url.hostname)) {
throw new Error('Blocked domain');
}
// If allowlist is configured, check against it
if (this.allowedDomains.size > 0 && !this.allowedDomains.has(url.hostname)) {
throw new Error('Domain not in allowlist');
}
// Resolve hostname to IP and check against blocked ranges
const addresses = await this.resolveHostname(url.hostname);
for (const address of addresses) {
if (this.isIPBlocked(address)) {
throw new Error('Blocked IP address');
}
}
return true;
} catch (error) {
throw new Error(`Invalid URL: ${error.message}`);
}
}
private resolveHostname(hostname: string): Promise<string[]> {
return new Promise((resolve, reject) => {
dns.resolve(hostname, (err, addresses) => {
if (err) {
reject(err);
} else {
resolve(addresses);
}
});
});
}
private isIPBlocked(ip: string): boolean {
for (const range of this.blockedIPRanges) {
if (net.ipInRange(ip, range)) {
return true;
}
}
return false;
}
}
// SSRF protection middleware
function protectSSRF(ssrfProtection: SSRFProtectionService) {
return async (req: Request, res: Response, next: NextFunction) => {
if (req.body.url) {
try {
await ssrfProtection.validateURL(req.body.url);
} catch (error) {
return res.status(400).json({
error: 'Invalid URL',
message: error.message
});
}
}
next();
};
}
// Usage on endpoints that fetch URLs
app.post('/api/fetch-content',
authenticate,
protectSSRF(new SSRFProtectionService({
blockedDomains: ['localhost', 'metadata.google.internal'],
blockedIPRanges: ['127.0.0.0/8', '10.0.0.0/8'],
allowedDomains: ['example.com', 'trusted-cdn.com']
})),
async (req, res) => {
const { url } = req.body;
const response = await fetch(url);
const content = await response.text();
res.json({ content });
}
);API8:2023 – Security Misconfiguration
The vulnerability
Default configurations, incomplete configurations, and misconfigurations expose APIs to unnecessary risk.
Implementation pattern
typescript// Security headers middleware
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:']
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
},
noSniff: true,
xssFilter: true,
referrerPolicy: { policy: 'same-origin' }
}));
// CORS configuration
import cors from 'cors';
const corsOptions = {
origin: function (origin: string | undefined, callback: Function) {
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [];
// Allow requests with no origin (mobile apps, curl, etc.)
if (!origin) {
return callback(null, true);
}
if (allowedOrigins.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
};
app.use(cors(corsOptions));
// Security configuration validation
class SecurityConfigValidator {
private requiredEnvVars = [
'JWT_SECRET',
'DATABASE_URL',
'ALLOWED_ORIGINS'
];
private secureEnvVars = [
'JWT_SECRET',
'API_KEY',
'DATABASE_PASSWORD'
];
validate(): void {
// Check for required environment variables
const missingVars = this.requiredEnvVars.filter(
varName => !process.env[varName]
);
if (missingVars.length > 0) {
throw new Error(
`Missing required environment variables: ${missingVars.join(', ')}`
);
}
// Check for weak secrets in production
if (process.env.NODE_ENV === 'production') {
const weakSecrets = this.secureEnvVars.filter(varName => {
const value = process.env[varName];
return value && (value.length < 32 || value === 'secret' || value === 'password');
});
if (weakSecrets.length > 0) {
console.warn(
`WARNING: Weak secrets detected in production: ${weakSecrets.join(', ')}`
);
}
}
}
}
// Initialize and validate configuration
const configValidator = new SecurityConfigValidator();
try {
configValidator.validate();
} catch (error) {
console.error('Security configuration error:', error.message);
process.exit(1);
}API9:2023 – Improper Inventory Management
The vulnerability
APIs that expose older versions, documentation endpoints, or debug interfaces without proper controls increase attack surface.
Implementation pattern
typescript// API versioning with deprecation
class APIVersionManager {
private versions: Map<string, {
deprecated: boolean;
sunsetDate?: Date;
supported: boolean;
}> = new Map();
constructor() {
this.initializeVersions();
}
private initializeVersions(): void {
this.versions.set('v1', {
deprecated: true,
sunsetDate: new Date('2026-06-01'),
supported: false
});
this.versions.set('v2', {
deprecated: false,
supported: true
});
}
isVersionSupported(version: string): boolean {
const versionInfo = this.versions.get(version);
return versionInfo?.supported || false;
}
isVersionDeprecated(version: string): boolean {
const versionInfo = this.versions.get(version);
return versionInfo?.deprecated || false;
}
getVersionStatus(version: string): { deprecated: boolean; sunsetDate?: Date } {
const versionInfo = this.versions.get(version);
return {
deprecated: versionInfo?.deprecated || false,
sunsetDate: versionInfo?.sunsetDate
};
}
}
// Version checking middleware
function checkAPIVersion(versionManager: APIVersionManager) {
return (req: Request, res: Response, next: NextFunction) => {
const version = req.headers['x-api-version'] || 'v2';
if (!versionManager.isVersionSupported(version)) {
return res.status(410).json({
error: 'Version Unsupported',
message: `API version ${version} is no longer supported. Please upgrade to v2.`
});
}
const status = versionManager.getVersionStatus(version);
if (status.deprecated) {
res.setHeader('X-API-Deprecation', 'true');
res.setHeader('X-API-Sunset-Date', status.sunsetDate?.toISOString() || '');
res.setHeader(
'X-API-Upgrade-Path',
'/api/v2' + req.path
);
}
req.apiVersion = version;
next();
};
}
// Documentation endpoint with authentication
app.get('/api/docs',
authenticate,
requirePermission(new AuthorizationService(), 'docs', 'read'),
(req, res) => {
const docs = generateAPIDocumentation(req.apiVersion);
res.json(docs);
}
);
// Disable debug endpoints in production
if (process.env.NODE_ENV === 'production') {
console.log('Debug endpoints disabled in production');
} else {
app.get('/debug/routes', (req, res) => {
res.json(app._router.stack.map((layer: any) => ({
path: layer.route?.path,
methods: Object.keys(layer.route?.methods || {})
})));
});
}API10:2023 – Unsafe Consumption of APIs
The vulnerability
APIs that trust data from external APIs without proper validation can be exploited through compromised upstream services.
Implementation pattern
typescript// External API response validation
import { z } from 'zod';
// Define expected schema for external API response
const ExternalUserSchema = z.object({
id: z.string().uuid(),
name: z.string().max(100),
email: z.string().email(),
role: z.enum(['user', 'admin'])
});
class ExternalAPIService {
private cache = new Map<string, { data: any; expires: Date }>();
private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutes
async fetchUserData(userId: string): Promise<any> {
// Check cache first
const cached = this.cache.get(userId);
if (cached && cached.expires > new Date()) {
return cached.data;
}
try {
const response = await fetch(`${process.env.EXTERNAL_API_URL}/users/${userId}`, {
headers: {
'Authorization': `Bearer ${process.env.EXTERNAL_API_KEY}`,
'Content-Type': 'application/json'
},
timeout: 5000 // 5 second timeout
});
if (!response.ok) {
throw new Error(`External API error: ${response.status}`);
}
const data = await response.json();
// Validate response against schema
const validatedData = ExternalUserSchema.parse(data);
// Cache the validated data
this.cache.set(userId, {
data: validatedData,
expires: new Date(Date.now() + this.CACHE_TTL)
});
return validatedData;
} catch (error) {
if (error instanceof z.ZodError) {
console.error('External API response validation failed:', error.errors);
throw new Error('Invalid response from external API');
}
// Return cached data if available on error
const cached = this.cache.get(userId);
if (cached) {
console.warn('Using cached data due to external API error');
return cached.data;
}
throw error;
}
}
}
// Circuit breaker for external API
class CircuitBreaker {
private failures = 0;
private lastFailureTime: Date | null = null;
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
constructor(
private threshold: number = 5,
private timeout: number = 60000 // 1 minute
) {}
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === 'OPEN') {
if (this.shouldAttemptReset()) {
this.state = 'HALF_OPEN';
} else {
throw new Error('Circuit breaker is OPEN');
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess(): void {
this.failures = 0;
this.state = 'CLOSED';
}
private onFailure(): void {
this.failures++;
this.lastFailureTime = new Date();
if (this.failures >= this.threshold) {
this.state = 'OPEN';
}
}
private shouldAttemptReset(): boolean {
if (!this.lastFailureTime) {
return true;
}
const timeSinceLastFailure = Date.now() - this.lastFailureTime.getTime();
return timeSinceLastFailure > this.timeout;
}
}Monitoring and incident response
Security monitoring implementation
typescriptclass SecurityEventLogger {
async logSecurityEvent(event: {
type: string;
severity: 'low' | 'medium' | 'high' | 'critical';
userId?: string;
ipAddress: string;
details: any;
}): Promise<void> {
// Log to security monitoring system
await fetch(`${process.env.SECURITY_MONITORING_URL}/events`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...event,
timestamp: new Date().toISOString(),
environment: process.env.NODE_ENV
})
});
// For critical events, also alert immediately
if (event.severity === 'critical') {
await this.alertTeam(event);
}
}
private async alertTeam(event: any): Promise<void> {
// Send to PagerDuty, Slack, etc.
await fetch(`${process.env.ALERT_WEBHOOK_URL}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `CRITICAL SECURITY EVENT: ${event.type}`,
details: event.details
})
});
}
}
// Usage in authentication flow
const securityLogger = new SecurityEventLogger();
app.post('/api/auth/login',
authLimiter,
async (req, res) => {
try {
const { email, password } = req.body;
const user = await authService.authenticate(email, password);
res.json({ token: user.token });
} catch (error) {
await securityLogger.logSecurityEvent({
type: 'AUTHENTICATION_FAILURE',
severity: 'medium',
ipAddress: req.ip,
details: {
email: req.body.email,
userAgent: req.headers['user-agent']
}
});
res.status(401).json({
error: 'Authentication failed'
});
}
}
);Conclusion
API security in 2026 requires defense in depth: multiple layers of protection that address the OWASP API Security Top 10 comprehensively. The patterns in this guide provide a foundation, but security is an ongoing process of monitoring, updating, and improving based on emerging threats.
Strategic question for your team: What's your API security maturity level, and what's the gap between your current state and production-ready security practices?
Need to harden your API security posture and implement OWASP Top 10 protections? Talk to Imperialis about API security assessment, implementation, and ongoing security operations.
Sources
- OWASP API Security Top 10 — Official OWASP API Security project
- OWASP API Security Top 10 Explained — Detailed explanations and examples
- NIST API Security Guidelines — NIST guidelines on API security