Application Security: Secure Coding OWASP Top 10 SAST & DAST Penetration Testing API Security Container Security
← Back to Domains OWASP Top 10 →
⏱ 12 min read 📊 Advanced 🗓 Updated Jan 2025

🛠 Security in the SDLC

Shifting security left means embedding security activities earlier in the Software Development Lifecycle — where fixing a vulnerability costs orders of magnitude less than patching it in production. A design-phase bug costs roughly 30x less to fix than the same bug discovered post-deployment.

Cost of Fixing Bugs by Phase

PhaseRelative Cost
Requirements / Design1x
Development5x
Testing / QA10x
Staging20x
Production30–100x

Maturity Models

  • OWASP SAMM (Software Assurance Maturity Model) — five business functions: Governance, Design, Implementation, Verification, Operations. Each scored 0–3.
  • BSIMM (Building Security In Maturity Model) — data-driven from real enterprise programs; 121 activities across 4 domains.
  • Use maturity models to benchmark your program and identify gaps, not as a compliance checkbox.
  • Target the level appropriate to your risk profile — a startup and a bank have different targets.

Threat Modeling at Design Phase

  • STRIDE — Spoofing, Tampering, Repudiation, Information Disclosure, Denial of Service, Elevation of Privilege. Per-component analysis.
  • PASTA (Process for Attack Simulation and Threat Analysis) — attacker-centric, risk-based, aligns business objectives to technical threats.
  • Attack Trees — hierarchical decomposition of attacker goals; useful for communicating threats to non-technical stakeholders.
  • Threat model during architecture review — not after code is written.
  • Security requirements must be tracked alongside functional requirements in your backlog.

Shift-Left in Practice

Add security acceptance criteria to every user story. "As a user, I can reset my password" must also include: "Password reset tokens expire in 15 minutes, are single-use, and invalidated on successful use." Security requirements that aren't written down don't get implemented.

🔒 Input Validation & Output Encoding

The cardinal rule: trust nothing from external sources. Every byte of user input is potentially hostile. Validation restricts what comes in; encoding makes what goes out safe to render or execute.

Validation Strategies

  • Whitelist (allowlist) — define what IS valid and reject everything else. Preferred approach. E.g., "username must be 3–32 alphanumeric characters."
  • Blacklist (denylist) — block known bad patterns. Fragile; attackers find bypasses (Unicode homoglyphs, encoding tricks). Use only when whitelist is impractical.
  • Server-side validation is mandatory — client-side validation is UX only. Any attacker bypasses the browser with curl or Burp Suite.
  • Validate type, length, format, range, and encoding. Reject early, reject clearly.
  • Canonicalize input before validating — normalize Unicode, decode percent-encoding, resolve path separators.

Output Encoding Contexts

  • HTML context — encode < > & " ' to HTML entities. Prevents injected markup from being interpreted.
  • JavaScript context — JSON-encode or use JavaScript string escaping. Never concatenate untrusted data into a script block.
  • URL context — percent-encode user data used in URLs. Prevents open redirects and parameter injection.
  • CSS context — rarely needed but escape hex values; CSS can be used for data exfiltration.
  • Use a library (OWASP Java Encoder, DOMPurify, html/template in Go) — do not write your own encoder.

SQL Injection Prevention

-- INSECURE: string concatenation (never do this)
query = "SELECT * FROM users WHERE username = '" + username + "'"
-- Attacker input: ' OR '1'='1
-- Results in: SELECT * FROM users WHERE username = '' OR '1'='1'
-- Returns all rows — authentication bypassed

-- SECURE: parameterized query (prepared statement)
query = "SELECT * FROM users WHERE username = ?"
stmt = conn.prepare(query)
stmt.execute(username)

-- SECURE: named parameters (Python SQLAlchemy style)
query = "SELECT * FROM users WHERE username = :username"
conn.execute(query, {"username": username})

-- ORM usage (also safe when used correctly)
User.objects.filter(username=username)  # Django ORM
User.where(username: username)          # ActiveRecord

ORMs Are Not Automatically Safe

Raw queries and query concatenation within an ORM are still vulnerable. User.objects.raw("SELECT * FROM users WHERE name = '%s'" % name) is just as dangerous as raw SQL concatenation.

🔐 Authentication & Session Security

Authentication is one of the most security-critical components in any application — and one of the most frequently broken. The core principle: never implement cryptographic primitives yourself. Use well-vetted libraries.

Password Hashing

  • bcrypt — adaptive work factor, salt built-in, widely supported. Minimum cost factor 12 for new systems.
  • Argon2id — winner of Password Hashing Competition (2015). Memory-hard, resistant to GPU attacks. Current best practice for new systems.
  • scrypt — memory-hard, older than Argon2. Still secure, widely available.
  • PBKDF2 — FIPS-approved, widely supported in compliance-heavy environments. Use with SHA-256, minimum 600,000 iterations (2023 NIST guidance).
  • Never use: MD5, SHA-1, SHA-256/512 without salt and iterations — these are fast hashes, trivially cracked with GPUs.

Session Management

  • Generate session tokens using a CSPRNG (Cryptographically Secure Pseudo-Random Number Generator) — never use math.random() or timestamps.
  • Session tokens must be at least 128 bits (16 bytes) of entropy.
  • Session fixation — regenerate session ID on privilege change (login, role change). Attacker pre-sets a session ID; if reused post-login, they're authenticated.
  • Session hijacking — use Secure and HttpOnly cookie flags. Bind session to user-agent / IP (with care for mobile networks).
  • Implement idle timeout and absolute timeout. Provide logout that actually invalidates the server-side session.

