Security is consistently treated as a concern to address after the product is built, and that sequencing is responsible for the overwhelming majority of application security incidents. Retrofitting security controls into an insecure architecture is expensive, disruptive, and incomplete. Building security in from the start is cheaper and more effective — but it requires knowing what to build in.
The OWASP Top 10 is the most widely cited reference for web application security vulnerabilities. Updated in 2021, it represents the most critical and prevalent security risks to web applications, as identified by security professionals across the industry. This guide covers each category with practical implementation guidance.
A01: Broken Access Control
Access control failures — where users can access data or functionality they are not authorised to access — moved to the top position in 2021 and remain the most widespread vulnerability category.
The failure modes are numerous: missing authorisation checks on API endpoints, insecure direct object references (accessing another user's record by changing an ID in the URL), privilege escalation (accessing admin functionality as a regular user), and CORS misconfiguration exposing APIs to unintended origins.
What to build:
Authorisation must be enforced server-side on every request. Client-side checks (hiding a button in the UI) are not security controls — they are UX. The backend must validate that the requesting identity has permission to perform the requested action on the requested resource, every time.
For multi-tenant systems, every database query that returns tenant-scoped data must include a `tenant_id` filter. The database-level Row Level Security approach — described in our multi-tenancy architecture guide — is the strongest implementation because it enforces the constraint at the data layer.
Deny by default: access should be denied unless explicitly granted. A missing permission check is a vulnerability; a redundant permission check is not.
A02: Cryptographic Failures
Previously called "Sensitive Data Exposure," this category covers failures in protecting data in transit and at rest — from transmitting data over unencrypted connections to storing passwords in plaintext or using weak hashing algorithms.
What to build:
- TLS 1.2 or 1.3 on all connections. No HTTP. HSTS headers to prevent downgrade attacks.
- Passwords must be hashed with a slow, purpose-built algorithm: bcrypt, scrypt, or Argon2. Never MD5, SHA-1, or unsalted SHA-256 for passwords.
- Sensitive fields in the database (payment card data, government ID numbers, health data) should be encrypted at rest using AES-256. The encryption keys should not live in the same database as the encrypted data.
- Secrets (API keys, database credentials) must never appear in source code, logs, or error messages. Use environment variables and a secrets manager (AWS Secrets Manager, HashiCorp Vault, Vercel environment variables).
A03: Injection
Injection attacks — where attacker-controlled input is interpreted as code — remain in the top three. SQL injection is the most common, but command injection, LDAP injection, and NoSQL injection follow the same pattern.
A SQL injection vulnerability allows an attacker to query, modify, or delete any data in your database — including data belonging to other users. In the worst cases, it allows full server takeover.
What to build:
Parameterised queries (prepared statements) eliminate SQL injection at the source. Never concatenate user input into SQL strings:
// VULNERABLE — never do this
const query = `SELECT * FROM users WHERE email = '${userInput}'`;
// SAFE — parameterised query
const result = await db.query(
"SELECT * FROM users WHERE email = $1",
[userInput]
);ORMs (Prisma, Drizzle, TypeORM) use parameterised queries by default, which makes SQL injection significantly harder to introduce accidentally. Raw query escape hatches in ORMs should be treated with the same caution as direct SQL.
For command execution: never pass user input to shell commands. If shell execution is required, use argument arrays (not string interpolation) and validate inputs against a strict allowlist before use.
A04: Insecure Design
Insecure design is a new category in 2021 — it refers to missing or ineffective security controls at the architecture level, rather than implementation bugs. A system can have no implementation vulnerabilities and still be insecurely designed.
Examples: business logic that allows a user to complete a multi-step process out of order (checkout without payment), rate limiting absent from authentication endpoints, password reset flows that leak information about whether an email address is registered.
What to build:
Threat modelling during the design phase — systematically asking "what could an attacker do here?" for each user-facing feature. The STRIDE model (Spoofing, Tampering, Repudiation, Information Disclosure, Denial of Service, Elevation of Privilege) provides a systematic framework.
Security requirements should be defined alongside functional requirements. If a feature handles sensitive data, the security controls for that data should be specified before implementation begins.
A05: Security Misconfiguration
Misconfiguration is the broadest category: default credentials left in place, unnecessary features enabled, verbose error messages exposing stack traces and internal paths, missing security headers, cloud storage buckets left publicly accessible.
What to build:
Security headers on every HTTP response:
Strict-Transport-Security: max-age=31536000; includeSubDomains
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{random}'
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()Error handling must never expose internal details in production responses. Log the full error server-side; return a generic message to the client.
Configuration review as part of the deployment pipeline: automated scanning of infrastructure configuration (Checkov, Trivy) catches common misconfigurations before they reach production.
A06: Vulnerable and Outdated Components
Using libraries with known CVEs is one of the most common sources of critical vulnerabilities. The Log4Shell vulnerability in 2021 — a critical RCE in an extremely widely used Java logging library — affected hundreds of thousands of systems.
What to build:
Automated dependency scanning integrated into CI/CD: `npm audit`, Snyk, or Dependabot catch known CVEs before deployment. Configure alerts for new vulnerabilities in existing dependencies.
Keep dependencies updated as a maintenance discipline. Packages that have not been updated in months accumulate vulnerability exposure. Regular dependency updates (monthly at minimum) keep the remediation cost small.
Do not use abandoned packages. A library with no maintenance activity in 2+ years and no active maintainers will not receive security patches. Plan a migration path before the dependency becomes a liability.
A07: Identification and Authentication Failures
Authentication vulnerabilities include weak passwords, credential stuffing (reusing breach credentials), brute force attacks, insecure session management, and missing multi-factor authentication on privileged accounts.
What to build:
Rate limiting and account lockout on authentication endpoints. After N failed attempts (typically 5–10) from the same IP or for the same account, require a delay, a CAPTCHA, or a lockout period. Without this, credential stuffing attacks are trivially effective.
Multi-factor authentication for all administrative accounts — and ideally for all user accounts. TOTP (time-based one-time password) authenticator apps are the minimum; hardware security keys (FIDO2/WebAuthn) are stronger.
Session tokens must be cryptographically random, sufficiently long (128 bits minimum), invalidated on logout, and regenerated after privilege changes (login, role change). Session fixation attacks exploit tokens that do not change across authentication state changes.
Password requirements should enforce length (minimum 12 characters) rather than complexity rules, and check against known breach databases (HaveIBeenPwned API) rather than requiring special characters that users work around predictably.
A08: Software and Data Integrity Failures
This category covers failures to verify the integrity of software and data — including insecure CI/CD pipelines, auto-update mechanisms without signature verification, and deserialisation of untrusted data.
What to build:
Verify integrity of third-party packages: use lockfiles (`package-lock.json`, `yarn.lock`) that pin exact versions and checksums. Do not use version ranges (`^1.0.0`) in production dependency specifications — they allow unexpected updates.
CI/CD pipeline security: restrict who can modify pipeline definitions, use separate credentials for deployment with minimal necessary permissions, and audit pipeline changes.
Never deserialise untrusted data from external sources without validation. If your application accepts serialised objects from users or external systems, validate and sanitise before deserialisation.
A09: Security Logging and Monitoring Failures
Without sufficient logging, security incidents cannot be detected in time to respond, and forensic investigation after an incident is impossible.
What to build:
Log authentication events: successful logins, failed login attempts, password changes, MFA events. Log authorisation failures (access denied events). Log all administrative actions with the identity of the actor.
Structured logs with consistent fields: timestamp, event type, actor identity, resource affected, outcome, and a request trace ID. Structured logs enable alerting rules and automated analysis.
Alert on anomalous patterns: multiple failed logins from one IP, access to sensitive resources outside normal hours, bulk data export events, new admin account creation. These are signals of active attacks that require human review.
Logs must not contain sensitive data: no passwords, no full credit card numbers, no session tokens, no PII that does not have a legitimate audit requirement.
A10: Server-Side Request Forgery (SSRF)
SSRF vulnerabilities allow an attacker to cause the server to make HTTP requests to arbitrary destinations — including internal services, cloud metadata endpoints, and other systems not accessible from the public internet.
The most famous SSRF attack is against the AWS instance metadata service at 169.254.169.254, which returns IAM credentials for the EC2 instance — effectively granting the attacker the permissions of the application's AWS role.
What to build:
Validate and sanitise any URL that the server will fetch. If your application fetches user-supplied URLs, implement:
- Allowlist of permitted domains or IP ranges
- Block requests to private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, 169.254.0.0/16)
- Resolve DNS before making the request and check the resolved IP against the blocklist (DNS rebinding attacks bypass domain-level checks)
On the infrastructure level: IMDSv2 (Instance Metadata Service v2) requires a session token for AWS metadata access, which makes SSRF-based credential theft significantly harder. Enable it on all EC2 instances.
Building a Security Culture
The OWASP Top 10 is a checklist, not a security programme. The organisations with the best security posture are those that have embedded security thinking into their development process, not those that audit for a list of vulnerabilities before shipping.
The practices that make the difference:
Security requirements alongside functional requirements. For every feature that handles sensitive data or privileged actions, specify the security controls required before implementation.
Developer security training. Engineers who understand why SQL injection works write code that prevents it naturally. One-time training is insufficient; security knowledge should be reinforced in code review and design discussions.
Security-oriented code review. Code review checklists should include security-relevant checks for the type of change: does this endpoint have authorisation? Is this input validated? Are secrets handled correctly?
Penetration testing. Before launching a product that handles sensitive data, engage a professional penetration test. Internal review catches a different class of problems than external testing by specialists who are actively trying to find vulnerabilities.
Security is not a feature you add to software — it is a property of how the software is built. The cost of addressing a security vulnerability in design is roughly 1x. In development, 6x. In production, 100x. The investment pays for itself many times over.