🛠 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
| Phase | Relative Cost |
|---|---|
| Requirements / Design | 1x |
| Development | 5x |
| Testing / QA | 10x |
| Staging | 20x |
| Production | 30–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.