JWT Security Pitfalls

  • alg:none attack — if your library accepts "none" as an algorithm, forged tokens with no signature are accepted. Explicitly whitelist allowed algorithms.
  • Weak secrets — HS256 with a short or guessable secret is brute-forceable. Use RS256 (asymmetric) for distributed systems; use strong secrets (256+ bits) for HS256.
  • Missing expiry — always set exp claim. Short-lived tokens (15 min) with refresh token rotation is best practice.
  • Sensitive data in payload — JWT payloads are base64-encoded, not encrypted. Anyone with the token can read the claims. Use JWE if confidentiality is needed.
  • Validate iss, aud, and exp on every request.

OAuth 2.0 / OIDC Best Practices

  • Use Authorization Code + PKCE for all public clients (SPAs, mobile). Never use Implicit Flow in new implementations.
  • Validate the state parameter to prevent CSRF on the callback.
  • Verify the nonce in OIDC ID tokens to prevent replay attacks.
  • Store tokens in memory or HttpOnly cookies — not localStorage (XSS accessible).
  • Use a well-known, audited OAuth library — do not hand-roll token exchange code.

📝 Error Handling & Logging

Error messages are a reconnaissance goldmine for attackers. Stack traces reveal framework versions, file paths, and internal logic. Meanwhile, insufficient logging means breaches go undetected for months.

Error Message Principles

  • External error messages must be generic — "An error occurred" not "NullPointerException in UserService.java:142".
  • Show internal errors only to authenticated administrators in a secured interface.
  • Use a reference ID in the user-facing error so support can correlate to the internal log.
  • Avoid revealing whether a username exists during login failures — use "Invalid credentials" not "User not found".
  • Avoid path disclosure — catch filesystem exceptions and return generic errors.

What to Log

  • Authentication events (success and failure, with IP and timestamp)
  • Access control failures (403s with user context)
  • Input validation failures (potential attack patterns)
  • High-value transactions (payments, admin actions, data exports)
  • Session events (creation, logout, timeout, token refresh)
  • Never log: passwords, session tokens, API keys, credit card numbers, PII beyond necessary identifiers

Structured & SIEM-Ready Logging

  • Use structured log formats (JSON) so logs are machine-parseable by SIEM tools.
  • Include consistent fields: timestamp (ISO 8601 UTC), event_type, user_id, ip_address, request_id, outcome.
  • Never log to the application database — use a separate log store that attackers can't modify.
  • Forward logs to a SIEM (Splunk, Elastic, Sentinel) in real time.
  • Define retention policy (typically 90 days hot, 1 year cold) based on compliance requirements.
// Structured log example (JSON)
{
  "timestamp": "2026-03-27T14:23:01.452Z",
  "event_type": "AUTH_FAILURE",
  "user_id": "user_8821",
  "ip_address": "203.0.113.42",
  "user_agent": "Mozilla/5.0...",
  "request_id": "req_a4f82c91",
  "outcome": "FAILURE",
  "reason": "invalid_credentials",
  "attempt_count": 3
}
// Note: no password, no session token, no PII beyond user_id

📦 Dependency & Supply Chain Security

Modern applications are 80%+ third-party code by volume. Every dependency you add is a trust decision. Supply chain attacks (SolarWinds, XZ Utils) demonstrate that even trusted packages can be compromised.

Software Composition Analysis (SCA)

  • Snyk — comprehensive SCA with remediation advice; integrates with GitHub, GitLab, Jira. Free tier available.
  • OWASP Dependency-Check — open source; supports Java, .NET, Node, Python. Maps dependencies to NVD CVEs.
  • Dependabot — GitHub native; auto-creates PRs to update vulnerable dependencies. Enable for every repo.
  • npm audit / pip-audit / mvn dependency:check — built-in toolchain audits. Run in CI on every build.
  • Subscribe to CVE feeds for your key dependencies — don't wait for automated scanners.

Lock Files & Reproducible Builds

  • Always commit lock files (package-lock.json, Pipfile.lock, Gemfile.lock, go.sum) — they pin exact versions including transitive dependencies.
  • Verify checksums/hashes for downloaded packages. npm uses integrity hashes; pip uses --require-hashes.
  • Use private registries or package mirrors for critical dependencies — protect against upstream deletion or compromise.
  • Audit new dependencies before adding — check author reputation, commit history, download count, and the dependency's own dependency tree.

SBOM & License Compliance

  • SBOM (Software Bill of Materials) — a formal inventory of all components in your software. Required by US Executive Order 14028 for federal software.
  • SBOM formats: SPDX (Linux Foundation) and CycloneDX (OWASP). Both are machine-readable.
  • Generate SBOMs with Syft, cyclonedx-cli, or Trivy.
  • License compliance: GPL in a commercial product can be problematic. FOSSA, TLDR Legal, or Snyk License Compliance can automate this.
  • Include SBOM generation in your CI/CD pipeline and store it with the release artifact.

80%+ of Your App Is Third-Party Code

A typical Node.js application has hundreds of transitive dependencies. Scan dependencies on every build — not just at project creation. A vulnerability disclosed today in a package you installed six months ago is just as exploitable as a fresh injection flaw. Automate dependency updates and treat "dependency upgrade" PRs as security work, not housekeeping.

Snyk OWASP Dependency-Check Dependabot SBOM SPDX CycloneDX npm audit pip-audit Supply Chain