What is the OWASP Top 10?
The OWASP Top 10 is a standard awareness document published by the Open Web Application Security Project (OWASP). It represents a broad consensus about the most critical security risks to web applications. The list is updated periodically based on data from hundreds of organizations and reflects the real-world prevalence and impact of different vulnerability categories.
Understanding the OWASP Top 10 is essential for every web developer. These are not theoretical risks — they represent the actual vulnerabilities that attackers exploit most frequently. Many compliance frameworks (PCI DSS, SOC 2, HIPAA) reference the OWASP Top 10 as a minimum standard for web application security.
The OWASP Top 10 Categories
- Broken Access Control — Restrictions on authenticated users are not properly enforced
- Cryptographic Failures — Failures related to cryptography leading to data exposure
- Injection — SQL, NoSQL, OS, and LDAP injection via untrusted data
- Insecure Design — Missing or ineffective security controls in the design phase
- Security Misconfiguration — Insecure default configs, missing hardening
- Vulnerable and Outdated Components — Using components with known vulnerabilities
- Identification and Authentication Failures — Broken authentication mechanisms
- Software and Data Integrity Failures — Code and infrastructure without integrity verification
- Security Logging and Monitoring Failures — Insufficient logging and detection
- Server-Side Request Forgery (SSRF) — Application fetches remote resources without validation
A01: Broken Access Control
Broken Access Control is the number one risk. It occurs when users can act outside their intended permissions. This includes accessing other users' data by changing an ID in the URL (IDOR), elevating privileges from regular user to admin, or accessing API endpoints without proper authorization checks.
// VULNERABLE: Insecure Direct Object Reference (IDOR)
app.get("/api/invoices/:id", async (req, res) => {
// Anyone can access any invoice by guessing IDs!
const invoice = await db.query("SELECT * FROM invoices WHERE id = $1", [req.params.id]);
res.json(invoice.rows[0]);
});
// SAFE: Verify ownership before returning data
app.get("/api/invoices/:id", authMiddleware, async (req, res) => {
const invoice = await db.query(
"SELECT * FROM invoices WHERE id = $1 AND user_id = $2",
[req.params.id, req.user.id] // Filter by authenticated user
);
if (!invoice.rows.length) {
return res.status(404).json({ error: "Invoice not found" });
}
res.json(invoice.rows[0]);
});
// Authorization middleware for role-based access
function authorize(...allowedRoles: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: "Authentication required" });
}
if (!allowedRoles.includes(req.user.role)) {
// Log the authorization failure
logger.warn({
event: "AUTHORIZATION_FAILURE",
userId: req.user.id,
userRole: req.user.role,
requiredRoles: allowedRoles,
resource: req.url,
});
return res.status(403).json({ error: "Insufficient permissions" });
}
next();
};
}
app.delete("/api/users/:id", authorize("admin"), deleteUserHandler);
A02: Cryptographic Failures
Cryptographic failures (previously called "Sensitive Data Exposure") occur when sensitive data is not properly protected. This includes transmitting data in cleartext, using weak cryptographic algorithms, using default or weak keys, not enforcing encryption, and caching sensitive data.
// Protecting sensitive data
// WRONG: Storing sensitive data in plain text
const userData = {
ssn: "123-45-6789", // Never store in plain text!
creditCard: "4111-1111-1111-1111",
};
// RIGHT: Encrypt sensitive fields before storage
import { SymmetricEncryption } from "./encryption";
class SensitiveDataStore {
private encryption: SymmetricEncryption;
private encryptionKey: Buffer;
constructor() {
this.encryption = new SymmetricEncryption();
// Key should come from a KMS (AWS KMS, HashiCorp Vault, etc.)
this.encryptionKey = Buffer.from(process.env.DATA_ENCRYPTION_KEY!, "base64");
}
async storeSensitiveData(userId: string, field: string, value: string) {
const encrypted = this.encryption.encrypt(value, this.encryptionKey);
await db.query(
`INSERT INTO sensitive_data (user_id, field_name, encrypted_value, iv, auth_tag)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (user_id, field_name) DO UPDATE SET
encrypted_value = $3, iv = $4, auth_tag = $5`,
[userId, field, encrypted.ciphertext, encrypted.iv, encrypted.authTag]
);
}
async retrieveSensitiveData(userId: string, field: string): Promise<string> {
const result = await db.query(
"SELECT encrypted_value, iv, auth_tag FROM sensitive_data WHERE user_id = $1 AND field_name = $2",
[userId, field]
);
if (!result.rows.length) throw new Error("Data not found");
const { encrypted_value, iv, auth_tag } = result.rows[0];
return this.encryption.decrypt(encrypted_value, this.encryptionKey, iv, auth_tag);
}
}
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 are all variants. The primary defense is parameterized queries and input validation.
A05: Security Misconfiguration
Security misconfiguration is one of the most common issues. It includes missing security headers, verbose error messages that leak information, default credentials, unnecessary features enabled, and permissive CORS policies.
// Security hardening checklist as middleware
import helmet from "helmet";
// Helmet sets many security headers automatically
app.use(helmet());
// Additional hardening
app.use((req, res, next) => {
// Remove server identification
res.removeHeader("X-Powered-By");
// Prevent MIME type sniffing
res.setHeader("X-Content-Type-Options", "nosniff");
// Prevent clickjacking
res.setHeader("X-Frame-Options", "DENY");
// Control referrer information
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
// Permissions policy
res.setHeader(
"Permissions-Policy",
"camera=(), microphone=(), geolocation=(), payment=(self)"
);
next();
});
// Disable detailed errors in production
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
if (process.env.NODE_ENV === "production") {
// Generic error in production
res.status(500).json({ error: "Internal server error" });
} else {
// Detailed error in development only
res.status(500).json({
error: err.message,
stack: err.stack,
});
}
});
A08: Software and Data Integrity Failures
This category covers assumptions made about software updates, critical data, and CI/CD pipelines without verifying integrity. Subresource Integrity (SRI) for CDN-hosted scripts, verifying package checksums, and securing your build pipeline all fall under this category.
// Subresource Integrity (SRI) for external scripts
// When loading scripts from CDNs, include integrity hashes
const sriScriptTag = `
<script
src="https://cdn.example.com/library.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8w"
crossorigin="anonymous"
></script>
`;
// Verify webhook payload integrity
function verifyGitHubWebhook(
payload: string,
signature: string,
secret: string
): boolean {
const expectedSignature = `sha256=${
crypto.createHmac("sha256", secret).update(payload).digest("hex")
}`;
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Secure deserialization
function safeJsonParse<T>(input: string, schema: z.ZodSchema<T>): T {
let parsed: unknown;
try {
parsed = JSON.parse(input);
} catch {
throw new Error("Invalid JSON");
}
// Validate structure after parsing
const result = schema.safeParse(parsed);
if (!result.success) {
throw new Error(`Validation failed: ${result.error.message}`);
}
return result.data;
}
A10: Server-Side Request Forgery (SSRF)
SSRF occurs when a web application fetches a remote resource based on a user-supplied URL without proper validation. An attacker can abuse this to access internal services, read metadata from cloud providers (like AWS instance metadata at 169.254.169.254), or port-scan internal networks.
import { URL } from "url";
import dns from "dns/promises";
import net from "net";
// SSRF prevention: validate URLs before fetching
async function safeFetch(userUrl: string): Promise<Response> {
// Parse and validate the URL
let parsed: URL;
try {
parsed = new URL(userUrl);
} catch {
throw new SecurityError("Invalid URL");
}
// Only allow HTTPS
if (parsed.protocol !== "https:") {
throw new SecurityError("Only HTTPS URLs are allowed");
}
// Block internal/private IP ranges
const addresses = await dns.resolve4(parsed.hostname);
for (const addr of addresses) {
if (isPrivateIP(addr)) {
throw new SecurityError("Access to internal resources is forbidden");
}
}
// Fetch with timeout and size limit
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000);
try {
const response = await fetch(userUrl, {
signal: controller.signal,
redirect: "error", // Don't follow redirects (could redirect to internal)
});
return response;
} finally {
clearTimeout(timeout);
}
}
function isPrivateIP(ip: string): boolean {
const parts = ip.split(".").map(Number);
return (
parts[0] === 10 ||
parts[0] === 127 ||
(parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) ||
(parts[0] === 192 && parts[1] === 168) ||
(parts[0] === 169 && parts[1] === 254) || // AWS metadata
ip === "0.0.0.0"
);
}
Security Warning: Apply All OWASP Defenses
- Defense in depth: Do not rely on a single defense. Apply multiple layers of security controls for each OWASP category.
- Stay updated: The OWASP Top 10 evolves. New categories like SSRF were added recently. Keep your knowledge current.
- Automate testing: Use SAST, DAST, and dependency scanning tools in your CI/CD pipeline to catch OWASP vulnerabilities automatically.
- Shift left: Address security in the design phase, not just in testing. Insecure Design (A04) reminds us that some vulnerabilities cannot be fixed with code alone.