Published
- 32 min read
Integrating OWASP Top 10 into Your Development Workflow
How to Write, Ship, and Maintain Code Without Shipping Vulnerabilities
A hands-on security guide for developers and IT professionals who ship real software. Build, deploy, and maintain secure systems without slowing down or drowning in theory.
Buy the book now
Practical Digital Survival for Whistleblowers, Journalists, and Activists
A practical guide to digital anonymity for people who can’t afford to be identified. Designed for whistleblowers, journalists, and activists operating under real-world risk.
Buy the book now
The Digital Fortress: How to Stay Safe Online
A simple, no-jargon guide to protecting your digital life from everyday threats. Learn how to secure your accounts, devices, and privacy with practical steps anyone can follow.
Buy the book nowIntroduction
The OWASP Top 10 is a comprehensive guide to the most critical security risks in web applications. Published by the Open Web Application Security Project (OWASP), it serves as a reference point for developers, security professionals, and organizations aiming to build secure systems.
By integrating the OWASP Top 10 into your development workflow, you can proactively address vulnerabilities, ensure compliance with security standards, and build trust with users. This article provides actionable insights into each OWASP Top 10 category and offers practical strategies for incorporating these best practices into your projects.
Why the OWASP Top 10 Matters
The OWASP Top 10 is widely recognized as a gold standard for web application security. It highlights common vulnerabilities that attackers exploit and provides guidelines for prevention.
Key Benefits of Integration:
- Proactive Security:
- Address vulnerabilities early in the development lifecycle.
- Enhanced Compliance:
- Meet regulatory and industry standards.
- Cost Efficiency:
- Fixing issues during development is cheaper than post-deployment remediation.
- User Trust:
- Deliver secure applications that inspire confidence.
Overview of the OWASP Top 10
Here’s a brief overview of the latest OWASP Top 10 categories:
- Broken Access Control:
- Unauthorized access to data or functionality.
- Cryptographic Failures:
- Weak or missing encryption.
- Injection:
- Manipulation of queries or commands via untrusted input.
- Insecure Design:
- Poorly designed systems that enable attacks.
- Security Misconfiguration:
- Default settings or incomplete configurations.
- Vulnerable and Outdated Components:
- Use of libraries with known vulnerabilities.
- Identification and Authentication Failures:
- Weak or mismanaged authentication systems.
- Software and Data Integrity Failures:
- Lack of mechanisms to ensure integrity.
- Security Logging and Monitoring Failures:
- Insufficient logging and detection.
- Server-Side Request Forgery (SSRF):
- Exploitation of server-side fetches.
How to Integrate OWASP Top 10 into Your Workflow
1. Implement Secure Coding Practices
Start by embedding secure coding principles into your development practices. This includes:
- Validating and sanitizing all user inputs to prevent injection attacks.
- Using parameterized queries or prepared statements.
- Encrypting sensitive data at rest and in transit.
Example (SQL Injection Prevention):
query = "SELECT * FROM users WHERE id = ?"
db.execute(query, (user_id,))
2. Automate Vulnerability Scanning
Use automated tools to identify vulnerabilities throughout the development lifecycle.
Tools to Consider:
- OWASP ZAP: For dynamic application security testing.
- Snyk: For dependency scanning and vulnerability detection.
- SonarQube: For static code analysis.
3. Conduct Regular Code Reviews
Code reviews are an effective way to identify and address security flaws. Create checklists aligned with OWASP Top 10 categories to ensure thorough evaluations.
Checklist Example:
- Are all input fields validated?
- Is sensitive data encrypted?
- Are authentication mechanisms robust?
4. Use Secure Frameworks and Libraries
Choose frameworks and libraries that adhere to security best practices and receive regular updates.
Example (Python):
- Use Django for its built-in CSRF protection and secure authentication mechanisms.
5. Integrate Security into CI/CD Pipelines
Incorporate security checks into your continuous integration and deployment pipelines to catch vulnerabilities early.
Example (GitHub Actions):
jobs:
security_scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run OWASP Dependency-Check
run: dependency-check.sh --project MyProject
6. Train Your Team
Educate your team on OWASP Top 10 risks and secure coding practices through workshops, online courses, or internal training sessions.
7. Prioritize Vulnerability Management
Not all vulnerabilities are equal. Use a risk-based approach to prioritize fixes based on impact and likelihood.
Addressing Specific OWASP Categories
Broken Access Control
Best Practices:
- Implement role-based access control (RBAC).
- Enforce the principle of least privilege.
Example (Node.js):
if (user.role !== 'admin') {
return res.status(403).send('Access Denied')
}
Cryptographic Failures
Best Practices:
- Use strong encryption algorithms (e.g., AES-256).
- Avoid hardcoding secrets; use secure storage solutions like AWS Secrets Manager.
Injection
Best Practices:
- Validate and sanitize all inputs.
- Use prepared statements for database queries.
Security Misconfiguration
Best Practices:
- Disable unnecessary features and endpoints.
- Regularly update and patch software.
Testing for OWASP Compliance
Testing ensures that your application adheres to OWASP Top 10 guidelines. Here are some strategies:
Manual Testing
- Simulate attacks to identify vulnerabilities, such as injection or XSS.
Automated Testing
- Use tools like OWASP ZAP and Burp Suite for comprehensive scans.
Penetration Testing
- Employ security professionals to identify advanced vulnerabilities.
OWASP Testing Tools by Category
Choosing the right tool for the right vulnerability class is one of the highest-leverage decisions in a security program. Running every scanner against every codebase wastes time and generates noise; mapping specific tools to specific OWASP categories produces focused, actionable findings.
The table below provides a practical reference organized by OWASP Top 10 category across four testing disciplines: Static Application Security Testing (SAST), Dynamic Application Security Testing (DAST), Software Composition Analysis (SCA), and manual testing techniques.
| OWASP Category | SAST | DAST | SCA / Supply Chain | Manual Techniques |
|---|---|---|---|---|
| A01 Broken Access Control | Semgrep, SonarQube | OWASP ZAP, Burp Suite | — | IDOR enumeration, AuthMatrix |
| A02 Cryptographic Failures | Bandit, FindSecBugs | testssl.sh, SSLyze | — | TLS config review, key storage audit |
| A03 Injection | Semgrep, CodeQL, Bandit | SQLMap, OWASP ZAP | — | Manual query review, input fuzzing |
| A04 Insecure Design | Manual ASVS review | — | — | STRIDE threat modeling, abuse cases |
| A05 Security Misconfiguration | Checkov, Trivy | Nikto, OWASP ZAP | — | Manual config audit, header inspection |
| A06 Vulnerable Components | OWASP Dependency-Check | — | Snyk, Dependabot, Socket.dev | CVE database review, SBOM analysis |
| A07 Auth Failures | Semgrep auth rules | Burp Intruder, jwt_tool | — | Session analysis, token lifecycle review |
| A08 Integrity Failures | OWASP Dependency-Track | — | Sigstore, Cosign, Syft | CI/CD pipeline review, build artifact audit |
| A09 Logging Failures | Semgrep log rules | — | — | Log review, SIEM query simulation |
| A10 SSRF | Semgrep SSRF rules | SSRFmap, Interactsh | — | URL parameter analysis, network egress test |
Integrating Tools at the Right Stage
The most common mistake teams make is treating security scanning as a once-per-sprint activity. The goal is to shift testing as far left as possible — catching issues at the moment of introduction rather than weeks later in a security review. Each stage of the pipeline serves a different purpose.
At the commit stage, lightweight static analysis tools like Semgrep run in pre-commit hooks within a second or two. They catch obvious patterns — hardcoded secrets, SQL string concatenation, use of dangerous functions — before they ever reach the repository. The tool runs locally and gives the developer immediate, contextual feedback that is far more effective than a report generated days later.
At the pull request stage, deeper SAST tools like CodeQL perform dataflow analysis to find vulnerabilities that require tracking how data travels across multiple function calls. Dependency scanning tools like Snyk or OWASP Dependency-Check evaluate every dependency — including transitive dependencies — against known CVE databases. These tools should be configured as required status checks that block merges on high-severity findings.
In the staging environment, DAST tools like OWASP ZAP actively probe a running application from the outside, discovering issues that static analysis cannot: race conditions, authentication bypasses that depend on runtime state, and server-side request forgery paths that are only reachable when the application is executing.
In production, runtime monitoring tools like Falco (for containers) or AWS GuardDuty observe the application’s actual behavior and alert on anomalies that indicate active exploitation. This is the last line of defense — by this stage, you want to detect compromise in minutes, not months.
OWASP 2021 vs 2017: What Changed
The OWASP Top 10 is revised every three to four years, driven by analysis of real-world vulnerability data contributed by hundreds of organizations worldwide. Understanding the shifts between the 2017 and 2021 editions is important for two reasons: it reveals which risk categories have grown more dangerous over time, and it identifies entirely new threat patterns that teams may not have previously addressed.
| Rank | OWASP 2017 | OWASP 2021 | Change |
|---|---|---|---|
| 1 | Injection | Broken Access Control | Moved up from #5; found in 94% of tested apps |
| 2 | Broken Authentication | Cryptographic Failures | Renamed from Sensitive Data Exposure |
| 3 | Sensitive Data Exposure | Injection | Moved down; XSS now merged in |
| 4 | XML External Entities (XXE) | Insecure Design | Brand new category in 2021 |
| 5 | Broken Access Control | Security Misconfiguration | XXE merged in; expanded scope |
| 6 | Security Misconfiguration | Vulnerable and Outdated Components | Moved up significantly |
| 7 | Cross-Site Scripting (XSS) | Identification and Authentication Failures | Renamed; XSS moved into A03 |
| 8 | Insecure Deserialization | Software and Data Integrity Failures | Renamed and expanded to supply chain |
| 9 | Using Components with Known Vulnerabilities | Security Logging and Monitoring Failures | Elevated; MTTD data drove this |
| 10 | Insufficient Logging and Monitoring | Server-Side Request Forgery (SSRF) | New; community-nominated |
Why Broken Access Control Moved to the Top
In 2017, Broken Access Control sat at position five. By 2021 it had become the number one risk. OWASP analyzed data from over 500,000 applications and found access control failures present in 94 percent of tested systems. This is a staggering prevalence rate. The underlying reason is that access control is genuinely hard to implement correctly at scale: as applications add features, new endpoints are created, and it is easy to forget to apply authorization checks consistently everywhere. An injection vulnerability in one SQL query is a discrete bug; broken access control is often a systemic failure that affects dozens or hundreds of endpoints.
The Significance of Insecure Design
Insecure Design is the most philosophically distinct addition in the 2021 edition. Every other OWASP Top 10 category can, in principle, be addressed by fixing bugs in the implementation. Insecure Design cannot. A system designed without proper rate limiting, without tenant isolation, or without separation of duties between roles cannot be made secure simply by patching code. It requires rethinking the architecture. This is why OWASP introduced this category — to drive security practices earlier in the software development lifecycle, into the requirements and design phases, before a single line of code is written.
SSRF and the Cloud-Native Reality
Server-Side Request Forgery was community-nominated, and its inclusion reflects the rise of cloud-native architectures. In cloud environments like AWS, GCP, and Azure, every virtual machine can reach an internal metadata endpoint that returns configuration data, credentials, and IAM role tokens. When an attacker can cause a server to make an arbitrary outbound HTTP request, they can reach this metadata endpoint and steal credentials that grant access to the entire cloud account. This was not a meaningful risk in traditional on-premise deployments, but in a world where most production applications run on public cloud infrastructure, SSRF is one of the most impactful vulnerabilities an attacker can exploit.
Deep Dive: The Complete OWASP Top 10
The overview section introduced each category at a high level. This section provides the depth needed to actually defend against each one: understanding the mechanics of the vulnerability, recognizing vulnerable code patterns, implementing secure alternatives, and selecting the right testing tools.
A01: Broken Access Control
Access control enforces policy about what authenticated users are permitted to do. Broken access control occurs when those policies are absent, inconsistently applied, or bypassable. Unlike authentication failures — where the attacker has no valid identity — access control failures affect authenticated users who can access data or perform actions that belong to other accounts or require higher privileges.
The most common manifestation is Insecure Direct Object Reference, where a unique identifier in a URL or request parameter maps directly to a database record. If the application does not verify that the requesting user owns or has permission to access that specific record, any authenticated user can enumerate identifiers to access others’ data. Consider an API endpoint like /api/invoices/1042 — without an ownership check, incrementing or decrementing the invoice ID exposes other users’ financial records.
Horizontal privilege escalation (accessing another user’s data) is distinct from vertical privilege escalation (accessing admin functionality), but both stem from the same root cause: missing authorization checks. An effective defense requires deny-by-default posture — no resource is accessible unless there is an explicit rule granting access — combined with server-side enforcement on every request.
// Vulnerable: no ownership check
app.get('/api/invoices/:id', authenticate, async (req, res) => {
const invoice = await Invoice.findById(req.params.id)
res.json(invoice) // Any authenticated user can access any invoice
})
// Secure: verify resource ownership before returning data
app.get('/api/invoices/:id', authenticate, async (req, res) => {
const invoice = await Invoice.findById(req.params.id)
if (!invoice || invoice.userId.toString() !== req.user.id.toString()) {
return res.status(403).json({ error: 'Forbidden' })
}
res.json(invoice)
})
Log every access control failure. A small number of 403 responses is normal; a rapid sequence of them from the same IP address is a strong signal of enumeration or active exploitation.
Recommended testing tools: Burp Suite with AuthMatrix extension for systematic authorization matrix testing, OWASP ZAP for automated crawling and access control probing, Semgrep with OWASP rules for static detection of missing authorization middleware.
A02: Cryptographic Failures
This category was renamed from “Sensitive Data Exposure” in 2021 to focus attention on the root cause rather than the symptom. Data is exposed because cryptography is absent, weak, or misapplied. The consequences range from exposed passwords in a database breach to intercepted payment data in transit.
The most widespread implementation error is password storage. Storing passwords as plaintext or with fast hash algorithms like MD5 and SHA-1 means that anyone who obtains the database has immediate access to all user passwords. Modern password hashing algorithms — Argon2id, bcrypt, and scrypt — are designed to be computationally expensive, meaning that even if the hash database is stolen, cracking each password requires significant time and resources.
A second common failure is transmitting sensitive data over plain HTTP. HTTPS is not optional for any application handling authenticated sessions or personal data. Equally important is enforcing HTTPS through HTTP Strict Transport Security headers, which instruct browsers to always upgrade connections and reject invalid certificates permanently, providing protection against SSL stripping attacks.
Hardcoded credentials and API keys in source code represent another critical failure in this category. Even private repositories carry risk: historical commits are never automatically scrubbed, developers may clone repositories to personal devices, and brief periods of accidental public exposure can be harvested by automated scanners.
# Vulnerable: MD5 for password storage
import hashlib
stored_hash = hashlib.md5(password.encode()).hexdigest()
# MD5 is broken; rainbow tables make this trivially reversible
# Secure: Argon2id via argon2-cffi
from argon2 import PasswordHasher
ph = PasswordHasher(time_cost=2, memory_cost=65536, parallelism=2)
hashed = ph.hash(password)
# Verify and rehash on login if parameters have changed
def check_password(stored_hash, input_password):
try:
ph.verify(stored_hash, input_password)
if ph.check_needs_rehash(stored_hash):
return True, ph.hash(input_password) # Return new hash
return True, None
except Exception:
return False, None
Recommended testing tools: SSLyze and testssl.sh for TLS configuration audits, GitLeaks and TruffleHog for scanning Git history for hardcoded secrets, Bandit for Python static analysis of cryptographic misuse, FindSecBugs for Java projects.
A03: Injection
Injection vulnerabilities occur when untrusted data is sent to an interpreter as part of a command, query, or expression. In the 2021 edition, OWASP merged Cross-Site Scripting into this category, recognizing XSS as a form of injection — client-side script injection into HTML output rather than server-side query injection. This consolidation is conceptually correct: the defense is the same in both cases: treat all external data as untrusted and never interpolate it directly into executable contexts.
SQL injection remains the most common form. It occurs when user-supplied values are concatenated directly into SQL query strings, allowing an attacker to alter the query’s structure. An input like ' OR '1'='1 terminates the intended string literal and appends a condition that evaluates as always true. Parameterized queries solve this completely by keeping data and code strictly separated at the database driver level — there is no query string manipulation possible.
NoSQL databases are not immune. MongoDB queries can be manipulated by passing query operator objects in JSON input. If a login endpoint accepts {"username": {"$ne": null}, "password": {"$ne": null}} and the application passes this directly to a query without validation, it matches every user in the database and bypasses authentication entirely.
// Vulnerable SQL (Node.js)
const query = `SELECT * FROM users WHERE email = '${req.body.email}' AND password = '${hash}'`
db.query(query)
// Secure: parameterized query
db.query('SELECT * FROM users WHERE email = ? AND password = ?', [req.body.email, hash])
// Vulnerable NoSQL (MongoDB)
const user = await User.findOne({ email: req.body.email })
// req.body.email could be { "$ne": null }, matching any user
// Secure: validate type before querying
const email = typeof req.body.email === 'string' ? req.body.email.trim() : null
if (!email) return res.status(400).json({ error: 'Invalid email' })
const user = await User.findOne({ email })
For XSS prevention, Content Security Policy headers provide a powerful second layer of defense. Even if a sanitization step fails, a properly configured CSP prevents injected scripts from executing.
Recommended testing tools: SQLMap for automated SQL injection detection on authorized targets, OWASP ZAP active scanner, Semgrep with injection rule packs, Bandit (Python), CodeQL for inter-procedural dataflow analysis.
A04: Insecure Design
Insecure Design is the only category in the OWASP Top 10 that cannot be resolved with a code fix alone. Patches address implementation errors; insecure design represents fundamental architectural flaws that require reconsidering how the system is structured. The critical implication for developers is that security must be considered from the very beginning of a project, not added as an afterthought during or after implementation.
Threat modeling is the primary tool for preventing insecure design. The STRIDE framework provides a structured way to enumerate potential threats during architecture review. For each component and data flow in a system, teams ask whether it is susceptible to Spoofing of identity, Tampering with data, Repudiation of actions, Information Disclosure, Denial of Service, or Elevation of Privilege. For each identified threat, a mitigation is designed and assigned to a specific component.
| STRIDE Threat | Description | Example |
|---|---|---|
| Spoofing | Impersonating another user or service | Forging a session token or JWT |
| Tampering | Modifying data in transit or at rest | Altering a transaction amount in a request |
| Repudiation | Denying that an action was performed | Disputing a financial transaction without audit trail |
| Information Disclosure | Exposing data to unauthorized parties | Verbose error messages revealing schema details |
| Denial of Service | Disrupting availability | Flooding a login endpoint without rate limiting |
| Elevation of Privilege | Gaining unauthorized permissions | Using IDOR to access admin-only resources |
Beyond threat modeling, insecure design manifests as business logic flaws that security scanners miss because they require understanding intent. A cart that allows negative-quantity items, a password reset flow that uses guessable tokens, or a multi-tenant system that stores tenant data in the same database table without row-level security policies are all design-level failures.
Recommended testing tools: OWASP Threat Dragon and Microsoft Threat Modeling Tool for structured threat modeling, OWASP ASVS Level 2 checklist for architecture review, manual code review guided by business logic understanding.
A05: Security Misconfiguration
Security misconfiguration is the most widespread vulnerability category in practice, observed in approximately 90 percent of applications tested. It encompasses every layer of the technology stack: web servers, application frameworks, databases, cloud IAM policies, container runtimes, and network infrastructure. The common thread is that default settings, unnecessary enabled features, or missing hardening steps create exploitable exposure.
Default credentials are a persistent problem. Administrative consoles shipped with vendor defaults — a username of “admin” and a password of “admin” — are actively scanned for by automated bots within minutes of a new deployment going online. Unnecessary services and endpoints provide additional attack surface with no corresponding business value. A /actuator/env endpoint in a Spring Boot application, if left exposed in production, can return environment variables including secrets injected at runtime.
Overly permissive CORS policies represent another common misconfiguration. Setting Access-Control-Allow-Origin: * on endpoints that require authentication effectively nullifies CSRF protections and allows any third-party website to make authenticated requests using the visitor’s credentials.
// Vulnerable Express.js CORS configuration
app.use(cors({ origin: '*' })) // Allows any origin
// Secure: explicit allowlist
const allowedOrigins = ['https://app.mycompany.com', 'https://admin.mycompany.com']
app.use(
cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true)
} else {
callback(new Error('Not allowed by CORS policy'))
}
},
credentials: true
})
)
When deploying containers, never run application processes as root inside the container. The principle of least privilege applies here too: a compromised process running as root inside a container can escalate to host-level access through kernel vulnerabilities or misconfigured volume mounts.
Recommended testing tools: OWASP ZAP and Nikto for web server misconfiguration detection, Checkov for scanning Terraform and Kubernetes manifests in CI, Trivy for container image misconfiguration and vulnerability scanning, Lynis for Linux system hardening audits.
A06: Vulnerable and Outdated Components
Modern applications are built on foundations of third-party code — NPM packages, Python libraries, Maven dependencies, base container images. This is efficient and productive, but it also means your application’s security posture is a function of every component’s security posture, including components you have never audited and may not even know you are using.
The Log4Shell vulnerability, disclosed in December 2021, illustrated this risk at global scale. A single vulnerability in the Java logging library log4j2 had a CVSS score of 10.0 — the maximum — and enabled unauthenticated remote code execution on any server running the affected version. It affected millions of servers across virtually every industry. Many organizations did not even know they were running the vulnerable library because it was a transitive dependency, included not by their own direct requirement but by another library they depended on.
Transitive dependencies are the deeper challenge. Running npm audit or pip-audit tells you about your direct dependencies’ vulnerabilities, but the chain goes deeper. A library you depend on may depend on another library with a critical CVE. Tools like OWASP Dependency-Check and Snyk traverse the full dependency tree and evaluate every node against vulnerability databases.
# GitHub Actions: automated dependency scanning on every push
name: Dependency Security Scan
on: [push, pull_request]
jobs:
dependency_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run OWASP Dependency-Check
uses: dependency-check/Dependency-Check_Action@main
with:
project: 'my-application'
path: '.'
format: 'HTML'
args: '--failOnCVSS 7 --enableRetired'
- name: Upload report artifact
uses: actions/upload-artifact@v4
with:
name: dependency-check-report
path: reports/
Alongside automated scanning, establish a policy for how quickly vulnerabilities of different severities must be remediated: critical CVEs fixed within 24–48 hours, high severity within one week, medium severity within 30 days. Without explicit SLAs, security debt accumulates indefinitely.
Recommended testing tools: OWASP Dependency-Check, Snyk, npm audit, pip-audit, cargo audit, Socket.dev (supply chain risk analysis for JavaScript), Dependabot (automated pull requests for dependency updates).
A07: Identification and Authentication Failures
Authentication establishes who is making a request. Failures in this category allow attackers to impersonate other users, bypass authentication entirely, or exploit sessions that should have expired. The consequences are typically high-impact: administrative takeover, account compromise, and data theft.
Credential stuffing is among the most common attacks: attackers obtain username/password lists from previous data breaches and systematically try them against new targets. Because users reuse passwords across sites, even a breach of an unrelated service can be the vector for account compromise. Effective defenses include multi-factor authentication, account lockout policies combined with CAPTCHA challenges after a threshold of failed attempts, and detection of credential stuffing via anomaly analysis on login IP diversity.
JSON Web Tokens require careful implementation. A common mistake is accepting the alg: none header value, which disables signature verification entirely. Another is using a weak or predictable signing secret that can be brute-forced offline once an attacker obtains a valid token. JWTs should always have a short expiry time, and refresh token rotation should be implemented to detect token theft.
// Vulnerable JWT handling
const jwt = require('jsonwebtoken')
const token = jwt.sign({ userId }, 'secret') // Weak key, no expiry
const decoded = jwt.verify(token, 'secret', { algorithms: ['HS256', 'none'] }) // accepts alg:none
// Secure JWT implementation
const token = jwt.sign(
{ userId, iat: Math.floor(Date.now() / 1000) },
process.env.JWT_SECRET, // Strong, randomly generated 256-bit key
{ algorithm: 'HS256', expiresIn: '15m' }
)
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256'] // Explicit algorithm allowlist, never 'none'
})
Session identifiers must be rotated on privilege change. When a user authenticates or elevates their privilege level, issue a new session ID and invalidate the old one to prevent session fixation attacks, where an attacker forces a known session ID on a victim before they authenticate.
Recommended testing tools: jwt_tool for JWT security assessment, Burp Suite Intruder for credential stuffing simulation in authorized tests, OWASP ASVS Chapter 2 as the definitive authentication security checklist.
A08: Software and Data Integrity Failures
Software and Data Integrity Failures is a broad category covering situations where an application — or its build and deployment pipeline — does not verify the integrity of code or data before trusting it. Insecure deserialization was the previous entry that evolved into this category; the 2021 version expanded the scope to encompass supply chain attacks on CI/CD infrastructure, unsigned updates, and use of CDN-hosted assets without integrity verification.
The SolarWinds attack demonstrated the catastrophic potential of CI/CD pipeline compromise. Malicious code was injected into the build process itself, meaning digitally signed binaries were distributed to customers containing an undetected backdoor for months. Protecting the integrity of the build pipeline is now a first-class security concern.
For client-side assets loaded from CDNs, Subresource Integrity attributes instruct browsers to verify a cryptographic hash of the fetched resource before executing it. If the CDN is compromised or the file is tampered with, the hash will not match and the browser will refuse to execute the script.
<!-- Vulnerable: no integrity verification -->
<script src="https://cdn.example.com/framework.min.js"></script>
<!-- Secure: SRI hash prevents execution if file is tampered with -->
<script
src="https://cdn.example.com/framework.min.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"
></script>
For server-side deserialization, the safest approach is to use data-only serialization formats (JSON, Protocol Buffers) with strict schema validation rather than allowing arbitrary object deserialization. Python’s pickle, Java’s native deserialization, PHP’s unserialize(), and Ruby’s Marshal.load all execute arbitrary code during deserialization and must never be used with untrusted input.
Recommended testing tools: OWASP Dependency-Track for tracking SBOM and vulnerability alerts, Syft for generating SBOMs, Cosign/Sigstore for container image signing and verification, Grype for vulnerability scanning against SBOMs.
A09: Security Logging and Monitoring Failures
An application that does not generate adequate security-relevant logs, or that generates them but never alerts on anomalies, is effectively blind to active attacks. According to the Verizon Data Breach Investigations Report, the median time from the start of a breach to its detection is measured in days; in many cases, breaches are first discovered by external parties rather than the victim organization’s own monitoring teams. Adequate logging and monitoring collapses this detection window.
Security logging must be structured (machine-parseable JSON, not free-text messages), tamper-evident (forwarded to a write-once store or centralized SIEM in real time), and comprehensive across a defined set of security-relevant event types. Equally important: logs must never contain sensitive data. Password hashes, raw credentials, session tokens, payment card numbers, and personally identifiable information must be explicitly excluded from all log message templates.
// Security event logging with Winston
const winston = require('winston')
const securityLogger = winston.createLogger({
level: 'warn',
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
transports: [new winston.transports.File({ filename: 'logs/security.log' })]
})
function logSecurityEvent(event, req, details = {}) {
securityLogger.warn({
event,
ip: req.ip,
userAgent: req.get('user-agent'),
path: req.originalUrl,
method: req.method,
timestamp: new Date().toISOString(),
...details
})
}
// Log access control failure
logSecurityEvent('ACCESS_DENIED', req, { userId: req.user?.id, resource: req.params.id })
// Log authentication failure (never include the attempted password)
logSecurityEvent('AUTH_FAILURE', req, {
username: req.body.username,
reason: 'invalid_credentials'
})
A minimal list of events that must always be logged: all authentication events (success and failure, including the reason for failure), all access control denials, all input validation failures on security-relevant fields, changes to user roles or permissions, and all administrative actions. These events feed anomaly detection rules — for example, alerting when more than ten authentication failures occur from the same IP within five minutes, or when an account performs an unusual geographic login after a long period of inactivity.
| Log This | Never Log This |
|---|---|
| Login success and failure with timestamp and IP | Passwords or password hashes |
| Role changes and permission grants | Session tokens or JWT values |
| Access control denials | Credit card or payment numbers |
| Input validation failures on security fields | Social security or national ID numbers |
| High-value transaction events | Private keys or API secrets |
| API rate limit threshold crossings | Full request bodies containing PII |
Recommended testing tools: Elastic SIEM (free, open source ELK-based), Splunk, Graylog, AWS CloudTrail combined with GuardDuty for cloud-native environments, Falco for Kubernetes runtime monitoring.
A10: Server-Side Request Forgery (SSRF)
Server-Side Request Forgery vulnerabilities arise when an application fetches a remote resource based on a URL supplied by the user or application configuration, without validating that the destination is an intended and safe target. The server makes the request using its own network identity and credentials — with access to internal services, cloud metadata endpoints, and other resources that are inaccessible to the external attacker directly.
In cloud environments, this is particularly dangerous. AWS exposes an instance metadata endpoint at the link-local address 169.254.169.254 that is accessible from inside every EC2 instance. Making a request to this endpoint from the application server returns the IAM role credentials associated with the instance, including temporary access keys that grant permissions to the cloud account. This makes SSRF a direct path to cloud account compromise. Google Cloud Platform uses metadata.google.internal for the same purpose; Azure uses 169.254.169.254 as well.
# Vulnerable: fetching user-supplied URL without validation
import requests
def fetch_preview(url):
response = requests.get(url, timeout=5) # url could be http://169.254.169.254/
return response.text
# Secure: allowlist-based URL validation
from urllib.parse import urlparse
import ipaddress
ALLOWED_HOSTS = {'api.partner.com', 'cdn.mycompany.com'}
PRIVATE_RANGES = [
ipaddress.ip_network('10.0.0.0/8'),
ipaddress.ip_network('172.16.0.0/12'),
ipaddress.ip_network('192.168.0.0/16'),
ipaddress.ip_network('127.0.0.0/8'),
ipaddress.ip_network('169.254.0.0/16'), # Link-local / metadata
]
def is_safe_url(url_string):
try:
parsed = urlparse(url_string)
if parsed.scheme != 'https':
return False
hostname = parsed.hostname
if hostname not in ALLOWED_HOSTS:
return False
try:
ip = ipaddress.ip_address(hostname)
for net in PRIVATE_RANGES:
if ip in net:
return False
except ValueError:
pass # hostname, not IP address literal
return True
except Exception:
return False
Beyond application-level validation, enforce network egress controls. A web application server should generally not need to reach arbitrary external hosts — security group rules, Kubernetes NetworkPolicy resources, or firewall rules that permit only outbound connections to known necessary hosts add a defense-in-depth layer that contains the blast radius of an SSRF vulnerability even if the application-level check is bypassed.
Recommended testing tools: SSRFmap for automated SSRF exploitation on authorized targets, Interactsh for out-of-band detection (detects blind SSRF), OWASP ZAP SSRF scanner, Semgrep with SSRF detection rules.
Securing Your CI/CD Pipeline Step by Step
Shifting security left is not simply about adding a scanner to the pipeline — it is about designing a pipeline where security gates are non-negotiable, meaningful, and fast enough that developers experience them as helpful rather than obstructions. A pipeline that catches 95 percent of issues but blocks for 30 minutes is counterproductive; one that catches 70 percent of issues and returns results in two minutes will be used consistently and actually improve security posture over time.
The following diagram illustrates a complete DevSecOps CI/CD workflow integrating OWASP Top 10 controls at every stage.
flowchart LR
A[Developer\nLocal] -->|pre-commit\nhooks| B[Commit Push]
B --> C[Pull Request CI]
C --> D[SAST\nSemgrep CodeQL]
C --> E[Secrets Scan\nGitLeaks]
C --> F[SCA\nSnyk OWASP DC]
D --> G{Gates\nPassed?}
E --> G
F --> G
G -->|Fail| H[Block Merge\nCreate Ticket]
G -->|Pass| I[Merge to Main]
I --> J[Build Container]
J --> K[Image Scan\nTrivy Grype]
K --> L[Deploy Staging]
L --> M[DAST\nOWASP ZAP]
M --> N{DAST\nClear?}
N -->|Fail| O[Security Ticket\nHold Release]
N -->|Pass| P[Deploy Production]
P --> Q[Runtime Monitor\nFalco GuardDuty]
Stage 1 — Pre-Commit Hooks
Install the pre-commit framework and configure hooks that run before every commit. These should complete in under five seconds to avoid developer friction.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/Yelp/detect-secrets
rev: v1.4.0
hooks:
- id: detect-secrets
args: ['--baseline', '.secrets.baseline']
- repo: https://github.com/returntocorp/semgrep
rev: v1.50.0
hooks:
- id: semgrep
args: ['--config', 'p/owasp-top-ten', '--error']
Stage 2 — Pull Request Security Gates
Define required status checks in your GitHub branch protection rules. The following GitHub Actions workflow runs SAST, secrets scanning, and dependency scanning as parallel jobs that must all pass before a merge is permitted.
name: Security Gates
on: [pull_request]
jobs:
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: returntocorp/semgrep-action@v1
with:
config: p/owasp-top-ten
env:
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
dependency-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Snyk vulnerability check
uses: snyk/actions/node@master
with:
args: --severity-threshold=high
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
Stage 3 — Container Scanning and DAST
After a successful merge and container build, scan the container image for OS-level and application-level vulnerabilities, then run OWASP ZAP against the staging environment.
image-scan:
runs-on: ubuntu-latest
steps:
- name: Scan container image with Trivy
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myregistry/${{ github.repository }}:latest'
format: 'table'
exit-code: '1'
severity: 'CRITICAL,HIGH'
dast:
needs: [deploy-staging]
runs-on: ubuntu-latest
steps:
- name: OWASP ZAP Full Scan
uses: zaproxy/[email protected]
with:
target: 'https://staging.myapp.example.com'
Each stage feeds results into the team’s issue tracker, creating security tickets at the appropriate severity level. Critical findings block deployment. High findings create P1 tickets requiring resolution within 48 hours. This transforms security findings from report artifacts into actionable work items tracked through the same processes as any other engineering work.
Common Mistakes and Anti-Patterns
Understanding which security mistakes appear most frequently in real codebases helps teams focus review attention and build more targeted training programs. The following anti-patterns are persistent across industries and technology stacks.
Relying on Client-Side Validation Alone
JavaScript validation in the browser is a convenience for users, not a security control. Any constraint enforced only in JavaScript can be bypassed by disabling the script, using the browser’s developer tools to modify values, or sending crafted HTTP requests directly to the API endpoint using a proxy tool like Burp Suite. Every security-relevant constraint — field length limits, allowed value ranges, minimum password strength, role-based feature access — must be re-validated on the server.
This mistake is particularly common in single-page applications where form validation logic tends to be rich and sophisticated on the frontend, giving developers false confidence that the server is similarly protected.
Trusting Input from HTTP Request Bodies for Authorization
A closely related mistake is using values from the request body or query string to determine which permissions a user should have for a given operation. Role or permission values in request parameters can be forged. Authorization decisions must always derive from the authenticated session, not from request-supplied claims.
// Anti-pattern: role from request body
app.post('/api/transfer', authenticate, async (req, res) => {
const { amount, toAccount, userRole } = req.body // userRole is forgeable
if (userRole !== 'premium') return res.status(403).send('Upgrade required')
await transfer(req.user.id, toAccount, amount)
})
// Correct: role from authenticated session
app.post('/api/transfer', authenticate, async (req, res) => {
const { amount, toAccount } = req.body
if (req.user.role !== 'premium') return res.status(403).send('Upgrade required')
await transfer(req.user.id, toAccount, amount)
})
Using eval() or Dynamic Code Execution with User Input
Calling eval(), Python’s exec(), PHP’s eval(), or JavaScript’s Function() constructor with any part of user-supplied data creates direct Remote Code Execution vulnerabilities. There is virtually no legitimate use case that requires executing user-supplied code, and the risk is complete system compromise.
Storing Secrets in Source Code or Versioned Configuration Files
Secrets stored in source code — even in private repositories — are at permanent risk. Git history is immutable; a secret that is committed and then “removed” in a later commit still exists in the repository history and can be retrieved. Automated scanning tools like GitLeaks actively scan public repositories for API key patterns and have harvested millions of valid credentials from GitHub repositories.
The correct pattern is to store secrets outside the codebase entirely, retrieving them from a secrets manager like HashiCorp Vault or a cloud provider’s native service at application startup using short-lived credentials with minimal permissions.
Generating Permissive Error Responses
Returning stack traces, database error messages, or internal file paths in API error responses is an information disclosure that assists attackers in reconnaissance. A stack trace reveals the application’s technology stack, framework versions, internal directory structure, and occasionally business logic logic details. All of these serve as inputs to targeted attacks.
Production error handling must log the full error server-side and return only a generic, user-facing message with a correlation ID that allows support teams to look up the full error without exposing it to the client.
Ignoring Transitive Dependency Vulnerabilities
Declaring only five direct dependencies and seeing a clean dependency audit does not mean the application is free of vulnerable components. Each of those five dependencies may pull in dozens of sub-dependencies. The total dependency tree of a modern Node.js application typically contains hundreds of packages, most of which the development team has never examined. Always scan the full dependency tree and generate a Software Bill of Materials so you know exactly what your application contains.
Skipping Security Testing in the Final Days Before a Release
Deadline pressure frequently causes security testing to be deferred or skipped entirely. This is a governance failure with predictable consequences: vulnerabilities accumulate unchecked over the release cycle and then either ship to production or are discovered too late to fix properly. The solution is to integrate lightweight security checks throughout development — pre-commit hooks, CI gates, automated scanning — so security is a continuous quality gate rather than a final-step audit.
Building a Security-First Culture
Integrating the OWASP Top 10 into your workflow requires more than technical changes—it involves fostering a culture of security within your team. Tools and checklists are necessary but not sufficient. The most resilient organizations are those where every engineer considers security an intrinsic part of quality, not a tax imposed by a compliance team.
Steps to Foster Security Awareness:
- Conduct regular security audits and training.
- Encourage collaboration between development and security teams.
- Establish clear guidelines for secure coding practices.
Establish Security Champions in Every Team
Designate a security champion on each engineering team — a developer with interest in security who acts as a liaison between the team and the security organization. Security champions do not need to be security specialists; they need to be passionate about the topic and willing to keep the team accountable to security standards. They attend threat modeling sessions, review pull requests with a security lens, and stay current on emerging vulnerabilities relevant to the team’s technology stack.
Run Blameless Security Retrospectives
When a security vulnerability is found in production, the instinct is to identify who wrote the vulnerable code and hold them accountable. This instinct produces the wrong outcomes. Engineers who fear blame stop disclosing vulnerabilities, stop asking questions, and stop flagging risks they observe. Blameless retrospectives examine the systemic conditions that allowed a vulnerability to be introduced and survive to production — unclear security requirements, absent automated checks, insufficient code review depth — and address those root causes rather than individual errors.
Measure and Report Security Metrics
What gets measured gets managed. Track security-relevant metrics over time: mean time to remediate high-severity findings, percentage of CI pipelines with security gates enabled, number of open critical CVEs across all repositories, and the code coverage of security-relevant test cases. Share these metrics in engineering all-hands and use them to prioritize security investment. Improvement in these numbers is evidence that the security program is working; deterioration is a signal for intervention.
Conclusion
Integrating the OWASP Top 10 into your development workflow is a proactive step toward building secure, reliable applications. By adhering to these best practices and leveraging the tools and strategies outlined in this guide, developers can effectively mitigate common vulnerabilities and deliver solutions that inspire trust.
The most important shift is conceptual: security is not a phase that happens after development, nor a checklist that a separate team applies at the end of a sprint. It is a continuous engineering discipline practiced at every stage of the software lifecycle, from the first design conversation to years of production operation. The OWASP Top 10 provides the vocabulary and the framework; the pipeline integrations, code patterns, and cultural practices described throughout this guide provide the implementation path.
Start implementing these measures today to protect your applications and stay ahead in the ever-evolving landscape of cybersecurity.