Skip to main content

Free 30-min security demo Book Now

Offensive360 Offensive360
Application Security

Application Security Checklist 2026

Application security checklist for development teams in 2026: injection prevention, authentication, secrets management, dependency scanning, and CI/CD.

Offensive360 Security Research Team — min read
application security checklist appsec checklist application security secure coding checklist appsec OWASP Top 10 SAST DAST secure development security controls AppSec 2026 web application security checklist

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, and SameSite=Strict or SameSite=Lax attributes
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, balance are 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 ENV or RUN instructions 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(), or dangerouslySetInnerHTML with 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-Policy header present and restricts inline scripts
  • CSP does not include unsafe-inline or unsafe-eval without 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 postMessage data 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 with Access-Control-Allow-Credentials: true
  • Vary: Origin header is set on all responses that include a specific origin in Access-Control-Allow-Origin
  • null origin 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.

HeaderRequired ValueWhat It Prevents
Content-Security-PolicyRestrict inline scripts and external originsXSS, data injection
Strict-Transport-Securitymax-age=31536000; includeSubDomainsSSL stripping
X-Content-Type-OptionsnosniffMIME-type sniffing attacks
X-Frame-OptionsDENY or SAMEORIGINClickjacking
Referrer-Policystrict-origin-when-cross-originReferrer leakage
Permissions-PolicyRestrict camera, mic, geolocationFeature abuse
  • All headers above are present on all application responses
  • Server: and X-Powered-By: headers do not disclose software version
  • Cache-Control: no-store is 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.

# 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-Type header alone
  • Executable extensions are rejected: .php, .aspx, .jsp, .sh, .exe, .py, .rb and 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

PhaseKey ControlsTool
CodeInjection prevention, secrets, output encodingSAST, code review
Pull RequestSAST scan, secrets scan, SCACI/CD pipeline
BuildDependency scan, IaC securitySCA, Checkov/tfsec
Deploy to StagingDAST scan, header checkDAST scanner
ProductionRuntime monitoring, log alertingSIEM, WAF
PeriodicFull pentest, dependency auditManual + SAST/DAST

Automation Coverage Summary

Checklist AreaSAST AutomatesDAST AutomatesManual Required
SQL injection✅ Full taint analysis✅ Confirmed by testingReview ORM raw queries
XSS✅ Output encoding✅ Reflected/storedDOM-based in complex SPAs
Hardcoded secrets✅ CWE-798Git history review
Auth/sessionPartial (code patterns)✅ Session behaviorMFA bypass, logic
CORS✅ Origin reflection✅ Headers check
Security headers✅ Header audit
IDOR/access controlPartial✅ ID enumerationComplex authorization
Business logic✅ Always manual
SCA✅ Dependency CVEsLicense 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:

  1. SAST in CI/CD — run a static analysis scan on every pull request; fix Critical and High findings before merge
  2. Secrets scanning — add a pre-commit hook with gitleaks to prevent committed secrets
  3. SCA — enable Dependabot or npm audit / pip audit to catch vulnerable dependencies
  4. 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:

  1. 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.

  2. One-time DAST scan — authenticated dynamic scan of your running application, testing authentication, headers, CORS, XSS, and injection at runtime.

  3. Book a demo — see how Offensive360 SAST + DAST + SCA integrates into your CI/CD pipeline and provides unified findings across all three testing approaches.

Offensive360 Security Research Team

Application Security Research

Find vulnerabilities before attackers do

Run Offensive360 SAST and DAST against your applications and get a full vulnerability report in minutes.