An application security checklist translates abstract security requirements into concrete, testable controls that development teams can work through systematically. Unlike compliance frameworks that describe what you should achieve, this checklist tells you exactly what to verify — with test steps, code examples, and automation options for each control.
This checklist is organized by the phase of development where each control is most effective: design, code review (SAST), runtime testing (DAST), and deployment. Use it for pre-launch security reviews, sprint-end security gates, or as the basis for a developer security awareness program.
1. Injection Prevention
Injection flaws — SQL injection, command injection, LDAP injection, template injection — consistently top the OWASP Top 10. They are also among the most preventable: the fix is a single, consistent pattern.
SQL Injection (CWE-89)
- Every SQL query uses parameterized statements or an ORM’s safe query API — no string concatenation or interpolation
- Data retrieved from the database is also parameterized when used in new queries (second-order injection prevention)
-
FromSqlRaw(),NativeQuery,cursor.execute()with format strings, and raw SQL methods are reviewed individually - SAST scanner flags CWE-89 with taint analysis (not just pattern matching) — verify against a known-vulnerable test case
What to check in code review:
# FAILING — string interpolation in SQL
query = f"SELECT * FROM orders WHERE customer = '{customer_name}'"
# PASSING — parameterized
cursor.execute("SELECT * FROM orders WHERE customer = %s", (customer_name,))
// FAILING — string concatenation
String sql = "SELECT * FROM users WHERE id = " + userId;
stmt = conn.createStatement().executeQuery(sql);
// PASSING — PreparedStatement
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
stmt.setInt(1, userId);
Command Injection (CWE-78)
- No user-controlled input passed directly to
subprocess,os.system(),Process.Start(),exec(), or shell equivalents - Where shell execution is necessary, use argument-list form (not shell=True / UseShellExecute=True) and validate input against a strict allowlist
- Server-side file processing (image conversion, PDF generation, archiving) does not accept raw user-controlled filenames
Template Injection / Code Injection (CWE-94)
- No user input passed to
eval(),exec(),render_template_string(), or template engines without sandboxing - Jinja2, Twig, Smarty, and Freemarker templates do not render user-controlled strings as template source
LDAP Injection
- LDAP queries use parameterized LDAP library methods — user input is never concatenated into LDAP filter strings
- Special LDAP characters (
*,(,),\,\x00) are escaped in user-supplied directory attribute values
2. Authentication and Session Management
Broken authentication is among the most impactful vulnerability classes because it directly enables account takeover. The controls below cover the most common failure modes.
Password Handling
- Passwords are hashed with bcrypt, scrypt, or Argon2 (not MD5, SHA1, unsalted SHA256, or any fast hash)
- Minimum password length enforced (≥ 12 characters for new applications)
- Password strength checked against known-breached lists (HIBP API or local list)
- Password confirmation field compared server-side, not just client-side
# FAILING — fast hash, no salt
import hashlib
password_hash = hashlib.sha256(password.encode()).hexdigest()
# PASSING — bcrypt with auto-generated salt
import bcrypt
password_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
Session Tokens
- Session tokens are cryptographically random — minimum 128 bits of entropy (
secrets.token_hex(32)in Python,RandomNumberGenerator.GetBytes(32)in C#) - Session tokens are regenerated after successful login (prevents session fixation)
- Session tokens are invalidated server-side on logout (not just cleared client-side)
- Session tokens are never passed in URL parameters
- Session cookies have
HttpOnly,Secure, andSameSite=StrictorSameSite=Laxattributes
Set-Cookie: sessionid=a3f8b2...; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600
Brute Force Protection
- Login endpoint has rate limiting (max 5-10 attempts per account per minute)
- Account lockout or progressive delay after repeated failures
- Password reset endpoint is also rate-limited
- OTP/TOTP verification is rate-limited (prevents brute-forcing 6-digit codes)
Multi-Factor Authentication
- MFA is available and enforced for accounts with elevated privileges
- MFA cannot be bypassed by directly accessing post-authentication endpoints (step-up auth is enforced server-side)
- TOTP secrets are stored hashed, not in plaintext
3. Authorization and Access Control
Broken access control is OWASP’s #1 finding category, appearing in 94% of tested applications. The most common failure: authorization checks are performed only in the UI, not in the API layer.
Enforce Authorization Server-Side
- Every API endpoint performs its own authorization check — never rely on UI visibility as an access control
- Authorization checks are centralized in middleware or a dedicated service — not scattered across individual handlers
- Removing authentication headers/cookies from any request returns 401/403 (not 200 with empty data)
# FAILING — relies on frontend not showing the button
@app.route('/api/admin/users')
def list_users():
return jsonify(User.query.all()) # No auth check!
# PASSING — server-side authorization check
@app.route('/api/admin/users')
@require_role('admin')
def list_users():
return jsonify(User.query.all())
Insecure Direct Object Reference (IDOR)
- Resource IDs in URLs and request bodies are validated against the authenticated user’s ownership or role
- Numeric sequential IDs are supplemented with an authorization check (or replaced with UUIDs — though UUIDs alone don’t substitute for authorization)
- Bulk operations (list all, export all) apply the same user-scoped filtering as single-resource endpoints
Test this manually: Log in as User A, find a resource ID (order, profile, document), then test it with User B’s authenticated session.
Mass Assignment Protection
- POST and PUT request handlers explicitly allowlist accepted fields — never bind request bodies directly to model objects
- Fields like
role,isAdmin,verified,balanceare never accepted from user input in object creation/update endpoints
// FAILING — binds all request fields to the user object
const user = new User(req.body);
// PASSING — explicit field allowlist
const user = new User({
name: req.body.name,
email: req.body.email,
// 'role' and 'isAdmin' are NOT included
});
4. Sensitive Data and Secrets Management
Hardcoded Credentials (CWE-798)
- No passwords, API keys, connection strings, tokens, or private keys in source code
- No secrets in configuration files committed to version control (
.env,appsettings.json,application.properties) - Git history scanned for previously committed secrets (
gitleaks,truffleHog,git-secrets) - SAST scanner detects hardcoded credential patterns (CWE-798) in CI/CD pipeline
# Run gitleaks on the repository (including history)
gitleaks detect --source . --report-format json --report-path gitleaks-report.json
# Pre-commit hook to catch secrets before they're committed
gitleaks protect --staged --verbose
If a secret has been committed: Rotate it immediately — assume it is compromised. Then clean history with git filter-repo. Removing from HEAD does not remove from git history.
Secrets at Runtime
- Credentials loaded from environment variables, a secrets manager (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault), or a platform secret store — not from config files
- Secrets are not logged (logging statements do not include passwords, tokens, or API keys)
- Container images do not have secrets baked in via
ENVorRUNinstructions in Dockerfile
# .env (local development only — NEVER commit)
DATABASE_URL=postgresql://...
API_KEY=sk-live-...
# .gitignore
.env
*.pem
*.key
secrets/
Sensitive Data Exposure
- TLS 1.2+ enforced on all endpoints; TLS 1.0/1.1 disabled
- HSTS header present:
Strict-Transport-Security: max-age=31536000; includeSubDomains - Sensitive fields (passwords, tokens, full PAN) never returned in API responses unless explicitly required
- Error responses do not include stack traces, internal paths, or database schema information in production
- Backup files (
.sql,.bak,.tar.gz) not accessible via web server
5. Cross-Site Scripting (XSS) Prevention
XSS vulnerabilities fall under CWE-79 and OWASP A03:2021.
Output Encoding
- All user-controlled content is HTML-encoded before rendering in HTML context
- User content in JavaScript context is JavaScript-encoded (not just HTML-encoded)
- User content in URL context is URL-encoded
- No
innerHTML,document.write(), ordangerouslySetInnerHTMLwith user-controlled content
<!-- FAILING — raw user input in HTML -->
<p>Hello, {{ user.name }}!</p> <!-- if not auto-escaped -->
<!-- PASSING — auto-escaped by template engine -->
<p>Hello, {{ user.name | escape }}!</p>
<!-- FAILING — raw user input in JavaScript context -->
<script>var username = '{{ user.name }}';</script>
<!-- PASSING — JavaScript-encoded -->
<script>var username = '{{ user.name | tojson }}';</script>
Content Security Policy
-
Content-Security-Policyheader present and restricts inline scripts - CSP does not include
unsafe-inlineorunsafe-evalwithout a hash or nonce - CSP is validated with a CSP evaluator tool (CSP Evaluator by Google)
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{random}'; object-src 'none'; base-uri 'self'
DOM-Based XSS
- Client-side JavaScript does not write URL fragments, query parameters, or
postMessagedata to DOM sinks - React, Vue, Angular, and other frameworks’ unsafe rendering APIs (
dangerouslySetInnerHTML,v-html,[innerHTML]) are not used with user-controlled strings
6. CORS Configuration
CORS misconfigurations are a common source of critical findings in modern APIs.
-
Access-Control-Allow-Origin: *is not set on any endpoint that processes authentication or returns user-specific data - CORS allowlist is a static set of explicitly trusted origins — no dynamic origin reflection (
response.headers['Access-Control-Allow-Origin'] = request.headers['Origin']without validation) -
Access-Control-Allow-Origin: *is never combined withAccess-Control-Allow-Credentials: true -
Vary: Originheader is set on all responses that include a specific origin inAccess-Control-Allow-Origin -
nullorigin is not trusted on any endpoint that processes authentication
// FAILING — reflects any origin
const origin = req.headers.origin;
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
// PASSING — explicit allowlist
const ALLOWED_ORIGINS = new Set(['https://app.yourcompany.com', 'https://yourcompany.com']);
const origin = req.headers.origin;
if (ALLOWED_ORIGINS.has(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Vary', 'Origin');
}
7. HTTP Security Headers
Security headers provide browser-level protection that complements application-level controls. All headers below take minutes to add in web server or application middleware configuration.
| Header | Required Value | What It Prevents |
|---|---|---|
Content-Security-Policy | Restrict inline scripts and external origins | XSS, data injection |
Strict-Transport-Security | max-age=31536000; includeSubDomains | SSL stripping |
X-Content-Type-Options | nosniff | MIME-type sniffing attacks |
X-Frame-Options | DENY or SAMEORIGIN | Clickjacking |
Referrer-Policy | strict-origin-when-cross-origin | Referrer leakage |
Permissions-Policy | Restrict camera, mic, geolocation | Feature abuse |
- All headers above are present on all application responses
-
Server:andX-Powered-By:headers do not disclose software version -
Cache-Control: no-storeis set on pages with sensitive data (account, payment, profile)
Quick check:
curl -si https://yourapp.com | grep -iE "content-security-policy|strict-transport|x-content-type|x-frame|referrer-policy"
8. Software Composition Analysis (SCA)
Third-party dependencies are a major attack vector — the 2020 SolarWinds attack and the 2021 Log4Shell vulnerability both exploited dependencies, not application code.
- All direct and transitive dependencies are scanned for known CVEs
- Dependency lock files are committed (
package-lock.json,Gemfile.lock,poetry.lock,go.sum) - No critical or high-severity CVEs in production dependencies (or documented exceptions with a patching timeline)
- SCA is automated in CI/CD — new dependency vulnerabilities are flagged on pull requests
- Abandoned/unmaintained packages are identified and evaluated for replacement
- Open-source license compliance reviewed (GPL-3.0, AGPL, and similar have specific obligations)
9. CI/CD Security Gates
Integrating security controls into the CI/CD pipeline catches vulnerabilities before they reach production, when they’re cheapest to fix.
Recommended Pipeline Controls
# GitHub Actions — complete AppSec pipeline
name: Application Security
on: [pull_request]
jobs:
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for secrets scanning
- name: Secrets scan (gitleaks)
uses: gitleaks/gitleaks-action@v2
- name: SAST scan (Offensive360)
run: |
curl -X POST https://api.offensive360.com/scan/code \
-H "X-API-Key: ${{ secrets.O360_API_KEY }}" \
--data-binary @.
- name: SCA scan
run: npm audit --audit-level=high # Or your language equivalent
dast:
runs-on: ubuntu-latest
needs: [build]
steps:
- name: Start application
run: docker-compose up -d
- name: DAST scan (Offensive360)
run: |
o360 dast --target http://localhost:8080 \
--auth-username [email protected] \
--auth-password ${{ secrets.TEST_PASS }} \
--fail-on HIGH
Pipeline Gates (Break the Build)
- SAST scan runs on every pull request — critical/high findings block merge
- Secrets scan runs on every commit — committed secrets block push
- SCA scan runs on dependency changes — critical CVEs block merge
- DAST scan runs on deployments to staging — critical/high findings trigger rollback alert
- Security gates have defined SLAs: Critical = block and fix immediately; High = fix within sprint
10. File Upload Security
File upload vulnerabilities (CWE-434) can lead to remote code execution, path traversal, or stored XSS.
- Uploaded file type is validated by content (magic bytes) — not by extension or
Content-Typeheader alone - Executable extensions are rejected:
.php,.aspx,.jsp,.sh,.exe,.py,.rband double-extension variants (.php.jpg) - Upload directory is outside the web root, or configured to never execute uploaded files
- Uploaded files are served with a content-disposition header (
Content-Disposition: attachment) to prevent execution in-browser - File size limits enforced (reject oversized files before processing)
- Filenames are sanitized — use a UUID/random name for storage, not the user-supplied filename
import uuid, os
from werkzeug.utils import secure_filename
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'pdf'}
UPLOAD_FOLDER = '/data/uploads' # Outside web root
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/upload', methods=['POST'])
def upload():
file = request.files['file']
if not allowed_file(file.filename):
return 'Invalid file type', 400
# Store with UUID name — ignore user-supplied filename
ext = file.filename.rsplit('.', 1)[1].lower()
safe_filename = f"{uuid.uuid4()}.{ext}"
file.save(os.path.join(UPLOAD_FOLDER, safe_filename))
return 'Uploaded', 200
11. Logging and Monitoring
Insufficient logging and monitoring is OWASP A09:2021 — and it’s the difference between detecting a breach in hours versus months.
- Authentication events logged: login success, login failure, logout, password change, MFA events
- Authorization failures logged: 403 responses, attempts to access out-of-scope resources
- Admin actions logged: user management, configuration changes, privilege changes
- Logs do NOT contain sensitive data: passwords, API keys, session tokens, PAN, SSN
- Logs are stored centrally and tamper-resistant (separate from application servers)
- Alerting configured for high-frequency authentication failures, unusual access patterns, and privilege escalation
- Log retention meets compliance requirements (PCI-DSS: 1 year; HIPAA: 6 years)
AppSec Checklist by Development Phase
| Phase | Key Controls | Tool |
|---|---|---|
| Code | Injection prevention, secrets, output encoding | SAST, code review |
| Pull Request | SAST scan, secrets scan, SCA | CI/CD pipeline |
| Build | Dependency scan, IaC security | SCA, Checkov/tfsec |
| Deploy to Staging | DAST scan, header check | DAST scanner |
| Production | Runtime monitoring, log alerting | SIEM, WAF |
| Periodic | Full pentest, dependency audit | Manual + SAST/DAST |
Automation Coverage Summary
| Checklist Area | SAST Automates | DAST Automates | Manual Required |
|---|---|---|---|
| SQL injection | ✅ Full taint analysis | ✅ Confirmed by testing | Review ORM raw queries |
| XSS | ✅ Output encoding | ✅ Reflected/stored | DOM-based in complex SPAs |
| Hardcoded secrets | ✅ CWE-798 | — | Git history review |
| Auth/session | Partial (code patterns) | ✅ Session behavior | MFA bypass, logic |
| CORS | ✅ Origin reflection | ✅ Headers check | — |
| Security headers | — | ✅ Header audit | — |
| IDOR/access control | Partial | ✅ ID enumeration | Complex authorization |
| Business logic | — | — | ✅ Always manual |
| SCA | ✅ Dependency CVEs | — | License review |
Offensive360 covers SAST, DAST, and SCA in a single platform — with findings from all three correlated in a unified report. A confirmed SQL injection appears as one finding with both the code location (SAST) and the verified exploit (DAST), giving you both the root cause and the proof of exploitability.
Frequently Asked Questions
How is this checklist different from the OWASP Testing Guide (WSTG)?
OWASP’s Web Security Testing Guide is exhaustive — over 200 test cases, written for professional penetration testers. This checklist is developer-oriented: it focuses on the controls that prevent vulnerabilities, not just the tests that detect them. Use this checklist during development and code review; use the WSTG for formal penetration testing engagements.
Should every item be checked before every release?
Automate what you can in CI/CD (injection detection via SAST, header checks via DAST, SCA), and those run on every commit or pull request. Manual checks (IDOR testing, business logic, complex authorization) should run on a regular cycle — before major releases, on any significant new feature, and on a quarterly basis for production applications.
What’s the minimum viable AppSec program for a small team?
For a small development team:
- SAST in CI/CD — run a static analysis scan on every pull request; fix Critical and High findings before merge
- Secrets scanning — add a pre-commit hook with
gitleaksto prevent committed secrets - SCA — enable Dependabot or
npm audit/pip auditto catch vulnerable dependencies - One annual pentest or DAST scan — an external review to catch what automated tools miss
This four-step baseline catches the majority of exploitable vulnerabilities before they reach production.
How do I prioritize findings?
Use CVSS severity as a starting point but adjust for exploitability and business context:
- Critical (fix immediately): Remote code execution, authentication bypass, plaintext credential exposure, mass data exposure
- High (fix this sprint): SQL injection, stored XSS, IDOR exposing PII, hardcoded credentials in active use
- Medium (fix next sprint): Reflected XSS, missing security headers, CORS misconfiguration with limited impact
- Low (scheduled backlog): Information disclosure, missing rate limiting on low-risk endpoints
Get Started with Automated AppSec
The fastest way to work through this checklist for an existing application:
-
One-time SAST scan for $500 — submit your source code and receive a full vulnerability report within 48 hours, no subscription required. Covers injection, secrets, XSS, weak cryptography, and 60+ other vulnerability classes across all major languages.
-
One-time DAST scan — authenticated dynamic scan of your running application, testing authentication, headers, CORS, XSS, and injection at runtime.
-
Book a demo — see how Offensive360 SAST + DAST + SCA integrates into your CI/CD pipeline and provides unified findings across all three testing approaches.