Introduction
The OWASP Top 10 represents the most critical web application security risks as identified by the Open Web Application Security Project. Updated periodically based on real-world vulnerability data, the 2021 edition introduced new categories like Insecure Design and Software and Data Integrity Failures while reshuffling existing risks based on their prevalence and impact. Understanding and mitigating every item on this list is the baseline expectation for any production web application.
Injection attacks alone accounted for 94% of applications tested in OWASP's dataset. Broken access control overtook injection as the #1 risk in 2021, reflecting the widespread failure to properly enforce authorization on every API endpoint and data access path. These aren't theoretical risks—every item on the OWASP Top 10 has been exploited in real breaches affecting millions of users.
This guide covers each OWASP Top 10 category with specific, actionable prevention strategies, code examples, and architectural patterns that eliminate these vulnerabilities at their root.
Understanding OWASP Top 10: Core Concepts
A01: Broken Access Control
Broken access control occurs when users can act outside their intended permissions. This includes accessing other users' data, modifying records they don't own, escalating privileges, and accessing admin functions without authorization. The 2021 edition elevated this to #1 because it appeared in 94% of tested applications.
A02: Cryptographic Failures
Previously "Sensitive Data Exposure," this category focuses on failures in cryptography that lead to data exposure. Weak algorithms (MD5, SHA-1), hardcoded keys, missing encryption at rest, and improper certificate validation are common manifestations. The shift in naming emphasizes that the root cause is cryptographic failure, not just the symptom of exposed data.
A03: Injection
Injection flaws occur when untrusted data is sent to an interpreter as part of a command or query. SQL injection, NoSQL injection, OS command injection, and LDAP injection all follow the same pattern: user input is concatenated into executable strings without proper sanitization or parameterization.
A04: Insecure Design
New in 2021, insecure design represents architectural flaws that cannot be fixed by implementation alone. Missing rate limiting, lack of fraud detection, insufficient abuse case modeling, and failure to separate tenants at the design level all constitute insecure design.
A05: Security Misconfiguration
Default credentials, unnecessary features enabled, overly permissive CORS, missing security headers, and verbose error messages are common misconfigurations. Cloud infrastructure adds complexity with misconfigured S3 buckets, permissive IAM policies, and exposed management interfaces.
Architecture and Design Patterns
Component 1: Authorization Layer
A centralized authorization layer enforces access control decisions at the API boundary. Every request is evaluated against a policy engine (OPA, Casbin, or custom) that checks subject, resource, action, and context. Authorization logic is never implemented inline within business logic.
Component 2: Input Validation Pipeline
An input validation pipeline sanitizes all external data before it enters the application. Schema validation (Zod, Joi, JSON Schema) enforces type safety and value constraints. Parameterized queries eliminate SQL injection, and output encoding prevents XSS.
Component 3: Cryptographic Service
A cryptographic service centralizes encryption, hashing, and key management. It enforces algorithm standards (AES-256-GCM, bcrypt, Argon2), manages key rotation, and provides audit logging for all cryptographic operations.
Component 4: Security Configuration Management
Infrastructure-as-code (Terraform, Pulumi) and configuration templates ensure consistent security settings across environments. Automated security scanning (Checkov, tfsec) catches misconfigurations before deployment.
Step-by-Step Implementation
Preventing Broken Access Control (A01)
import { z } from 'zod';
// Centralized authorization middleware
function authorize(requiredPermission: string) {
return async (req: Request, res: Response, next: NextFunction) => {
const user = req.user;
if (!user) {
return res.status(401).json({ error: 'Authentication required' });
}
// Check permission against policy engine
const allowed = await policyEngine.evaluate({
subject: user.id,
action: requiredPermission,
resource: req.path,
context: {
method: req.method,
params: req.params,
ip: req.ip,
},
});
if (!allowed) {
await logSecurityEvent({
type: 'authorization_failure',
userId: user.id,
path: req.path,
permission: requiredPermission,
});
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
// Object-level authorization check
async function getUserProfile(requesterId: string, targetUserId: string) {
// Always verify the requester can access this specific resource
if (requesterId !== targetUserId) {
const hasPermission = await policyEngine.evaluate({
subject: requesterId,
action: 'read:user_profile',
resource: `user:${targetUserId}`,
});
if (!hasPermission) throw new ForbiddenError();
}
return db.user.findUnique({ where: { id: targetUserId } });
}Preventing SQL Injection (A03)
// VULNERABLE: String concatenation
const query = `SELECT * FROM users WHERE email = '${email}'`; // NEVER DO THIS
// SECURE: Parameterized queries with Prisma
const user = await prisma.user.findUnique({
where: { email: email },
});
// SECURE: Parameterized queries with raw SQL
const user = await db.query(
'SELECT * FROM users WHERE email = $1',
[email]
);
// SECURE: Input validation with Zod
const UserQuerySchema = z.object({
email: z.string().email().max(255),
id: z.string().uuid().optional(),
});
async function getUser(rawInput: unknown) {
const input = UserQuerySchema.parse(rawInput); // Throws on invalid input
return db.user.findUnique({ where: { email: input.email } });
}Preventing XSS (A03/A05)
import DOMPurify from 'isomorphic-dompurify';
// Server-side: HTML sanitization
function sanitizeUserHTML(html: string): string {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li'],
ALLOWED_ATTR: ['href', 'title', 'class'],
ALLOW_DATA_ATTR: false,
});
}
// Client-side: React auto-escaping (safe by default)
function UserContent({ content }: { content: string }) {
// React auto-escapes this — safe
return <div>{content}</div>;
}
// Client-side: dangerouslySetInnerHTML with sanitization
function RichContent({ html }: { html: string }) {
const clean = DOMPurify.sanitize(html);
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
// Prevent stored XSS: sanitize on input AND output
app.post('/api/comments', async (req, res) => {
const { body } = CommentSchema.parse(req.body);
const sanitized = sanitizeUserHTML(body);
await db.comment.create({ data: { body: sanitized, userId: req.user.id } });
res.json({ success: true });
});Preventing CSRF (A01)
import csrf from 'csurf';
// CSRF protection middleware
const csrfProtection = csrf({
cookie: {
httpOnly: true,
secure: true,
sameSite: 'strict',
},
});
// Apply to all state-changing routes
app.use('/api', csrfProtection);
// Client-side: Include CSRF token in requests
app.get('/csrf-token', csrfProtection, (req, res) => {
res.json({ token: req.csrfToken() });
});
// Frontend: Include token in form submissions and AJAX requests
const csrfToken = await fetch('/csrf-token').then((r) => r.json());
await fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'CSRF-Token': csrfToken.token,
},
body: JSON.stringify({ amount: 100, to: 'account2' }),
});Security Misconfiguration Prevention (A05)
// Security configuration checklist
const securityConfig = {
// Remove default credentials
admin: {
username: process.env.ADMIN_USERNAME, // Never use 'admin'
password: process.env.ADMIN_PASSWORD, // Never use 'password'
},
// CORS: Restrict to known origins
cors: {
origin: ['https://yourdomain.com', 'https://app.yourdomain.com'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
},
// Error handling: Don't leak internals
errorHandler: (err: Error, req: Request, res: Response) => {
console.error(err); // Log full error server-side
res.status(500).json({ error: 'Internal server error' }); // Generic response
},
// Disable unnecessary features
helmet: {
contentSecurityPolicy: true,
crossOriginEmbedderPolicy: true,
crossOriginOpenerPolicy: true,
crossOriginResourcePolicy: true,
hidePoweredBy: true,
hsts: { maxAge: 63072000, includeSubDomains: true, preload: true },
noSniff: true,
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
xssFilter: false, // Disabled in favor of CSP
},
};Real-World Use Cases and Case Studies
Use Case 1: Broken Access Control in SaaS Platform
A SaaS platform discovered that API endpoints returned data across tenant boundaries. The /api/v1/invoices/{id} endpoint accepted any invoice ID without verifying the requesting user's tenant ownership. Fix: implement row-level security in the database layer, enforcing WHERE tenant_id = current_tenant() on every query.
Use Case 2: SQL Injection in Legacy Application
A legacy PHP application used string concatenation for all database queries. An attacker exploited a search parameter to extract the entire user database including password hashes. Remediation: migrated to PDO with prepared statements, added WAF rules as temporary mitigation, and implemented automated SQL injection testing in CI/CD.
Use Case 3: Security Misconfiguration Exposing Admin Panel
A Kubernetes deployment exposed the admin dashboard to the internet with default credentials. An attacker gained cluster-admin access and deployed cryptocurrency miners. Fix: disabled dashboard access outside the VPN, implemented RBAC with least-privilege roles, and added CIS Kubernetes benchmark scanning to the deployment pipeline.
Best Practices for Production
-
Deny by default: Every access control decision should default to deny. Only explicitly granted permissions allow access. This prevents new endpoints from being accidentally exposed.
-
Use parameterized queries exclusively: Never concatenate user input into SQL, NoSQL, or OS commands. ORMs like Prisma and TypeORM handle this automatically; for raw queries, always use parameter placeholders.
-
Validate on input, encode on output: Validate all input with strict schemas (Zod, JSON Schema). Encode all output based on context: HTML encoding for web pages, JavaScript encoding for inline scripts, URL encoding for URLs.
-
Encrypt sensitive data at rest and in transit: Use AES-256-GCM for encryption, bcrypt/Argon2 for password hashing. Never store encryption keys with encrypted data. Use envelope encryption with a KMS.
-
Implement security logging and monitoring: Log all authentication events, authorization failures, input validation failures, and configuration changes. Alert on anomalous patterns. Maintain audit trails for compliance.
-
Keep dependencies updated: Run
npm auditin CI/CD pipelines. Subscribe to security advisories for critical dependencies. Use Dependabot or Renovate for automated update PRs. -
Conduct threat modeling during design: Use STRIDE or PASTA frameworks to identify threats before implementation. Document trust boundaries, data flows, and attack surfaces. Design mitigations into the architecture.
-
Deploy WAF as defense-in-depth: A Web Application Firewall (WAF) provides an additional layer of protection against known attack patterns. AWS WAF, Cloudflare WAF, and ModSecurity offer pre-built rulesets for OWASP Top 10.
Common Pitfalls and Solutions
| Pitfall | Impact | Solution |
|---|---|---|
| IDOR (Insecure Direct Object Reference) | Users access other users' data | Verify resource ownership in every endpoint |
| Client-side-only authorization | Easily bypassed with API tools | Always enforce authorization server-side |
| Using MD5/SHA-1 for passwords | Passwords cracked in seconds with rainbow tables | Use bcrypt or Argon2 with proper cost factors |
| Verbose error messages | Internal architecture exposed to attackers | Log detailed errors server-side, return generic messages |
| Disabled security features in dev left in prod | Missing protection in production | Environment-aware security configuration |
| Missing rate limiting | Brute force and credential stuffing attacks | Implement rate limiting per IP and per user |
Performance Optimization
Security controls add measurable overhead. Parameterized queries may require query plan caching. Encryption adds CPU cycles. Authorization checks require database lookups. Optimize by caching authorization decisions (with short TTLs), using connection pooling for database queries, and offloading encryption to hardware acceleration when available.
// Authorization decision caching
const authCache = new LRUCache<string, boolean>({
max: 10000,
ttl: 60 * 1000, // 1 minute
});
async function checkPermissionCached(
userId: string,
permission: string,
resource: string
): Promise<boolean> {
const key = `${userId}:${permission}:${resource}`;
const cached = authCache.get(key);
if (cached !== undefined) return cached;
const result = await policyEngine.evaluate({ subject: userId, action: permission, resource });
authCache.set(key, result);
return result;
}Comparison with Alternatives
| OWASP Category | Manual Code Review | SAST Tools | DAST Tools | RASP |
|---|---|---|---|---|
| Broken Access Control | Medium detection | Low | Medium | High |
| Injection | High detection | High | High | High |
| XSS | Medium detection | High | High | High |
| Insecure Design | High (requires expertise) | Low | Low | N/A |
| Misconfiguration | Low | Medium | Medium | N/A |
| Speed | Very slow | Fast | Medium | Real-time |
| False Positive Rate | Low | High | Medium | Low |
A comprehensive security program combines all four approaches: SAST in CI/CD, DAST against staging, RASP in production, and periodic manual code review for architectural flaws.
Advanced Patterns and Techniques
Row-Level Security in PostgreSQL
-- Enable RLS on the users table
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
-- Users can only see their own data
CREATE POLICY user_isolation ON users
FOR ALL
USING (tenant_id = current_setting('app.current_tenant')::uuid);
-- Set tenant context per request
CREATE FUNCTION set_tenant(tenant_id uuid) RETURNS void AS $$
BEGIN
PERFORM set_config('app.current_tenant', tenant_id::text, true);
END;
$$ LANGUAGE plpgsql;Input Validation Pipeline
import { z } from 'zod';
import { createMiddleware } from 'hono/factory';
function validateInput<T extends z.ZodSchema>(schema: T) {
return createMiddleware(async (c, next) => {
const result = schema.safeParse(await c.req.json());
if (!result.success) {
return c.json({ error: 'Validation failed', details: result.error.issues }, 400);
}
c.set('validatedInput', result.data);
await next();
});
}
// Usage
const CreateUserSchema = z.object({
email: z.string().email().max(255),
name: z.string().min(1).max(100).regex(/^[a-zA-Z\s]+$/),
role: z.enum(['user', 'admin']).default('user'),
});
app.post('/api/users', validateInput(CreateUserSchema), async (c) => {
const input = c.get('validatedInput');
// Input is guaranteed valid and typed
const user = await createUser(input);
return c.json(user);
});Testing Strategies
OWASP Top 10 vulnerabilities should be tested at every level. Unit tests verify input validation and authorization logic. Integration tests verify parameterized queries and output encoding. End-to-end tests verify that security headers are present, CSRF protection works, and access control is enforced across the full request lifecycle.
import { test, expect } from '@playwright/test';
test('prevents SQL injection in search', async ({ request }) => {
const response = await request.get('/api/search?q=1%27%20OR%201%3D1--');
expect(response.status()).toBe(400);
const body = await response.json();
expect(body.error).toContain('Invalid input');
});
test('prevents unauthorized access to other users data', async ({ request }) => {
// Login as user A
const loginResponse = await request.post('/api/login', {
data: { email: 'usera@test.com', password: 'password' },
});
const { token } = await loginResponse.json();
// Try to access user B's profile
const response = await request.get('/api/users/user-b-id/profile', {
headers: { Authorization: `Bearer ${token}` },
});
expect(response.status()).toBe(403);
});Future Outlook
The OWASP Top 10 continues evolving with the threat landscape. AI-specific vulnerabilities (prompt injection, model poisoning) may appear in future editions. Supply chain security, API security, and LLM security are emerging categories that OWASP is actively researching through dedicated projects.
OWASP Top 10 Testing Checklist
Test each OWASP Top 10 category systematically. For injection vulnerabilities, use parameterized queries and test all user inputs with SQL injection payloads. For broken authentication, test session management, password policies, and multi-factor authentication. For XSS, test all user-generated content rendering with script injection payloads. Use automated scanners like OWASP ZAP or Burp Suite for initial testing, then follow up with manual testing for business logic vulnerabilities that automated tools cannot detect.
Security Headers Configuration
Implement security headers as a defense-in-depth strategy alongside OWASP protections. Set X-Content-Type-Options: nosniff to prevent MIME type sniffing. Configure X-Frame-Options: DENY or use CSP frame-ancestors to prevent clickjacking. Add Strict-Transport-Security with a long max-age to enforce HTTPS. Set Referrer-Policy: strict-origin-when-cross-origin to control information leakage. Use Permissions-Policy to disable unnecessary browser features like camera, microphone, and geolocation for your application.
Conclusion
The OWASP Top 10 provides a prioritized framework for addressing the most critical web application security risks. Prevention requires architectural commitment: centralized authorization, parameterized queries, input validation pipelines, cryptographic best practices, and continuous security testing. Treat the OWASP Top 10 as a minimum security baseline, not a comprehensive security program—real-world applications require additional controls for business logic abuse, API security, and emerging AI-specific threats. Start with broken access control and injection prevention, as these two categories alone account for the majority of real-world breaches.