Skip to content
xscriptor-ai

secure-coding

Cross-language secure coding reference pack for 2026: injection prevention across SQL, NoSQL, ORM, OS command, template, LDAP, and XPath sinks; XSS/CSRF; output encoding; Trusted Types and CSP; SSRF and unsafe deserialization; passwords, AEAD, randomness, keys, and secrets; validation, file uploads, and path traversal; per-language cheatsheets (Python, TS/Node, Go, Rust, JVM, C/C++); dependency, provenance, and patch-SLA hygiene; and a secure code review checklist with SAST/DAST/SCA/fuzzing tests. Use when writing, reviewing, or hardening application code in any language; when fixing injection, XSS, CSRF, SSRF, deserialization, crypto, secret-handling, validation, upload, or dependency vulnerabilities; when choosing parameterization, encoding, or validation strategies; or when auditing a codebase against OWASP-style defect classes. Optional: consumers degrade gracefully when absent.

seniorv2.0.0senior/secure-coding
View source on GitHub

Secure Coding

Secure coding is the practice of writing and reviewing application code so that attacker-controlled data never gains control of an interpreter, a privilege boundary, or a cryptographic decision. Most real exploits are not exotic: they are concatenated strings, unvalidated object shapes, missing authorization checks, reused nonces, and stale dependencies. This pack is the working reference for preventing and reviewing those defect classes in any stack.

The depth lives in references/; this file is the map. Named libraries and tools are examples of a category, not endorsements. Version numbers are floors or ranges, never exact pins; verify upstream before depending on a specific release.

Non-Negotiable Core Rules

  1. All input is hostile until validated. HTTP bodies, headers, cookies, query strings, file contents, message-queue payloads, database rows written by other users, and third-party API responses are all untrusted. Trust is granted per boundary, never per network location.
  2. Parameterize, never concatenate, across every interpreter. Queries, OS commands, templates, LDAP filters, and XPath expressions receive data through bindings. Where binding is impossible (identifiers, sort keys, operator choice), use a fixed allowlist, not escaping.
  3. Validate at the boundary, encode at the sink. Validation decides what enters the system; output encoding decides what reaches HTML, SQL, shell, or logs. Do both, and never use one as a substitute for the other.
  4. Authorization is server-side, per request, per object, per action. Authentication answers who; authorization answers whether this principal may do this to this object now. Enforce it in the data layer or a policy layer that every route passes through.
  5. Encoding is context-specific. HTML text, attributes, JavaScript, CSS, URLs, JSON, XML, CSV, and headers each need a different encoder. One global "escape" function guarantees bugs in at least one context.
  6. Use vetted cryptography only. AEAD or nothing; unique nonces per key; CSPRNG for every security decision; constant-time comparison for secrets. Never invent primitives, modes, or protocols; never use Math.random, rand() unseeded, or timestamps as entropy.
  7. Secrets never live in source, images, logs, or clients. Use workload identity and short-lived credentials; store long-lived material in a KMS/HSM or secrets manager; scan CI and git history; rotate on exposure and schedule.
  8. Deserialize only trusted, schema-validated data. Native object serialization (Java, Python pickle, PHP, Ruby Marshal, .NET BinaryFormatter, YAML unsafe load) crosses trust boundaries only in attacks. Prefer JSON/CBOR/protobuf with explicit schemas; sign state you must round-trip.
  9. Deny by default: egress, filesystem, CORS, CSP, cookies. Explicit allowlists for outbound network destinations, upload paths, browser origins, and cookie scope. Every exception is a documented decision with an owner.
  10. Treat dependencies and build inputs as trust decisions. Lockfiles are mandatory, provenance is verified where available, install scripts are constrained, and every advisory has a patched SLA. A vulnerable transitive package is your vulnerability.
  11. Fail closed, log safely, bound resources. On errors, deny rather than permit; never log secrets, tokens, or full payloads; cap sizes, counts, depth, and time so one request cannot exhaust the process (see references/05-validation-encoding.md).
  12. Fix the class, not the instance, and pin it with a test. For every vulnerability, add a regression test, a lint/SAST rule where possible, and a note in the review record so the same pattern cannot silently return.

Decision Tables

Control selection by data path

| Data path | Primary control | Secondary control | Never do | |---|---|---|---| | User value into SQL/ORM | Bound parameter | Schema validation, least-privilege DB account | String formatting or f-string | | Table/column/sort identifier | Allowlist constant map | Reject unknown keys | Quote/escape identifiers | | User value into OS command | argv array, no shell | Absolute binary path, -- terminator | sh -c, exec, shell=True | | User value into HTML | Context-aware output encoding | CSP, Trusted Types, sanitizer for rich text | innerHTML with raw data | | User value into a template | Pass as template variable | Logic-less engine, sandboxed rendering | Compiling user input as a template | | User URL fetched by server | Scheme allowlist + DNS/IP pinning | Egress proxy, redirect re-validation | Fetching arbitrary user URLs directly | | Serialized state from client | Signed + schema validated | HMAC, short TTL | Native deserialization | | User file upload | Content sniff + re-encode + random name | Store outside web root, serve inert | Trusting extension or MIME header | | Secret at runtime | Secrets manager / KMS | Short TTL, rotation, audit | Config file, env default, hardcode |

Where the control belongs

| Layer | Responsible for | Examples | |---|---|---| | Edge / gateway | Transport, coarse rate limits, request size caps | TLS, WAF, quotas | | Application boundary | Authentication, schema validation, canonicalization | Middleware, DTOs, Zod/Pydantic | | Domain logic | Authorization, invariants, business rules | Policy checks, ownership filters | | Data layer | Query parameterization, row scoping, constraints | Prepared statements, RLS, FK/CHECK | | Presentation | Output encoding, CSP, cookie attributes | Template engine, security headers | | Platform / CI | Dependency policy, secret scanning, least privilege | Lockfiles, OIDC, scanners |

Cryptographic primitive selection

| Need | Use | Avoid | |---|---|---| | Password storage | Argon2id (or scrypt/bcrypt with tuned work factor) | MD5/SHA-1/SHA-256 alone, fast hashes, unsalted | | Symmetric encryption | AES-256-GCM or ChaCha20-Poly1305, unique nonce | ECB, CBC without MAC, static IV | | Key derivation | HKDF for key material, Argon2id/PBKDF2 for passwords | Ad-hoc KDF, password as key | | Signatures | Ed25519 (EdDSA), ECDSA P-256/P-384, RSA-PSS >= 3072 | RSA PKCS#1 v1.5 for new designs, MD5/SHA-1 | | Key exchange | X25519/ECDH P-256+ with TLS 1.3 or Noise | Custom Diffie-Hellman, anonymous DH | | Integrity comparison | Constant-time equality | == on MACs/tokens | | Random tokens | CSPRNG, >= 128 bits | UUIDv1, counters, timestamps, rand() | | Post-quantum migration | Hybrid classical + ML-KEM (FIPS 203) where supported | Waiting until migration is forced |

Secret storage by environment

| Environment | Storage | Notes | |---|---|---| | Local dev | .env gitignored, direnv, mock secrets | Never a real production credential | | CI/CD | OIDC workload identity, masked variables | Prefer federation over long-lived tokens | | Staging/prod | KMS/HSM or secrets manager, injected at runtime | Envelope encryption, audit, rotation | | Customer-managed | External Secrets Operator, sealed secrets | Cluster compromise must not reveal plaintext |

Reference Index

| File | Scope | Load when | |---|---|---| | references/01-injection.md | SQL/NoSQL/ORM injection, OS command, template (SSTI), LDAP/XPath; parameterization, identifier allowlists, per-context escaping, DB hardening | Any query, command, template, or directory call built from user data; reviewing data access code | | references/02-xss-csrf.md | Output encoding contexts, dangerous sinks, sanitizers, Trusted Types, CSP, cookie attributes, CSRF tokens, SameSite, Fetch Metadata, CORS interactions | Building or reviewing browser-facing UI, session handling, headers, or cross-origin APIs | | references/03-ssrf-deserialization.md | SSRF patterns, cloud metadata endpoints, redirect and DNS-rebinding handling, egress allowlists; unsafe deserialization per language, safe formats, signed state | Server-side fetches, webhooks, URL previews, import/export, cache or session deserialization | | references/04-crypto-secrets.md | Password hashing, AEAD and nonce rules, secure randomness, constant-time comparison, key storage and rotation, secret life cycle, common misuse | Choosing or reviewing crypto, storing secrets, generating tokens, handling keys | | references/05-validation-encoding.md | Validation strategy and trust boundaries, schema libraries, canonicalization, Unicode pitfalls, file uploads, path traversal, output encoding contexts, CSV/header/log injection | Designing DTOs or middleware, handling files or paths, normalizing input, fixing encoding bugs | | references/06-per-language-cheatsheets.md | Sharp-edge rules and safe APIs for Python, TypeScript/Node, Go, Rust, Java/Kotlin, C/C++; links to sibling language packs | Writing or reviewing code in a specific language and you need the local unsafe/safe mapping | | references/07-dependencies.md | Dependency hygiene, lockfiles, integrity and provenance, SBOM, audit automation, patch SLAs, typosquatting and dependency confusion, CI hardening | Adding a dependency, triaging advisories, hardening install and release pipelines | | references/08-review-checklist.md | Secure code review process and per-class checklist, security-focused tests (SAST/DAST/SCA/IaC/secrets), fuzzing and sanitizers, threat-driven test cases, CI gates and triage | Reviewing a diff or service, building a security test plan, setting CI security gates |

Sibling Packs

| Pack | Use for | Link | |---|---|---| | security | Threat modeling (STRIDE), authn/authz architecture, API and container security, zero trust | ../security/SKILL.md | | web | Browser security depth: CSP, Trusted Types, cookies, third-party scripts, headers | ../web/SKILL.md | | api-design | OAuth 2.1, OIDC, scopes, idempotency, webhook verification, contract review | ../api-design/SKILL.md | | testing | Test strategy, fixtures, fuzzing ergonomics, CI structure for security tests | ../testing/SKILL.md | | cloud | Supply-chain security, admission control, platform hardening, secret operators | ../cloud/SKILL.md | | python | Python idioms, async, SQLAlchemy/pydantic depth, packaging and deployment | ../python/SKILL.md | | typescript | TS/Node depth: types, runtimes, framework security, tooling | ../typescript/SKILL.md | | go | Go depth: concurrency, database/sql, pgx, module and supply-chain practice | ../go/SKILL.md | | rust | Rust depth: ownership, unsafe/FFI review, axum, sqlx, cargo audit workflows | ../rust/SKILL.md | | java-kotlin | JVM depth: JDBC/JPA, Spring Security, serialization, Gradle/Maven supply chain | ../java-kotlin/SKILL.md |

How to Use This Pack

  • Start with the control-selection table, then load exactly the references the task needs. One reference is usually enough for a fix; reviews load two or three.
  • For a new service or feature, read 01, 02, and 05 before writing handlers, 04 before adding any crypto or session logic, and 07 before adding dependencies.
  • For an incident or audit, read 08 first to structure the review, then the defect-class reference it points to.
  • References cross-link with relative paths (./NN-*.md) and point to sibling packs for depth; follow those links instead of re-deriving guidance here.

Port

  • Port id — skill://senior/secure-coding (version in metadata.port-version).
  • Kind — read-only reference pack; no side effects, no tools, no scripts required.
  • Entrypoint — this SKILL.md; depth lives in references/.
  • Load modes
    1. Installed agent: skill({ name: "secure-coding" }) in OpenCode; Claude Code reads <skills-dir>/secure-coding/SKILL.md.
    2. Orchestrator: read SKILL.md, then load only the references the task needs.
    3. Not installed: consumers MUST degrade gracefully using their own guidance and report the degraded mode. Never block on the pack.
  • Consumer contract — metadata.consumers lists the agents that may load it (senior-appsec, senior-pentest, all coding agents, orchestrator); consumers reference it as load skill secure-coding (optional).
  • Stability — stable; breaking changes bump port-version major.

References (8)

01-injection

Injection

Scope: preventing attacker-controlled data from becoming code in SQL/NoSQL/ORM queries, OS commands, templates, LDAP filters, and XPath expressions.

Root Cause and Universal Rules

Injection happens when data crosses into an interpreter as structure because the program built one string with concatenation, formatting, or interpolation. The fix is always the same: keep code and data structurally separate.

  1. Bind values as parameters. Never format them into the statement.
  2. Allowlist anything that cannot be parameterized: identifiers, sort direction, operators, engine names, template paths.
  3. Validate type, length, and shape at the boundary before the value reaches any interpreter (see ./05-validation-encoding.md).
  4. Encode only at the output sink, per context. Escaping is a last resort, not a design.
  5. Grant the interpreter least privilege: the DB account cannot run DDL or read other schemas, the process cannot spawn arbitrary binaries, the template engine cannot touch the filesystem.
  6. Treat stored data from other users as untrusted (second-order injection): it may be concatenated later by a report builder, admin filter, or export job.

SQL Injection

Required pattern per stack

| Stack | Unsafe | Safe | |---|---|---| | Python DB-API | cursor.execute(f"SELECT ... {v}") | cursor.execute("SELECT ... %s", (v,)) | | psycopg 3 | f-string SQL | %s params; sql.Identifier for names | | SQLAlchemy | text(f"...") | text("... :id").bindparams(id=v) or ORM filters | | Django ORM | raw(f"..."), .extra() | ORM lookups; raw("... %s", [v]) if unavoidable | | Node pg | template literal with ${v} | $1 placeholders + values array | | Knex | whereRaw(f"...") | .where({ col: v }) or bindings array | | Prisma | $queryRawUnsafe | $queryRaw tagged template | | TypeORM | .query(f"...") | query builder with named params | | JDBC | Statement + concat | PreparedStatement + setX | | JPA/Hibernate | native query concat, HQL concat | setParameter, Criteria API | | Go database/sql | fmt.Sprintf into query | QueryContext(ctx, "... $1", v) | | .NET | SqlCommand concat | Parameters.Add("@p", v) | | Rust sqlx | format! into query | query!/query_as! or .bind(v) |

Raw-query escape hatches still exist in every ORM; review every occurrence of raw, text, queryRaw, createNativeQuery, whereRaw, $executeRawUnsafe, em.createNativeQuery, and execute_sql and demand a binding or an allowlist.

Identifiers, sort keys, and grammar positions

Most engines cannot bind identifiers, ORDER BY, LIMIT, ASC/DESC, or operator names. Map user keys to constants; never quote user input as an identifier.

SORTS = {"created": "created_at", "name": "lower(name)"}
direction = "DESC" if direction == "desc" else "ASC"
sort = SORTS.get(sort_key)
if sort is None:
    raise BadRequest("unknown sort key")
sql = f"SELECT id, name FROM items ORDER BY {sort} {direction}"

LIKE and wildcard abuse

Treat % and _ in user input as literal when the user is not supposed to write a pattern. Escape them with a chosen escape character and declare it (ESCAPE '\'), or reject patterns and offer structured filters instead. Bind the completed pattern as a value.

ORM and second-order pitfalls

  • Query builders are safe only when values flow through their parameter APIs; literal(), sql.raw(), and string fragments reintroduce injection.
  • HQL/JPQL with + concatenation is as injectable as SQL.
  • Stored values used in ORDER BY, dynamic reports, admin filters, and CSV exports are a common second-order path. Apply the same allowlists at read time.
  • Over-posting (mass assignment) lets clients set fields they should not; bind DTOs with explicit field allowlists (see ./05-validation-encoding.md).

Database hardening

  • One DB account per service; no DDL, no SUPERUSER, no cross-schema reads.
  • Statement timeouts and row limits to blunt blind injection and denial of service.
  • Disable or restrict dangerous features you do not need: COPY ... PROGRAM, LOAD_FILE, INTO OUTFILE, xp_cmdshell, stacked queries, CREATE EXTENSION.
  • Return generic errors to clients; detailed SQL errors belong in server logs, not responses.

NoSQL Injection

MongoDB

The classic bug is passing a request body straight into a filter, allowing operator injection:

// Attacker sends {"email": {"$gt": ""}} -> matches the first user
const user = await db.collection("users").findOne({ email: req.body.email });
// Fix: enforce scalar type before the query
const email = req.body.email;
if (typeof email !== "string" || email.length > 254) throw new BadRequest("invalid email");
const user = await db.collection("users").findOne({ email });

Rules:

  • Validate scalars; reject objects/arrays where a string is expected. Reject keys starting with $ or containing . in user-supplied filter objects.
  • $where, $function, $accumulator, and map-reduce execute JavaScript server-side: ban them with user data; if unavoidable, disable server-side scripting or sandbox with strict limits.
  • $regex: treat user input as a literal by escaping regex metacharacters, or reject patterns. Bound pattern length and set regex timeouts to avoid ReDoS.
  • Aggregation pipelines assembled from user input: allowlist stage and operator names; never let the client choose $lookup, $out, $merge, or $function freely.
  • Mongoose schema typing and strict mode help, but do not replace explicit scalar checks.

Redis

  • Never pass user data into EVAL, EVALSHA, FUNCTION, CONFIG, MODULE, or DEBUG.
  • Prefer high-level client APIs over raw send_command with interpolation.
  • Use ACLs to restrict command families per workload; disable dangerous commands entirely.

Search engines and document stores

  • OpenSearch/Elasticsearch query_string parses a mini-language: do not accept raw user queries unless it is the product feature and you have scoped roles. Prefer the DSL with typed fields.
  • Disable inline scripting where possible; restrict script sources and timeouts.
  • Solr: avoid json.query/stream.body parameters reachable from users; apply parameter allowlists.

OS Command Injection

| Language | Unsafe | Safe | |---|---|---| | Python | os.system, subprocess(..., shell=True) | subprocess.run([exe, *args], shell=False, check=True) | | Node | child_process.exec | execFile/spawn with args array, shell: false | | Go | exec.Command("sh", "-c", ...) | exec.Command(binary, args...) (never invokes a shell) | | Java | Runtime.exec(String) | new ProcessBuilder(List.of(exe, args...)) | | .NET | concat into FileName/Arguments | ProcessStartInfo.ArgumentList | | Ruby | backticks/system(str) | system(*argv), Open3.capture3(*argv) | | PHP | exec/shell_exec/system with interpolation | proc_open with array; escapeshellarg only as fallback | | Rust | sh -c with formatted string | Command::new(binary).args(args) |

Shell metacharacters (;, |, &, $(), backticks, newlines, <, >) are the visible half of the problem. The other half is argument injection: even without a shell, a value beginning with - can change the target program's behavior (for example scp -oProxyCommand=..., git --upload-pack, curl -o). Defenses:

  • Prefer a library over spawning a process at all. Many "need shell" cases are really missing library calls (archive extraction, image conversion, DNS lookup, video probing).
  • Use absolute binary paths from a fixed set; never let users choose the executable.
  • Insert -- before positional arguments when the tool supports it, and allowlist flags.
  • Pass a clean environment (env= / env_clear); strip PATH, LD_PRELOAD, BASH_ENV, IFS.
  • Validate every argument as if it were a query parameter: type, length, character allowlist.
import subprocess, re

NAME = re.compile(r"\A[a-zA-Z0-9_-]{1,64}\Z")

def probe(name: str) -> bytes:
    if not NAME.match(name):
        raise ValueError("invalid name")
    return subprocess.run(
        ["/usr/bin/ffprobe", "-v", "quiet", "--", name],
        shell=False, capture_output=True, check=True, timeout=5,
    ).stdout

Template Injection (SSTI)

Server-side template injection occurs when user input is compiled as a template instead of passed as data. Every mainstream engine (Jinja2, Twig, Freemarker, Velocity, Thymeleaf, Handlebars, EJS, Pug, Razor) evaluates expressions, and sandbox escapes are discovered regularly.

  • Default: render("page.html", content=user_text), never render_string(user_text).
  • Rich text is an output-encoding problem, not a template problem (see ./02-xss-csrf.md).
  • If user-authored templates are a product requirement: use a logic-less engine (Mustache), restrict the object model to plain data, render in a separate unprivileged process with CPU, memory, and wall-clock limits, and never expose imports, reflection, or filesystem helpers.
  • Expression languages in config and logs are the same hazard: SpEL, OGNL, MVEL, Groovy, JEXL, and logging lookups (including the Log4Shell pattern) must never evaluate user input.
  • Client-side, eval, new Function, and string-based setTimeout are template injection by another name; treat them as XSS sinks.

LDAP and XPath

LDAP filters have their own grammar. Authentication code like (&(uid=USER)(password=PASS)) is bypassable with input such as *)(uid=* — and embedding a password in a filter is wrong even when escaped.

  • Escape filter values per RFC 4515 (\, *, (, ), NUL as \5c, \2a, \28, \29, \00).
  • Escape distinguished names per RFC 4514 (,, +, ", \, <, >, ;, #, leading/trailing spaces, leading #).
  • Prefer a library filter builder; for authentication, do a search by identifier then a bind as the found DN, never a filter containing the password.
  • Treat wildcard expansion as a feature you opt into, not a default.
  • XPath has no bindings in most APIs: escape quotes and use single-quoted literals, or use the engine's variable API when available; prefer DOM traversal over string-built XPath for access control.
import ldap.filter
safe = ldap.filter.escape_filter_chars(user_input)   # escape filter values
dn = ldap.dn.escape_dn_chars(uid)                    # escape DN components

Adjacent Injection Classes

  • CRLF/header injection — strip \r and \n (and encoded variants) from values placed into response headers, redirects, and emails.
  • Log injection / log forging — encode or strip newlines and control characters before logging user data; prefer structured logging (JSON) so data stays in fields.
  • Regex injection / ReDoS — do not build regexes from user input; if you must, escape metacharacters, bound length, and enforce timeouts (see ./05-validation-encoding.md).
  • CSV formula injection — values beginning with =, +, -, @, tab, or CR must be prefixed or quoted before export (see ./05-validation-encoding.md).
  • Serialization injection — covered in ./03-ssrf-deserialization.md.

Detection and Tests

  • SAST/grep for formatting APIs near interpreter calls: f", %, +, format(, String.format, template literals adjacent to execute, query, exec, system, render, compile.
  • Unit tests with hostile payloads per sink: ' OR 1=1--, 1; DROP TABLE, $(id), `id`, {{7*7}}, ${7*7}, *)(uid=*, ..%2f, \u0000.
  • Integration tests that assert the DB role cannot run DDL and the process user cannot write outside its work directory.
  • Error-path tests: force failures and assert no SQL, paths, or stack traces reach the response.

Anti-Patterns

  • "We escape quotes, so f-strings are fine" — escaping without a defined context is not escaping.
  • Building IN (?) clauses by joining user strings instead of expanding bound placeholders.
  • ORDER BY with validated "words" — validation must be an allowlist of known column names.
  • Sanitizing on input in one place and concatenating later (second-order) in another.
  • Trusting an ORM: raw fragments, literal(), and string-based find filters bypass its safety.
  • Disabling server-side prepared statements to "improve performance" while concatenating values.
  • Running the app's DB user with DDL rights "for migrations"; migrations get their own credential.
  • Logging the full query with values on error, leaking secrets and user data.

Checklist

  • [ ] Every SQL/NoSQL/ORM call binds values; every raw escape hatch is reviewed and justified.
  • [ ] Identifiers, sort keys, directions, and operators come from constant allowlists.
  • [ ] No shell invocation with user data; argv arrays, absolute binaries, --, clean env.
  • [ ] No user input compiled as a template or expression; logic-less engine if unavoidable.
  • [ ] LDAP filter and DN values are escaped or built by a library; no password-in-filter auth.
  • [ ] Interpreter credentials are least-privilege; timeouts and row limits are set.
  • [ ] Errors are generic to clients and detailed only in protected logs.
  • [ ] Regression tests exist for each fixed injection path, with the original payload.
02-xss-csrf

XSS and CSRF

Scope: preventing script execution and cross-site request forgery in browser-facing code through context-aware output encoding, sink discipline, Trusted Types, CSP, cookies, CSRF tokens, and CORS.

Threat Model

The browser is a hostile execution environment: any script running on the origin has full access to the DOM, storage, and authenticated requests to that origin. XSS is the failure to keep attacker-controlled content from becoming code; CSRF is the failure to distinguish a request the user intended from one another site caused the browser to send. Both are authorization problems at heart, so no client-side check is ever sufficient. For platform-level browser security depth, see ../../web/SKILL.md.

Never trust: URL parameters, form fields, localStorage, postMessage payloads, DOM text, third-party API responses, or unverified JWT claims. Always enforce authorization server-side on every request.

Output Encoding by Context

Encode at the point of output, with the encoder for that exact context. One value often needs several encodings in one document (for example a URL inside an HTML attribute inside JavaScript).

| Context | Encoding | Notes | |---|---|---| | HTML text | HTML entity encoding (& < > " ') | Framework templates do this by default | | HTML attribute | Attribute encoding plus quoted values | Always quote; never write unquoted attributes | | URL / query parameter | Percent-encoding | encodeURIComponent-equivalent; validate scheme | | JavaScript string | Script-safe encoding (\uXXXX, JSON serialize) | Never inject into script blocks if avoidable | | CSS | Validate against a value pattern; avoid expression/url() with user data | CSS injection can exfiltrate data | | JSON response | Proper JSON serializer with correct Content-Type | Avoid HTML sniffing with nosniff | | XML | XML entity encoding | Avoid building XML by concatenation | | Shell/SQL/LDAP | Parameterization, not encoding | See ./01-injection.md | | CSV | Quote fields; neutralize leading = + - @ | Prevents spreadsheet formula injection | | HTTP headers | Strip/encode CR/LF | Prevents response splitting and header forgery |

Framework auto-escaping is the primary defense; do not fight it. Explicitly unescaped output is where bugs concentrate.

Dangerous Sinks

| Sink | Language / framework | Safe alternative | |---|---|---| | innerHTML, outerHTML, insertAdjacentHTML, document.write | DOM | textContent, createElement, setAttribute | | dangerouslySetInnerHTML | React | Render text; sanitize if HTML is required | | v-html | Vue | Interpolation; sanitize for rich text | | [innerHTML], bypassSecurityTrustHtml | Angular | Standard binding; avoid bypass APIs | | {@html ...} | Svelte | Text interpolation | | eval, new Function, string setTimeout | JS | Parse data, never execute it | | jQuery .html(), .append() with strings | jQuery | .text(), DOM nodes | | element.setAttribute("onclick", ...) | DOM | addEventListener | | src/href with user data | DOM | Validate scheme (https:, mailto:); block javascript:, data: | | WebView loadDataWithBaseURL / similar | Mobile | Escape or load from trusted origin |

DOM XSS sources include location, document.referrer, window.name, postMessage events, and storage. Trace them to sinks; frameworks do not protect DOM flows the same way they protect server-rendered templates.

Rich Text and Sanitization

If the product requires HTML from users:

  • Sanitize with a maintained allowlist library (DOMPurify or equivalent) and pin it; sanitizer bypasses are a recurring CVE class.
  • Prefer sanitizing on the server when content is rendered server-side, or sanitize at the last step before insertion in the client; do not trust previously-sanitized storage blindly.
  • Allowlist tags and attributes; drop script, style, iframe, event handlers, and javascript: URLs. Consider rel="noopener noreferrer" on user links and target restrictions.
  • Keep a CSP as the backstop because sanitizers will occasionally fail.

Trusted Types

Trusted Types turns DOM XSS sinks into runtime errors unless the value passes a policy. It is the strongest available defense for DOM-based XSS in Chromium-based browsers and should be adopted with a fallback report-only phase.

Content-Security-Policy:
  require-trusted-types-for 'script';
  trusted-types default dompurify;
  • Roll out in report-only first, collect violations, fix sinks, then enforce.
  • Define narrow policies; do not create a passthrough policy (value => value) that nullifies the protection.
  • Use the DOMPurify adapter policy rather than hand-rolled HTML building.
  • Treat trusted-types enforcement as progressive enhancement; other engines ignore the directive. Verify current browser support upstream before making it a hard requirement.

Content Security Policy

CSP is defense in depth and the main mitigation for exfiltration via injected script. Start report-only, iterate, enforce. For the full browser treatment see ../../web/SKILL.md.

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-RANDOM' 'strict-dynamic';
  style-src 'self';
  img-src 'self' data: https://cdn.example.com;
  font-src 'self';
  connect-src 'self' https://api.example.com;
  frame-ancestors 'none';
  base-uri 'none';
  form-action 'self';
  object-src 'none';
  upgrade-insecure-requests;
  report-to csp-endpoint
  • Prefer per-response nonces (or hashes for static inline) over allowlists; 'strict-dynamic' removes the need to enumerate script hosts.
  • Never ship 'unsafe-eval'; avoid 'unsafe-inline' in script-src. If inline styles are unavoidable during migration, isolate them in style-src and plan removal.
  • Always set base-uri, form-action, object-src, and frame-ancestors; they close entire attack classes.
  • Use report-to (with Report-To/Reporting-Endpoints) to collect violations before enforcing.
  • CSP does not stop exfiltration to an allowed endpoint; restrict connect-src and validate data server-side.

Security Headers Baseline

Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Content-Security-Policy: <see above>
Permissions-Policy: geolocation=(), camera=(), microphone=(), payment=(self)
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp   # only when crossOriginIsolated is needed

HSTS preload is effectively permanent; confirm every subdomain is HTTPS first. COOP/COEP enable crossOriginIsolated but break cross-origin embeds unless they opt in. Set headers at the edge as well as the origin so cached and error responses cannot omit them.

Cookies

Set-Cookie: __Host-session=...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=...

| Attribute | Rule | |---|---| | HttpOnly | Always for session/auth cookies; blocks theft via XSS | | Secure | Always; never send auth cookies over plain HTTP | | SameSite | Lax default; Strict for sensitive flows; None only with Secure for genuine cross-site needs | | Path | Tightest practical; session cookies commonly / | | Max-Age/Expires | Session cookies for auth; long-lived only for non-sensitive preferences | | Prefix | __Host- for session cookies (requires Secure, Path=/, no Domain); __Secure- otherwise |

  • Keep tokens out of localStorage/sessionStorage; prefer HttpOnly cookies plus CSRF defense.
  • Rotate the session identifier on login, logout, and privilege change; invalidate server-side.
  • Consider Partitioned (CHIPS) only for real embedded cross-site contexts; unsupported browsers ignore it, so test the embed flow.
  • Clear cookies server-side on logout (Max-Age=0) and delete the session record.

CSRF

CSRF exists wherever the browser attaches credentials automatically (cookies, HTTP auth, mTLS, IP-based auth). Token-based APIs with Authorization headers are naturally resistant if tokens are not also accepted from cookies.

| Defense | Strength | Notes | |---|---|---| | Synchronizer token | Strong | Per-session or per-request token, compared server-side, tied to the session | | Double-submit cookie | Medium | Only if the cookie is signed/HMACed; naive double-submit is bypassable via subdomains | | SameSite=Lax/Strict | Strong baseline | Not all flows tolerate Strict; None removes the protection | | Origin/Referer check | Good secondary | Compare to allowlist; fail closed when absent on state-changing requests | | Fetch Metadata (Sec-Fetch-Site, Sec-Fetch-Mode) | Good secondary | Reject cross-site on non-GET APIs | | Custom header requirement | Good for SPAs | Forces preflight; pairs with CORS | | Re-authentication / step-up | Strong for sensitive actions | Password change, email change, payments, key management |

Implementation rules:

  • Protect every state-changing method (POST, PUT, PATCH, DELETE) and state-changing GETs (which should not exist).
  • Bind the token to the authenticated session, not just to the browser; rotate on login.
  • Keep tokens out of URLs, logs, and Referer leakage; put them in a header or form field.
  • Login and logout endpoints need CSRF protection too (login CSRF).
  • JSON APIs are not automatically safe: a cross-site HTML form can send text/plain bodies that some parsers accept; enforce Content-Type and prefer token or header requirements.
  • Do not disable CSRF middleware for "internal" routes; if a route is truly token-authenticated, document why it is exempt and test the exemption.

CORS Interactions

CORS controls who may read responses, not who may send requests; it is not CSRF protection.

| Misconfiguration | Risk | Fix | |---|---|---| | Access-Control-Allow-Origin: * with credentials | Browsers block it, but developers often work around it | Never; use explicit origin allowlist | | Reflecting the Origin header | Any site reads authenticated data | Compare against a static allowlist | | null origin allowed | Sandboxed iframes and data: URIs bypass | Reject null unless a documented need | | Partial origin matching (endsWith("example.com")) | evil-example.com passes | Exact string match after parsing | | Access-Control-Allow-Credentials: true on public APIs | Widens impact of XSS/CSRF | Only where credentials are truly needed | | Long Access-Control-Max-Age with broad methods | Stale permissive policy | Keep short; re-evaluate on change | | Missing Vary: Origin | Cache serves one origin's policy to another | Always set when reflecting origin |

Design rules: preflight is a server-side authorization checkpoint, but browsers only preflight non-simple requests; treat preflight as defense in depth, not control. Keep the API origin separate from the app origin, and be explicit about which origins may use credentials.

Tests and Verification

  • Unit/integration tests: inject <script>, <img onerror>, javascript: URLs, </script> and assert the output is inert in the rendered context.
  • Use browser automation with a CSP report collector to catch policy violations in CI.
  • Test cookie flags and Set-Cookie output in integration tests; assert __Host- usage.
  • Test CSRF: state-changing request without token, with a foreign origin, and with SameSite=None cookies must fail; assert exemptions are explicitly listed.
  • Test CORS with a hostile Origin and with null; assert the response omits the CORS headers.

Anti-Patterns

  • Writing a single escapeHtml() and reusing it in attributes, URLs, and JavaScript contexts.
  • Sanitizing on input and assuming the stored value is safe forever.
  • Allowing javascript: and data: schemes in user-controlled links.
  • Building HTML strings in JavaScript and inserting them with innerHTML.
  • Treating CSP as an alternative to encoding instead of a backstop.
  • Using SameSite=None to fix an embed and silently reopening CSRF.
  • Reflecting Origin into CORS headers "so the frontend works".
  • Access-Control-Allow-Credentials: true with a wildcard or permissive origin list.

Checklist

  • [ ] Every output context uses the matching encoder; no unescaped interpolations anywhere.
  • [ ] Dangerous sinks are absent or fed only sanitized, allowlisted HTML.
  • [ ] Trusted Types (report-only, then enforced) and a nonce-based CSP are deployed.
  • [ ] Security headers and cookie attributes are set at origin and edge; tests assert them.
  • [ ] CSRF protection covers all state-changing routes, including login/logout; exemptions documented.
  • [ ] CORS uses an exact origin allowlist with Vary: Origin; credentials only where required.
  • [ ] Session identifiers rotate on authentication and privilege changes.
  • [ ] Regression tests include XSS payloads, cookie flag assertions, and hostile-origin requests.
03-ssrf-deserialization

SSRF and Deserialization

Scope: server-side request forgery defenses and safe handling of serialized data across trust boundaries, with per-language unsafe APIs and migration paths.

SSRF: Why It Happens

Any feature where the server fetches a URL chosen by a user is SSRF surface: webhooks, URL previews, image proxies, PDF generators, RSS importers, file importers, analytics pixels, package installers, CI runners, and "test connection" buttons. The attacker does not need the response back for impact: blind SSRF can still hit internal APIs, cloud metadata, and admin panels.

The root cause is treating a URL as a destination when it is actually attacker-controlled input that controls network access.

Address-Level Defenses

| Target class | Examples | Rule | |---|---|---| | Loopback | 127.0.0.0/8, ::1 | Block unless explicitly required | | Private ranges | 10/8, 172.16/12, 192.168/16 | Block by default | | Link-local | 169.254.0.0/16, fe80::/10 | Block; includes cloud metadata | | Metadata | 169.254.169.254, fd00:ec2::254, GCP/Azure equivalents | Block and require IMDSv2/hop limit | | CGNAT | 100.64.0.0/10 | Block | | Unique local | fc00::/7 | Block | | Wildcard / unspecified | 0.0.0.0, :: | Block | | Public unicast | Everything else | Allow only through an egress allowlist |

Do not try to parse hostnames with regex. Resolve the name, then validate every returned address, including IPv4-mapped IPv6 (::ffff:127.0.0.1) and alternate encodings (decimal, octal, hex, shorthand like 127.1). Normalize the URL with one parser, then extract and validate with the same parser; parser differentials between validators and HTTP clients are a classic bypass.

URL Validation Algorithm

  1. Parse with the platform URL parser; reject unparsable input.
  2. Allowlist schemes: https (and http only when required and constrained). Reject file, gopher, ftp, dict, ldap, unix, jar, and scheme-less forms.
  3. Reject credentials in the URL (user:pass@host) unless you explicitly support them.
  4. Resolve the hostname; reject if any address is in a blocked range. Pin the resolved IP and connect to that IP with the hostname in SNI/Host, so DNS cannot rebind between check and connect.
  5. Optionally require the host to match an allowlist of domains for sensitive features.
  6. Cap response size, redirects, and total time.
  7. Disable or re-validate redirects: either do not follow them, or resolve and validate each hop, with a hard limit (for example 3). A public host can redirect to 169.254.169.254.
  8. Connect with a client that does not honor proxy environment variables, and strip headers that leak credentials (Authorization, Cookie, X-Forwarded-*, internal Host).
import ipaddress, socket
from urllib.parse import urlparse

BLOCKED = [
    ipaddress.ip_network(n) for n in (
        "0.0.0.0/8", "10.0.0.0/8", "100.64.0.0/10", "127.0.0.0/8",
        "169.254.0.0/16", "172.16.0.0/12", "192.168.0.0/16",
        "::1/128", "fc00::/7", "fe80::/10",
    )
]

def resolve_and_check(url: str) -> str:
    parsed = urlparse(url)
    if parsed.scheme not in {"https", "http"} or not parsed.hostname:
        raise ValueError("scheme not allowed")
    infos = socket.getaddrinfo(parsed.hostname, parsed.port or 443, proto=socket.IPPROTO_TCP)
    ips = {info[4][0] for info in infos}
    for raw in ips:
        ip = ipaddress.ip_address(raw)
        if ip.ipv4_mapped:
            ip = ip.ipv4_mapped
        if any(ip in net for net in BLOCKED):
            raise ValueError("address not allowed")
    return sorted(ips)[0]   # pin this IP in the HTTP client connection

This sketch is deliberately incomplete: production code must also handle the connection phase (Host header, TLS server_hostname, no proxy inheritance), redirects, and response limits.

Cloud Metadata Protections

  • Enforce IMDSv2 (session-token-required) wherever the provider offers it, and set the hop limit to 1 so container workloads cannot reach the metadata service.
  • Prefer workload identity (OIDC federation, IRSA, GKE Workload Identity, managed identity) over instance credentials so metadata access has no value.
  • Add network policy/egress firewall rules blocking 169.254.169.254 and link-local ranges from application namespaces.
  • Azure and GCP metadata endpoints require specific headers; do not let user input set headers.

Egress Architecture

The most reliable SSRF defense is network-level: the application cannot reach anything except an explicit allowlist through a forward proxy.

| Layer | Control | |---|---| | Application | URL validation, IP pinning, scheme allowlist, response caps | | Forward proxy | Allowlist of destinations, TLS inspection where appropriate, no proxy auth leakage | | Network | Egress rules denying RFC1918, link-local, and metadata; DNS via controlled resolver | | Identity | No ambient instance credentials reachable from app namespaces |

Blind SSRF detection: monitor outbound requests to unusual destinations, DNS queries to internal names, and timing differences; log which user or feature initiated each outbound fetch. Cap outbound request rates per account to slow scanning.

SSRF in non-HTTP protocols counts too: gopher and dict can speak to Redis/Memcached, file can read local files, and some libraries follow smb:// or ldap://. Scheme allowlisting is the fix.

Deserialization: The Rule

Never deserialize native object formats from untrusted input. The safe boundary is: bytes from untrusted source -> parse with a schema-aware, non-executable format -> validate fields.

| Language | Unsafe APIs | Safe alternatives | |---|---|---| | Java | ObjectInputStream, readObject, XStream, SnakeYAML load, Fastjson autotype, Kryo, Jackson enableDefaultTyping | JSON with a fixed DTO; PolymorphicTypeValidator if polymorphism is required; SnakeYAML SafeConstructor | | Python | pickle, dill, marshal, yaml.load (default loader), eval of literals | json, yaml.safe_load, pydantic for validation | | Ruby | Marshal.load, YAML.unsafe_load, Oj with object mode | JSON.parse with explicit schema handling | | PHP | unserialize without allowed_classes | json_decode; unserialize(..., ['allowed_classes' => false]) | | .NET | BinaryFormatter, NetDataContractSerializer, LosFormatter, TypeNameHandling.All/Objects | System.Text.Json, DataContractSerializer with known types | | Node | node-serialize, serialize-javascript evaluated, vm as a sandbox | JSON.parse with schema validation; node:vm is not a security boundary | | Go | encoding/gob across trust boundaries, yaml.Unmarshal into interface{} | encoding/json into typed structs; validate with a schema | | Rust | Serde into Value then unchecked casts; bincode across trust boundaries | Typed structs, deny_unknown_fields, schema validation |

Even "safe" formats need care: huge payloads cause memory exhaustion, deeply nested JSON causes stack exhaustion, duplicate keys and type confusion break naive validation, and JSON numbers lose precision (money, IDs). Apply size limits, depth limits, strict schemas, and explicit numeric handling.

Signed State and Tokens

When state must round-trip through the client (cart, filters, OAuth state, signed cookies):

  • Serialize to a canonical JSON (or CBOR) representation with a schema version.
  • Sign with a modern MAC or signature over the exact bytes (HMAC-SHA-256 with a keyed secret, or Ed25519); include a version, issue time, and expiry inside the signed payload.
  • Verify before parsing into domain objects; reject unknown versions rather than guessing.
  • Keep keys out of the token; rotate with a kid and accept an overlap window.
  • Never use encryption as integrity: encrypt-then-MAC or AEAD only (see ./04-crypto-secrets.md). Do not trust encrypted-but-unsigned payloads; attackers can often manipulate them.

Deserialization Gadget Chains

Java and .NET deserialization bugs become remote code execution through gadget chains available on the classpath (commons-collections, Spring, Groovy, and countless others). You cannot enumerate all gadgets, so do not accept native serialized data at all.

  • Remove the capability: delete ObjectInputStream usage, disable Java serialization endpoints, and block JNDI lookups where the runtime allows.
  • If legacy protocols require Java serialization internally, apply a strict object-input filter allowlist (jdk.serialFilter), authenticate the sender, and keep the protocol off untrusted networks.
  • Audit libraries that deserialize implicitly: session stores, cache values, message queues, RMI, JMX, RPC frameworks, and view-state (ASP.NET ViewState needs MAC enforcement).
  • Jackson: never enable default typing globally; use a PolymorphicTypeValidator with an explicit allowlist, or avoid polymorphic types in API-facing DTOs.

Migration Notes

  • Replace pickle/YAML-unsafe/Java-serialization session formats with JSON plus HMAC (or server-side sessions) behind a feature flag; invalidate old tokens at cutover.
  • For internal RPC, move from Java serialization or .NET BinaryFormatter to protobuf/gRPC, which do not construct arbitrary objects.
  • Treat cache poisoning via deserialization as an incident class: purge caches when changing formats.
  • Add a deserialization lint rule (for example ban pickle.loads, Marshal.load, BinaryFormatter) and a test that feeds each endpoint malformed and oversized payloads.

Anti-Patterns

  • Denylisting hostnames (localhost, metadata.google.internal) instead of validating resolved IPs.
  • Validating a URL, then passing the original string to the HTTP client (TOCTOU with DNS rebinding).
  • Following redirects without re-validating each hop.
  • Allowing user-controlled headers (X-Forwarded-Host, Host, custom auth) on outbound fetches.
  • Relying on node:vm, a regex, or a WAF as a deserialization sandbox.
  • Signing data with a symmetric secret but not verifying before parsing.
  • Storing a serialized object in a cookie or hidden field because "the user cannot modify it".

Checklist

  • [ ] Every outbound fetch from user input validates scheme, resolved IP, and redirects; the resolved IP is pinned for the connection.
  • [ ] Egress firewall blocks metadata and private ranges; workload identity replaces instance keys.
  • [ ] Response size, timeout, and redirect count are capped; per-user outbound rate limits exist.
  • [ ] No native deserialization (pickle, ObjectInputStream, Marshal, BinaryFormatter, unserialize, unsafe YAML) on untrusted input; a lint rule enforces it.
  • [ ] Client-round-tripped state is signed, versioned, expires, and is schema-validated before use.
  • [ ] JSON parser has size and depth limits; validation rejects unknown/duplicate fields.
  • [ ] Tests cover redirect-to-metadata, DNS rebinding, oversized payloads, and malformed state.
04-crypto-secrets

Cryptography and Secrets

Scope: choosing and using cryptographic primitives correctly, generating random values, storing keys, and managing secrets across their life cycle.

Non-Negotiable Rules

  1. Never design your own primitives, modes, or protocols. Use vetted libraries (libsodium, Tink, OpenSSL/BoringSSL, Go crypto, WebCrypto, the platform's crypto framework).
  2. Encrypt with AEAD only (AES-GCM, ChaCha20-Poly1305, XChaCha20-Poly1305, AES-GCM-SIV) or use an encrypt-then-MAC composition from a library that provides it.
  3. A nonce must never repeat under the same key. Prefer a counter or libsodium's random nonces; with AES-GCM and random 96-bit nonces, rotate keys well before the birthday bound (millions of messages) or use a misuse-resistant construction.
  4. Never use a password, passphrase, or user identifier as a key. Derive keys (HKDF) or hash passwords (Argon2id).
  5. All randomness that affects security comes from a CSPRNG. Never Math.random, rand() unseeded, mt_rand, timestamps, UUIDv1, or counters.
  6. Compare secrets, MACs, and tokens in constant time. Early-exit == leaks information.
  7. Keys and secrets live in a KMS/HSM or secrets manager, never in source, images, logs, or client bundles. Design for rotation from day one.

Primitive Selection

| Need | Primitive | Parameters / notes | |---|---|---| | Password storage | Argon2id | Tune to ~50-100 ms+; OWASP-era baseline: m=19 MiB, t=2, p=1, increase as hardware allows | | Password storage (fallback) | scrypt, bcrypt | scrypt N=2^17, r=8, p=1; bcrypt cost >= 10-12 | | Fast integrity / hashing | SHA-256, SHA-3, BLAKE3 | Not for passwords; not a MAC | | Legacy compatibility only | SHA-1, MD5 | Never in new designs; MD5 is collision-broken, SHA-1 too | | Symmetric encryption | AES-256-GCM, ChaCha20-Poly1305 | 96-bit nonce for GCM, 192-bit for ChaCha20-Poly1305 | | Large data / streams | XChaCha20-Poly1305, AES-GCM with counter nonces | Segment with unique nonces per chunk | | HMAC | HMAC-SHA-256 (or keyed BLAKE3) | For tokens and webhook signatures | | Key derivation | HKDF (RFC 5869) | Separate keys per purpose via info | | Password-based KDF | Argon2id | Never PBKDF2 below current guidance; if required, >= 600k iterations SHA-256 | | Signatures | Ed25519, ECDSA P-256/P-384, RSA-PSS >= 3072 | RSA PKCS#1 v1.5 only for existing protocols | | Key agreement | X25519, ECDH P-256+ | Inside TLS 1.3 or Noise; not hand-rolled | | TLS | 1.3 preferred, 1.2 minimum with modern suites | Disable renegotiation and legacy ciphers | | Post-quantum readiness | Hybrid X25519+ML-KEM (FIPS 203) where supported, ML-DSA/SLH-DSA for signatures | Crypto agility now; verify library and protocol support upstream |

Deprecated advice to refuse: SHA-256 for passwords, AES-CBC without MAC, ECB anywhere, openssl enc with a password, static IVs, "encrypt and then base64 means safe", custom token formats, JWT none or HMAC algorithms with public keys.

Password Hashing

from argon2 import PasswordHasher

ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=1)
digest = ph.hash(password)                    # store this string (embeds params+salt)
ph.verify(digest, candidate)                  # raises on mismatch
if ph.check_needs_rehash(digest):
    store(ph.hash(candidate))                 # upgrade transparently on login
  • Hash on the server, in the login path, with a unique salt per password (libraries handle this).
  • Rate-limit and add a per-account delay or lockout; hashing does not stop online guessing.
  • On password change, rehash; on parameter upgrades, rehash on next successful login.
  • If you must support multiple algorithms during migration, store the algorithm and parameters with the digest and upgrade lazily.
  • Do not impose composition rules (uppercase/symbol quotas); enforce a length floor (for example 12+), check against breached-password lists, and allow long passphrases and paste.
  • Password reset tokens: >= 128 bits of CSPRNG, single use, short TTL, hashed at rest, invalidated on use or on password change.

AEAD Usage

import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

key = AESGCM.generate_key(bit_length=256)     # in practice: from a KMS, not ad hoc
aead = AESGCM(key)
nonce = os.urandom(12)                        # unique per encryption under this key
ct = aead.encrypt(nonce, plaintext, associated_data=b"tenant:42:v1")
pt = aead.decrypt(nonce, ct, associated_data=b"tenant:42:v1")
  • Bind context with associated data (tenant, user, purpose, version) so ciphertexts cannot be replayed into another context.
  • Store version || nonce || ciphertext || tag with an explicit format; never rely on implicit framing.
  • Rotate the key before nonce exhaustion; never reset a counter when restarting a process.
  • Do not use the same key for encryption and signing; derive separate keys with HKDF.
  • On decryption failure, fail closed and do not reveal whether the tag or padding failed.

Secure Randomness

| Language | Use | Never | |---|---|---| | Python | secrets.token_urlsafe, secrets.token_bytes, os.urandom | random, uuid.uuid1() | | Node | crypto.randomBytes, crypto.randomUUID (v4), crypto.getRandomValues | Math.random | | Go | crypto/rand | math/rand without rand/v2 crypto | | Rust | rand::rngs::OsRng, getrandom | fastrand, thread_rng for secrets | | Java | SecureRandom (default provider) | java.util.Random, Math.random | | .NET | RandomNumberGenerator.GetBytes, RandomNumberGenerator.GetInt32 | Random, Guid.NewGuid() for secrets | | C/C++ | getrandom(2) / BCryptGenRandom / SecRandomCopyBytes | rand, srand(time(NULL)) |

For tokens: at least 128 bits of entropy (256 for long-lived or high-value); encode URL-safe; store hashed if the token is a bearer credential; bind to a purpose and expiry. randomUUID from crypto is fine for identifiers, but UUIDv1 (time/MAC-based) and predictable IDs are not secrets.

Constant-Time Comparison

// Go: wrong vs right
if subtle.ConstantTimeCompare([]byte(got), []byte(want)) != 1 { reject() }   // right
  • Python: hmac.compare_digest; Node: crypto.timingSafeEqual; Rust: subtle::ConstantTimeEq; Java: MessageDigest.isEqual (constant-time for equal lengths); .NET: CryptographicOperations.FixedTimeEquals.
  • Beware length leaks: constant-time compare of unequal lengths still reveals length. Hash both sides to a fixed size first when length itself is sensitive.
  • Do not branch on secret bytes: no if (mac[0] == ...) checks, no early return on first mismatch, no lookup tables indexed by secrets.

Key Management

| Layer | Practice | |---|---| | Root key | HSM/KMS, never exported, dual control for destructive actions | | Data keys | Envelope encryption: KMS encrypts a DEK, DEK encrypts data; cache short TTL | | Per-tenant keys | Separate key material per tenant where data-residency or isolation demands | | Key versions | kid in ciphertext metadata; old versions decrypt-only during rotation | | Rotation | Scheduled (for example <= 12 months for root, sooner for high-volume keys) and event-driven on exposure | | Access | IAM/KMS policies per workload; audit every decrypt/encrypt; deny by default | | Dev/test | Separate keys and accounts; no production key material in lower environments |

Rotation procedure that works:

  1. Generate the new key version; publish it alongside the old.
  2. Write new data with the new version; tag all ciphertexts with their version.
  3. Re-encrypt old data lazily or in a batch job; track progress.
  4. After a defined window with zero old-key reads, disable and then destroy the old version.
  5. Document the rollback window; destroying a key is irreversible.

Secret Management

| Environment | Storage | Notes | |---|---|---| | Developer laptop | .env gitignored, direnv, mock values | Never real production secrets | | CI/CD | OIDC federation to the cloud, masked variables | Prefer short-lived tokens over static keys | | Runtime | Secrets manager / KMS, mounted or injected at start | No secrets in env when avoidable; env leaks via crash dumps and child processes | | Kubernetes | External Secrets Operator, CSI secrets store, sealed secrets | Encryption at rest alone is insufficient; RBAC is the boundary | | Build artifacts | None | Images must be reproducible without embedded secrets |

Life cycle rules:

  • Inventory every secret: owner, purpose, storage, expiry, rotation method, consumers.
  • Prefer identity over keys: workload identity, mTLS, SPIFFE, short-lived cloud credentials.
  • Scan repositories and CI logs for secrets (gitleaks, TruffleHog, provider-native scanning) and treat any historical commit as compromised.
  • Never log credentials, tokens, cookies, or full request bodies; redact at the logging layer and test the redaction.
  • On exposure: revoke first, then rotate, then audit usage, then fix the root cause. Assume exfiltration for any secret committed to a public repository.
  • Limit blast radius: per-service credentials, per-purpose keys, least-privilege scopes, and egress restrictions so stolen secrets are less useful.

Common Misuse Catalog

| Mistake | Consequence | Fix | |---|---|---| | AES-CBC without authentication | Padding-oracle decryption, tampering | AEAD or encrypt-then-MAC | | Reused nonce/IV | Keystream reuse in CTR/GCM, forgery | Counter or random with rotation policy | | Static IV stored with data | Deterministic ciphertext, equality leaks | Fresh random nonce per message | | Password as encryption key | Offline brute force of all data | Argon2id-derived key via a KMS | | == on MACs/tokens | Timing oracle | Constant-time compare | | Custom JWT parsing without signature verification | Full auth bypass | Verify signature, alg, iss, aud, exp with a maintained library | | Accepting alg: none or HMAC with a public key | Token forgery | Pin allowed algorithms server-side | | Self-signed/disabled TLS verification | MITM | Verify chains; pin where the threat model requires | | Random tokens from Math.random | Predictable sessions/resets | CSPRNG, >= 128 bits | | Secrets in client bundles or mobile apps | Public disclosure | Never ship secrets; proxy server-side |

Checklist

  • [ ] Passwords use Argon2id (or scrypt/bcrypt) with tuned params and transparent rehash.
  • [ ] All encryption is AEAD; nonces are unique; ciphertexts carry version/key id context.
  • [ ] Keys come from KMS/HSM; envelope encryption; rotation is scheduled and rehearsed.
  • [ ] CSPRNG is used for every token, session, nonce, and secret; no Math.random/rand().
  • [ ] Secret comparisons are constant-time; no branching on secret bytes.
  • [ ] Secrets are absent from git history, images, logs, and client code; scanning runs in CI.
  • [ ] Token formats use maintained libraries with algorithm allowlists and full claim validation.
  • [ ] Tests cover tampered ciphertext, wrong key/nonce reuse detection, and token tampering.
05-validation-encoding

Validation and Encoding

Scope: input validation strategy, schema validation, canonicalization, file uploads, path traversal, and output encoding for every context.

Validation Strategy

Validation is a boundary concern: it decides what data is allowed to enter a component. It is not a substitute for output encoding, authorization, or parameterization, and it should happen as close to the trust boundary as possible.

  1. Define the trust boundary for each component: HTTP handlers, queue consumers, CLI entrypoints, scheduled jobs reading external data, webhook receivers, and file parsers.
  2. Validate every field for type, presence, length, range, format, and enum membership. Positive validation (allowlist) beats negative validation (denylist) in every case.
  3. Reject rather than repair. "Cleaning" input produces surprising data and hides attacks; if the value is wrong, return a structured error.
  4. Validate syntactic shape first, then semantic meaning (does this user exist, is this account number valid for this tenant, is this transition legal).
  5. Never trust client-side validation; it is UX only. Revalidate server-side, including on internal service calls and replays.
  6. Keep validation in one layer (DTO/schema/middleware) so tests cover it once and handlers receive typed, trusted values.
  7. Bound everything: body size, field count, nesting depth, string length, array length, numeric range, file count, decompressed size. Unbounded input is a denial-of-service primitive.

Schema validation by stack

| Stack | Library examples | Notes | |---|---|---| | Python | pydantic v2, attrs/cattrs, msgspec, marshmallow | Validate at the edge; use extra="forbid" | | TypeScript/Node | Zod, Valibot, ArkType, JSON Schema (Ajv) | Infer types from schemas; parse, do not cast | | Go | encoding/json + go-playground/validator, ozzo-validation | Struct tags plus explicit rules; distinguish zero values | | Rust | serde + deny_unknown_fields, garde, validator | Types encode most invariants; validate the rest | | Java/Kotlin | Bean Validation (Jakarta), JSON Schema validators | @Valid on controller DTOs; no entity binding from requests | | C/C++ | hand-rolled parsers + fuzzing | Prefer a small typed parser; fuzz every entrypoint |

Rules that outlive library choice:

  • Unknown fields are rejected or explicitly ignored by policy. Silent acceptance of extra fields enables mass assignment and smuggling.
  • Enums are closed sets; never accept "any string representing a role" and compare later.
  • Numeric inputs specify min/max and reject NaN/Infinity; parse integers from strings strictly.
  • IDs are validated as opaque values, then checked for authorization at the data layer.
  • Reject duplicate JSON keys or define deterministic behavior; parsers differ.
  • Validate the Content-Type and parse according to the declared format; do not sniff into other parsers on mismatch.

Canonicalization and Unicode

Canonicalize before validating or comparing. Validation on a non-canonical form can be bypassed by an equivalent representation that the later consumer normalizes differently.

| Step | Risk if skipped | |---|---| | Decode percent-encoding exactly once | Double-decoding bypasses path/URL filters | | Normalize Unicode (NFKC for identifiers) | Homoglyph and full-width bypasses | | Case-fold or lower-case for case-insensitive fields | ADMIN vs admin mismatches | | Normalize path separators and resolve ./.. | Path traversal and allowlist bypass | | Trim and normalize whitespace | Header/field smuggling, confusing comparisons | | Strip control characters where not allowed | CRLF/header/log injection, terminal escapes | | Reject invalid UTF-8 (do not silently replace) | Parser differentials, truncation attacks |

Encoding traps to remember: overlong UTF-8 sequences, percent-encoded nulls (%00), Unicode normalization forms (NFC/NFD/NFKC/NFKD), bidi control characters in displayed names, zero-width characters in identifiers and URLs, and mixed scripts in domains (IDN homograph attacks). Decide which characters are legal for each field and reject everything else.

File Uploads

File upload is the highest-risk input in most applications: it carries attacker bytes, attacker names, attacker content types, and often ends up parsed by native code.

| Step | Requirement | |---|---| | Size | Enforce max bytes at the edge and again in code; stream to disk, never fully into memory | | Count | Cap files per request and per account quota | | Name | Discard the client name; generate a random name and keep the original only as display metadata (escaped) | | Type | Determine by content sniffing (magic bytes) and/or decoding; treat Content-Type as a hint | | Content | Re-encode images/documents with a hard-broken pipeline; do not pass bytes through | | Storage | Store outside the web root or in object storage; never in a path derived from user input | | Serving | Serve with Content-Disposition: attachment, X-Content-Type-Options: nosniff, and a dedicated domain | | Execution | No execute permission; containers with read-only mounts; no shell-outs during validation | | Archive | Prevent zip-slip and decompression bombs; limit ratios, entry counts, and nesting | | Scanning | Malware scanning with quarantine; expose download only after the verdict | | Lifecycle | Expiry, cleanup jobs, and deletion on account removal |

import magic, secrets, pathlib

ALLOWED = {"image/png", "image/jpeg", "application/pdf"}
MAX_BYTES = 10 * 1024 * 1024
UPLOAD_ROOT = pathlib.Path("/var/app/uploads")

def store(upload) -> pathlib.Path:
    data = upload.read(MAX_BYTES + 1)
    if len(data) > MAX_BYTES:
        raise ValueError("too large")
    if magic.from_buffer(data[:4096], mime=True) not in ALLOWED:
        raise ValueError("type not allowed")
    dest = UPLOAD_ROOT / secrets.token_hex(16)     # never use the client filename
    dest.write_bytes(data)
    return dest

SVG and HTML are executable documents: sanitize or forbid them, and never serve user-uploaded HTML from the application origin. Image parsing libraries are a common RCE surface (ImageMagick policy, libvips, browser-side decoders); keep them patched, sandboxed, and configured with resource limits. For uploaded documents that will be previewed, use a conversion service with a hardened sandbox.

Path Traversal

Filename and path parameters are prime traversal targets. The only reliable pattern is: resolve the path, then verify it is inside an allowlisted base directory after symlink resolution.

import pathlib

BASE = pathlib.Path("/srv/data").resolve()

def open_user_file(name: str):
    if name != pathlib.PurePath(name).name or "\x00" in name:
        raise ValueError("invalid name")             # reject separators and nulls
    candidate = (BASE / name).resolve()              # resolves .. and symlinks
    if not candidate.is_relative_to(BASE):           # Python 3.9+
        raise ValueError("escape attempt")
    return candidate.open("rb")
  • Prefer opaque IDs mapped to paths in a database over user-supplied paths.
  • If paths are required, allowlist characters and extensions, reject absolute paths, and check both the raw and the decoded/canonicalized form.
  • Apply the check after symlink resolution; an in-base symlink can point anywhere.
  • Watch archive extraction: each entry name must pass the same check (zip-slip), and absolute entry names and .. segments must be rejected.
  • On Windows, handle drive letters, UNC paths, alternate data streams (:), and short-name (8.3) forms; case-insensitive comparisons apply.

Output Encoding Contexts

Encoding transforms data for one specific consumer. The same string may need multiple encodings in one response, in order (for example URL-encode, then HTML-attribute-encode).

| Context | Correct mechanism | Common error | |---|---|---| | HTML text | HTML entity encoding | Using URL encoding in HTML | | HTML attribute | Attribute encoding with quoted values | Forgetting quotes or using text encoding only | | URL component | Percent-encoding of each component | Encoding the whole URL including scheme/host | | JavaScript | JSON-encode + \u escaping; avoid inline script | Inserting into a script block unescaped | | CSS | Allowlist values; avoid user data in url()/expression | "CSS escaping" functions that are not | | SQL/NoSQL | Parameter binding | Manual quote doubling | | OS command | argv arrays, no shell | escapeshellarg with a shell still in play | | LDAP | RFC 4515 filter escaping / library builder | Quote doubling | | XML | Entity encoding; no DTD for untrusted docs | CDATA wrapping with ]]> inside | | JSON | Serializer with correct headers and nosniff | String concatenation | | CSV | Quote fields; neutralize leading = + - @ and control chars | Excel formula execution | | HTTP headers | Strip CR/LF; encode non-ASCII | Header/response splitting | | Logs | Structured fields; escape control chars | Log forging, secret leakage | | HTML rich text | Sanitizer allowlist (see ./02-xss-csrf.md) | Home-grown tag stripping |

Encoding must happen at the last possible moment before the data crosses the boundary, using the sink's own encoder. Double encoding (an encoder applied to already-encoded data) produces visible garbage and can be its own vulnerability when a downstream component decodes once more.

Error Handling and Limits

  • Return structured, generic errors to clients; keep detail in server logs with a correlation ID. Never echo stack traces, SQL, file paths, or framework versions.
  • Distinguish client errors (400/422) from authorization failures (403/404 by policy) without leaking object existence when that matters.
  • Timeouts for every external call and expensive operation; limits on regex complexity, JSON depth, XML entity expansion, and decompression ratio.
  • Fail closed: an error in an authorization or validation component must deny the request, not fall through to allow.

Tests

  • Table-driven invalid-input tests per endpoint: wrong types, empty, too long, too short, wrong charset, invalid UTF-8, duplicate keys, deeply nested JSON, huge arrays, NaN/Infinity.
  • Property-based tests for parsers and normalizers (see ./08-review-checklist.md).
  • Traversal tests with ..%2f, ..%5c, %252e%252e, absolute paths, nulls, and symlinked files.
  • Upload tests with mismatched extension/content, polyglot files, zip bombs, and huge files.
  • Encoding tests that render output and assert the browser/consumer sees inert data.
  • Resource-limit tests that assert timeouts and 413/429 behavior under oversized input.

Anti-Patterns

  • Validating with a denylist of "bad characters" and calling it sanitization.
  • Trusting Content-Type, file extension, or client-declared dimensions.
  • Concatenating paths and checking for ".." with a substring test.
  • Applying one generic escaper to HTML, URLs, and JavaScript alike.
  • Validating on the client and treating the server as already-validated.
  • Decoding input twice ("to be helpful") and validating only the first pass.
  • Accepting unknown JSON fields and binding them into entities (mass assignment).
  • Unbounded regexes over user input (ReDoS) and unbounded decompression.

Checklist

  • [ ] Every trust boundary has a schema with positive validation and unknown-field policy.
  • [ ] Size, count, depth, and numeric range limits are enforced before expensive work.
  • [ ] Canonicalization order is documented: decode, normalize, validate, then store.
  • [ ] Uploads are sniffed, re-encoded, randomly named, stored inert, and served safely.
  • [ ] Path parameters resolve inside a base directory after symlink resolution.
  • [ ] Every output sink uses the encoder for its context; encoding happens at the sink.
  • [ ] Errors are generic to clients, detailed in protected logs, and fail closed.
  • [ ] Tests include malformed encodings, traversal, polyglot uploads, and resource exhaustion.
06-per-language-cheatsheets

Per-Language Cheatsheets

Scope: sharp-edge secure-coding rules and safe-API mappings for Python, TypeScript/Node, Go, Rust, Java/Kotlin, and C/C++, with links to the sibling language packs for depth.

Generic Rules That Apply Everywhere

  • Use a typed, schema-validating boundary (DTOs) instead of passing request data through maps, dictionaries, or dynamic objects.
  • Parameterize every interpreter path; allowlist identifiers (see ./01-injection.md).
  • Import secrets from the environment or a secrets manager, never from source; scan commits in CI (see ./04-crypto-secrets.md and ./07-dependencies.md).
  • Wrap user data at the output sink with the context encoder; never trust a previous "clean" step.
  • Turn on the language's security linters and fail the build on new high-severity findings (see ./08-review-checklist.md).

Python

Depth pack: ../../python/SKILL.md (3.12-3.14 idioms, pydantic v2, SQLAlchemy 2.0, FastAPI hardening).

| Risk | Unsafe | Safe | |---|---|---| | SQL | f-string into execute | driver placeholders; SQLAlchemy bound params | | Command | os.system, shell=True | subprocess.run([...], shell=False, check=True) | | Deserialization | pickle.loads, yaml.load | json, yaml.safe_load, pydantic models | | XML | xml.etree default parser (XXE) | defusedxml; disable entities | | Randomness | random, uuid.uuid1 | secrets.token_urlsafe, os.urandom | | Comparison | == on tokens/MACs | hmac.compare_digest | | Paths | string concat with .. | pathlib.Path.resolve() + is_relative_to | | Templates | render_template_string(user) | render named templates with context data | | Hashing | hashlib.md5/sha1 | argon2-cffi for passwords; SHA-256 for integrity | | TLS | verify=False, old protocols | default verification; ssl with TLS 1.2+ |

Django-specific: keep DEBUG=False, set SECURE_* settings, use CSRF_* middleware, never mark_safe on user data, use get_object_or_404 with user-scoped querysets, set ALLOWED_HOSTS, and prefer django.core.signing for signed cookies. FastAPI/Starlette: pydantic with extra="forbid", explicit response models, and dependency-injected authz.

TypeScript / Node

Depth pack: ../../typescript/SKILL.md (runtimes, frameworks, Node backend patterns).

| Risk | Unsafe | Safe | |---|---|---| | Command | exec, execSync with interpolation | execFile/spawn with args, shell: false | | SQL | template literals in queries | driver placeholders + values array | | Prototype pollution | Object.assign(target, userObj), deep-merge libs | validate keys; Object.create(null); ban __proto__/constructor keys | | Deserialization | node-serialize, eval, new Function, vm as sandbox | JSON.parse + Zod/Valibot; no dynamic code | | XSS | innerHTML, dangerouslySetInnerHTML with raw data | textContent; sanitizer; CSP/Trusted Types | | Randomness | Math.random for tokens | crypto.randomBytes, crypto.randomUUID | | Comparison | === on MACs/tokens | crypto.timingSafeEqual | | Paths | path.join(base, userInput) alone | resolve + startsWith(base + path.sep) | | Headers/CORS | reflecting Origin | explicit allowlist; Helmet-style defaults | | Regex | user-built new RegExp | escape input; safe-regex checks; timeouts |

Note: node:vm is not a security boundary and can be escaped; do not use it to run untrusted code. For untrusted JavaScript execution, isolate in a separate process or WASM sandbox with resource limits. In frameworks, prefer helmet-style header baselines and CSRF middleware, and bind payloads to DTO schemas instead of as casts.

Go

Depth pack: ../../go/SKILL.md (database/sql, pgx, routing, supply chain).

| Risk | Unsafe | Safe | |---|---|---| | SQL | fmt.Sprintf into db.Query | QueryContext with $1/? placeholders | | Command | exec.Command("sh", "-c", ...) | exec.Command(binary, args...); -/-- guard | | Templates | text/template for HTML | html/template (contextual auto-escaping) | | Deserialization | gob across trust boundaries, YAML into interface{} | encoding/json into typed structs | | Randomness | math/rand | crypto/rand; rand/v2 still not for secrets | | Comparison | == on tokens | crypto/subtle.ConstantTimeCompare | | Paths | filepath.Join then open | filepath.Clean + filepath.Rel prefix check | | TLS | zero-value tls.Config in some paths; InsecureSkipVerify: true | explicit MinVersion: tls.VersionTLS12, verified chains | | Secrets | struct tags/logging configs | env/secrets manager; redact slog attributes |

Run go vet, gosec, govulncheck, and the race detector in CI. Prefer crypto/rand even for non-secret IDs if they appear in access control, and be careful with unsafe and reflect around untrusted input.

Rust

Depth pack: ../../rust/SKILL.md (ownership, async, unsafe/FFI review, sqlx/axum, cargo workflows).

| Risk | Unsafe | Safe | |---|---|---| | SQL | format! into sqlx::query | sqlx::query!/query_as! or .bind(value) | | Command | sh -c with interpolated string | Command::new(binary).args([...]) with validation | | Secrets | String secrets copied everywhere | secrecy::SecretString, zeroize on drop | | Randomness | fastrand, thread_rng for secrets | rand::rngs::OsRng / getrandom | | Comparison | == on MACs | subtle::ConstantTimeEq | | Deserialization | serde_json::Value then unchecked casts | typed structs, deny_unknown_fields, validation | | Crypto | hand-rolled unsafe crypto | ring, rustls, aws-lc-rs, argon2, chacha20poly1305 | | FFI | unchecked pointer/length from C | try_into, checks before unsafe, document invariants |

Cargo hygiene: commit Cargo.lock, run cargo audit and cargo deny (licenses, bans, advisories), avoid unnecessary unsafe, and add #![forbid(unsafe_code)] in crates that do not need it. Audit proc-macro and build-script dependencies as executable code in the build.

Java / Kotlin

Depth pack: ../../java-kotlin/SKILL.md (JDBC/JPA, Spring Security, serialization, Gradle/Maven).

| Risk | Unsafe | Safe | |---|---|---| | SQL | Statement + concat, HQL concat | PreparedStatement, JPA setParameter, Criteria API | | Command | Runtime.exec(String) | ProcessBuilder(List.of(...)) with validation | | Deserialization | ObjectInputStream, Jackson default typing, Fastjson autotype, XStream | fixed DTOs; PolymorphicTypeValidator allowlist; no native serialization | | XML | DocumentBuilderFactory defaults (XXE) | disable DTDs and external entities; XMLConstants features | | YAML | new Yaml() unsafe load | SafeConstructor with explicit types | | Randomness | java.util.Random, Math.random | SecureRandom | | Comparison | Arrays.equals on MACs | MessageDigest.isEqual | | Expression languages | SpEL/OGNL/MVEL with user input | never evaluate user input; use data binding | | Logging | user data into log messages unescaped | parameterized logging; CRLF encoding | | Paths | new File(base, userInput) | Path.normalize + startsWith(base) after toRealPath | | Crypto | Cipher.getInstance("AES") (ECB) | AES/GCM/NoPadding with random IV; BouncyCastle where needed |

Kotlin: string templates ("SELECT ... $id") are a common reintroduction of SQL injection; the same parameterization rules apply. Spring Boot: secure defaults, CSRF enabled for cookie sessions, method security annotations enforced, actuator endpoints protected, and no entity binding directly from request bodies. Note that the SecurityManager is disabled/removed in recent JDKs, so do not design sandboxing around it; isolate untrusted code in processes or containers instead.

C / C++

Depth pack: ../../systems/SKILL.md (systems, memory, tooling context).

| Risk | Practice | |---|---| | Buffer overflows | Use snprintf, std::string/std::span, bounds-checked containers; never strcpy, strcat, gets, sprintf | | Integer overflow | Check before arithmetic; use __builtin_*_overflow, std::ckd* (C++26, verify upstream) or explicit checks; size before allocation | | Use-after-free / double free | RAII, smart pointers, ownership annotations; sanitizers in CI | | Format strings | Never pass user data as the format: printf("%s", user) | | Secrets in memory | Zero with explicit_bzero/memset_s; disable core dumps; avoid swapping secrets | | Parsing untrusted input | Fuzz with libFuzzer/AFL++ and run ASan/UBSan/MSan; avoid recursive descent without depth limits | | Command execution | execve with argv arrays; never system()/popen() with user data | | Randomness | getrandom(2)/BCryptGenRandom/SecRandomCopyBytes; never rand() | | Crypto | OpenSSL/libsodium/BoringSSL; check return values; no custom memory comparison | | Compiler hardening | -D_FORTIFY_SOURCE=3, -fstack-protector-strong, -fPIE -pie, -Wl,-z,relro,-z,now, _GLIBCXX_ASSERTIONS, CFI where available |

C23 and modern C++ reduce some classes (bounds-checked APIs, std::span, constexpr validation) but do not eliminate the need for fuzzing and sanitizers. Treat every memcpy, pointer arithmetic, and integer conversion as a review checkpoint.

Tooling Matrix

| Language | SAST / Lint | Dependency audit | Fuzzing / Runtime | |---|---|---|---| | Python | Bandit, Semgrep, ruff (S rules) | pip-audit, uv audit, OSV-Scanner | Atheris, Hypothesis | | TS/Node | ESLint security plugins, Semgrep, CodeQL | npm/pnpm audit, OSV-Scanner | Jazzer.js, fast-check | | Go | gosec, staticcheck, govulncheck | govulncheck, OSV-Scanner | native go test -fuzz | | Rust | clippy (-D warnings), cargo-geiger | cargo-audit, cargo-deny, OSV-Scanner | cargo-fuzz, proptest | | Java/Kotlin | SpotBugs + FindSecBugs, CodeQL, Semgrep | OWASP Dependency-Check, Gradle/Maven audit, OSV-Scanner | Jazzer | | C/C++ | clang-tidy, CodeQL, Coverity | OSV-Scanner, Trivy | libFuzzer, AFL++, ASan/UBSan/MSan/TSan |

Pick tools that run in CI and fail on new high-severity findings; a scanner that nobody reads is not a control (see ./08-review-checklist.md).

Cross-Language Anti-Patterns

  • "Framework X escapes for me" applied to raw/untrusted/unsafe escape hatches the framework offers.
  • Copy-pasting a safe snippet from one language into another without the matching library.
  • Relying on types at compile time for runtime data (casts, as, interface{}, Any).
  • Custom crypto or custom token formats in the language's "simple" APIs.
  • Security tooling installed but not enforced on pull requests.

Checklist

  • [ ] Each service has the language's SAST, audit, and secret-scanning tools wired into CI.
  • [ ] No dynamic code execution (eval, exec, new Function, reflection-driven calls) on untrusted input.
  • [ ] Native object deserialization is absent; formats are JSON/CBOR/protobuf with schemas.
  • [ ] Command execution uses argv arrays with validated arguments.
  • [ ] Secrets come from the environment or a manager; nothing is committed.
  • [ ] Language-specific sharp edges from the table above are covered by tests.
07-dependencies

Dependencies and Supply Chain

Scope: dependency hygiene, lockfiles, integrity and provenance, audit workflows, patch SLAs, and defenses against typosquatting, dependency confusion, and compromised build pipelines.

Threat Model

Your product is the transitive closure of everything you install. Real 2020s-2026 incidents came from many directions:

| Attack | Mechanism | Defense | |---|---|---| | Typosquatting | Name close to a popular package (reqeusts, lodahs) | Verify names, scope registries, review before install | | Combosquatting | org-package-extra names | Prefer official scoped packages | | Dependency confusion | Internal name resolved from the public registry | Scope private names; registry pinning; allowlists | | Maintainer account takeover | New malicious version from a trusted package | Lockfiles, delayed updates, provenance, review diffs | | Malicious install script | postinstall/build script runs on install | Disable scripts where possible; sandboxed install | | Compromised CI | Token theft publishes a poisoned version | OIDC, least privilege, pinned actions, protected branches | | Protestware / sabotage | Maintainer intentionally breaks consumers | Vendoring/replaceable deps for critical paths | | Abandoned package | Unpatched known CVE | Ownership review, forks, replacement plan | | Build-time exfiltration | Compiler/plugin reaches network or secrets | Egress controls, ephemeral credentials, artifact signing |

Treat the package manager as a remote code execution channel. Minimize what you install, pin what you keep, and verify where it came from.

Hygiene Rules

  1. Minimize. Every dependency is attack surface, compile time, license risk, and bytes. Prefer the standard library and platform APIs. Replace micro-dependencies with 20 lines of local code.
  2. Evaluate before adding. Check: recent releases, maintainer count and responsiveness, open security advisories, transitive dependency count, install scripts, download provenance, and whether the license is acceptable. A package with one maintainer and an postinstall script deserves more scrutiny.
  3. Pin and lock. Applications commit exactly one authoritative lockfile per project and CI installs strictly from it (--frozen-lockfile, npm ci, uv sync --frozen). Libraries publish compatible ranges but test against a lock.
  4. Update deliberately, not continuously. Automated update PRs are triaged like any change: read the changelog, check the diff, run tests, and merge on a schedule with a rollback path.
  5. Separate dev and runtime. Test, lint, and build tooling must not ship in runtime images or production environments.
  6. Prefer fewer registries. Configure one registry per scope; never let resolution fall back between public and private sources for the same name.
  7. Vendor what is critical. For release-blocking dependencies (crypto, parsers, protocol libraries), keep a fork or vendored copy and a documented replacement path.

Lockfiles and Integrity

| Ecosystem | Lockfile | Strict install | Integrity mechanism | |---|---|---|---| | Python (uv) | uv.lock | uv sync --frozen | Hashes in lock | | Python (pip) | requirements.txt with hashes (pip-tools) | pip install --require-hashes | --hash=sha256:... | | Python (Poetry) | poetry.lock | poetry install --sync | Hashes in lock | | Node (npm) | package-lock.json | npm ci | integrity (sha512) fields | | Node (pnpm) | pnpm-lock.yaml | pnpm install --frozen-lockfile | integrity fields | | Node (yarn) | yarn.lock | yarn install --immutable | Checksums | | Go | go.sum | go mod verify, GOFLAGS=-mod=readonly | Module hashes, checksum DB | | Rust | Cargo.lock | cargo build --locked | Checksums in lock | | Java (Gradle) | gradle.lockfile, dependency verification metadata | --write-locks in CI check mode | verification-metadata.xml | | Java (Maven) | Maven resolver checksums; versions-maven-plugin | Reproducible builds config | sha256/sha512 from repo | | .NET | packages.lock.json | dotnet restore --locked-mode | Content hashes |

  • Commit lockfiles; review lockfile diffs like code. Unexpected transitive additions are a signal.
  • Enable integrity verification everywhere it exists; if the ecosystem supports a checksum database (Go, npm provenance, Cargo), use it.
  • For browser-loaded third-party assets, use Subresource Integrity (SRI) with crossorigin and a pinned version; never latest.
  • Track the registry and namespace for every package; document private scopes.

Provenance and SBOM

  • Provenance — prefer packages that publish signed build attestations (npm attestations, Sigstore signatures, SLSA provenance). Verify where the ecosystem supports it; treat unsigned packages above an uninteresting size as needing justification.
  • SLSA — aim for level 2+ for your own artifacts (hosted build, signed provenance) and prefer dependencies that publish provenance. See ../../cloud/SKILL.md for platform supply-chain controls.
  • SBOM — generate a CycloneDX or SPDX SBOM per release, attach it to the artifact, and keep it queryable. It is the inventory you use when the next advisory drops.
  • Signing — sign your own artifacts (Sigstore/cosign, Signtool, GPG) and verify at deploy time; admission policies can reject unsigned images.
  • VEX — when an advisory affects a component you do not use vulnerably, record a VEX statement instead of silently ignoring the finding.

Audit Workflow

  1. Inventory — SBOM plus lockfiles; know every direct and transitive component and its version.
  2. Detect — scheduled scans plus PR-time checks: OSV-Scanner, Trivy/Grype, ecosystem tools (pip-audit, npm audit, govulncheck, cargo audit), and platform bots (Dependabot, Renovate). Include container base images and CI actions.
  3. Triage — score with exploitability, not just CVSS: is the vulnerable function reachable, is the input attacker-controlled, is there a public exploit or KEV listing, what is the exposure (internet-facing vs internal)?
  4. Prioritize — map to the patch SLA table below; escalate exploited or internet-facing issues immediately.
  5. Patch — bump the direct dependency or use an override/patch for the transitive one; if no fix exists, mitigate (feature flag off, WAF rule, network isolation) and schedule removal.
  6. Verify — tests plus a scan proving the finding is gone; confirm the fix version is what the lockfile resolved to.
  7. Record — accept residual risk explicitly with an owner and expiry; suppression without an expiry becomes permanent debt.

Patch SLAs (defaults; tighten for exposed systems)

| Severity / context | Target | |---|---| | Exploited in the wild (KEV) or public exploit, reachable | Hours; mitigate now, patch < 24 h | | Critical, internet-facing | 24-72 h | | Critical, internal | 7 days | | High | 14-30 days | | Medium | 30-90 days (next scheduled cycle acceptable) | | Low | Next planned upgrade; track, do not block | | EOL runtime/base image | Migrate before end-of-support; treat as high |

Typosquatting and Dependency Confusion

  • Adopt scoped names for internal packages (@company/..., a private index prefix) so a public package can never occupy the same name.
  • Configure the package manager so a private scope always resolves to the private registry, and public scopes never fall through to internal ones.
  • Publish placeholder packages on public registries for internal names you cannot scope, or reserve the namespace.
  • Verify new dependency names character-by-character against the official documentation; do not trust search results, generated snippets, or LLM suggestions.
  • Review lockfile changes in code review; an unexpected registry URL or integrity hash change is a red flag.
  • Consider a private proxy/cache (Artifactory, Nexus, Cloudsmith, devpi) that allowlists sources and records what was fetched.

Install and Build Hardening

| Control | Examples | |---|---| | Disable lifecycle scripts | npm ignore-scripts=true; pnpm ignore-scripts; Python wheels over sdists; Cargo cargo deny bans | | Sandbox installs | Ephemeral container with no secrets, egress allowlist, read-only FS | | Freeze resolution | npm ci, pnpm --frozen-lockfile, uv sync --frozen, cargo --locked | | Pin CI actions | Full commit SHA, not tags; review updates like code | | Least-privilege CI | permissions: contents: read default; OIDC per job; no long-lived cloud keys | | Protect publish paths | 2FA, trusted publishing (OIDC), protected branches, environment approvals | | Separate build and release | Build once, promote the same artifact; no rebuild between stages | | Egress control | Build network restricted to registries and artifact stores | | Reproducible builds | Pin toolchains and base images by digest; verify build inputs |

Never give a pull_request_target workflow a checkout of untrusted PR code with secrets in scope. Never echo secrets into logs; mask and rotate CI variables like production credentials.

Incident Response for a Compromised Dependency

  1. Freeze deployments and dependency resolution; pin to the last known-good versions.
  2. Identify exposure: which versions, which builds, which environments, which credentials the build could reach.
  3. Treat build-time secrets as compromised; rotate and re-issue.
  4. Remove or replace the package; rebuild artifacts from clean inputs; re-sign and redeploy.
  5. Audit for persistence: modified CI configs, new publish tokens, changed lockfiles, unexpected reverse shells or scheduled jobs.
  6. Publish an internal timeline and add detection (a rule, a lockfile guard) so the class is caught next time.

Anti-Patterns

  • Installing from unverified snippets found in issues, gists, or generated code without checking the package name.
  • npm install <pkg> in production images without a lockfile or with latest ranges.
  • Auto-merging dependency bump PRs without reading the changelog or running tests.
  • Ignoring container base image and CI action updates while chasing application CVEs.
  • Suppressing scanner findings permanently with no owner or expiry.
  • Private package names without a scope next to a public registry.
  • Storing long-lived registry/cloud tokens in CI when OIDC federation is available.
  • Treating an SBOM as a compliance artifact rather than an operational inventory.

Checklist

  • [ ] Every application has one committed lockfile and CI installs strictly from it.
  • [ ] Dependency additions require a documented review of maintenance, provenance, and scripts.
  • [ ] Install scripts are disabled or sandboxed; builds run with no standing secrets and limited egress.
  • [ ] Provenance/signatures are verified where the ecosystem supports them; artifacts are signed.
  • [ ] An SBOM is generated per release and stored with the artifact.
  • [ ] Scans run on PRs and on a schedule for OS packages, language deps, containers, and CI actions.
  • [ ] Patch SLAs are documented and met; exceptions have owners and expiry dates.
  • [ ] Private namespaces are scoped and registry resolution cannot fall back to public sources.
  • [ ] A dependency-compromise runbook exists and has been rehearsed.
08-review-checklist

Secure Code Review and Security Testing

Scope: a repeatable secure code review process, per-class review checklists, security-focused test techniques, threat-driven test cases, and CI gates that keep fixes in place.

Review Goals

A secure review answers four questions for every change:

  1. What new data reaches what interpreter or trust boundary? (Injection, deserialization, SSRF.)
  2. Who can call this, and what can they reach? (Authentication, authorization, tenancy.)
  3. What can an attacker learn or force? (Disclosure, DoS, races, error handling.)
  4. What assumptions does it add to the system, and are they tested? (Config, dependencies, crypto, invariants.)

Static analysis finds patterns; only a human (or a threat-driven test) answers whether the authorization check covers the object in question. For threat modeling depth, see ../../security/SKILL.md.

Review Process

  1. Scope the diff. Identify routes, jobs, and consumers touched; note new dependencies, config, permissions, and serialization formats.
  2. Map data flows. For each new input, trace source -> transforms -> sinks. Mark trust boundaries: HTTP, queue, database row from another tenant, third-party API, file, URL fetch.
  3. Check authorization per entry point. Enumerate actors and verify object-scoped checks, not just route-level middleware. Look for IDOR patterns (get(id) without a tenant filter).
  4. Review sinks against the class tables below. Use git diff-focused review for changes and a lighter full-service pass periodically.
  5. Verify failure modes. Force errors: dependency down, malformed input, partial writes, concurrent requests, expired tokens. Confirm fail-closed behavior.
  6. Check tests. Every fix and every sensitive path needs a security-relevant test (below).
  7. Record findings with severity, exploit narrative, location, and a concrete fix. Re-review the fix and confirm the regression test fails without it.
  8. Add prevention. When the same defect class appears twice, add a lint/SAST rule or a shared helper so the pattern becomes hard to repeat.

High-Risk Diff Signals

Scan new code for these greppable patterns before anything else:

  • String formatting near interpreters: f", %s with %, +, format(, template literals next to query, execute, exec, system, spawn, render, compile.
  • Raw/unsafe escape hatches: raw(, text(, $queryRawUnsafe, createNativeQuery, literal(, sql.raw, eval, Function(, vm.run, os.system, shell=True, pickle, Marshal, unserialize, ObjectInputStream, BinaryFormatter.
  • Authz gaps: routes without the policy/middleware decorator; lookups by ID without owner/tenant scope; admin checks done in the UI only; is_staff vs object permission confusion.
  • Secrets: committed config, tokens in logs, long-lived credentials, new environment variables without a manager, .env in an image layer.
  • Network: new outbound fetch of user URLs, disabled TLS verification, * CORS, 0.0.0.0 binds.
  • Crypto: md5, sha1, ECB, CBC without MAC, static IV, Math.random, rand(), == on MACs.
  • Files: upload handlers, archive extraction, path joins, send_file with user paths.
  • Dependencies: new package, version change, lockfile churn with new transitive entries.
  • CI: workflow changes, new tokens/permissions, pull_request_target, unpinned actions.

Per-Class Checklist

Authentication and session

  • [ ] Credentials verified with a password-hashing library and constant-time comparison.
  • [ ] Session identifiers are CSPRNG, rotated on login and privilege change, and invalidated on logout.
  • [ ] Cookies carry HttpOnly, Secure, SameSite, and a __Host- prefix where applicable.
  • [ ] Password reset and email verification tokens are single-use, expiring, hashed at rest.
  • [ ] MFA bypass paths, "remember me", and recovery flows have explicit tests.
  • [ ] JWT/OAuth tokens verify signature, algorithm allowlist, issuer, audience, and expiry.

Authorization

  • [ ] Every endpoint and background consumer enforces authorization server-side.
  • [ ] Object access filters include owner/tenant scope in the query, not after the fetch.
  • [ ] Role changes, account deletion, and privilege transitions invalidate cached decisions.
  • [ ] Admin/internal endpoints are isolated (network and role) and audited.
  • [ ] Tests prove one tenant cannot read or mutate another tenant's objects.

Injection and interpreters

  • [ ] Values are bound; identifiers/sort/operators come from allowlists (./01-injection.md).
  • [ ] No shell with user data; argv arrays, absolute binaries, -- terminators.
  • [ ] Templates render data, never compile user input.
  • [ ] LDAP/XPath filters are escaped or library-built.

Browser-facing

  • [ ] Context-aware encoding at every sink; no raw HTML sinks with untrusted data (./02-xss-csrf.md).
  • [ ] CSP (nonce-based), Trusted Types, and the security header baseline are set and tested.
  • [ ] CSRF protection covers all state-changing routes, including login/logout.
  • [ ] CORS uses an exact allowlist with Vary: Origin; credentials only where required.

Data protection and crypto

  • [ ] AEAD with unique nonces; keys from a KMS; rotation defined (./04-crypto-secrets.md).
  • [ ] Passwords use Argon2id/scrypt/bcrypt with tuned parameters and lazy rehash.
  • [ ] Secrets absent from code, images, logs, and client bundles; CI scans history.
  • [ ] PII is minimized, encrypted at rest where required, and access is audited.

Input, files, and paths

  • [ ] Boundary schemas validate type/length/range/enum and reject unknown fields (./05-validation-encoding.md).
  • [ ] Uploads are content-sniffed, re-encoded, randomly named, stored inert, served safely.
  • [ ] Path parameters resolve inside a base directory after symlink resolution.
  • [ ] Decompression, regex, and parser resources are bounded.

Network and serialization

  • [ ] User-controlled outbound URLs are scheme/IP/redirect validated with egress controls (./03-ssrf-deserialization.md).
  • [ ] No native deserialization of untrusted data; signed state is verified before parsing.
  • [ ] Webhooks verify signatures and timestamps with replay protection.

Resilience and observability

  • [ ] External calls have timeouts, retries with jitter and caps, and circuit breakers.
  • [ ] Race-prone operations use transactions, locks, or atomic compare-and-set.
  • [ ] Errors to clients are generic; logs capture detail without secrets or full payloads.
  • [ ] Security events (auth failures, permission denials, admin actions) are audited with actors.

Dependencies and configuration

  • [ ] Lockfile updated intentionally; new packages reviewed (./07-dependencies.md).
  • [ ] Default credentials and debug modes are off; config validation fails startup on insecure values.
  • [ ] Infrastructure changes have least-privilege IAM and no public exposure by default.

Security-Focused Tests

Security tests are ordinary tests aimed at abuse cases. They belong in the same suites and CI runs as functional tests so they cannot rot.

| Technique | Catches | Practical use | |---|---|---| | Unit abuse tests | Injection, encoding, authz logic | Hostile payload per sink/route | | Property-based tests | Parsers, canonicalization, invariants | Hypothesis/fast-check/proptest generators | | Fuzzing | Memory safety, parser crashes, logic edge cases | libFuzzer/AFL++/cargo-fuzz/go-fuzz/Jazzer/Atheris | | SAST | Unsafe patterns at scale | Semgrep, CodeQL, Bandit, gosec, SpotBugs+FindSecBugs, clippy | | SCA | Known vulnerable dependencies | OSV-Scanner, Trivy, ecosystem audits | | Secret scanning | Credential leakage | gitleaks, TruffleHog, provider scanning | | IaC scanning | Cloud/Kubernetes misconfig | Checkov, Trivy config, kube-linter | | DAST | Runtime issues on a live app | ZAP, Burp, authenticated scans in staging | | Sanitizers | Memory/UB/thread errors in native code | ASan, UBSan, MSan, TSan | | Contract/authz matrices | Broken access control | Parameterized tests across roles and tenants |

Rules for security tests:

  • Write them from the attacker's perspective: "user A requests B's object", "payload contains </script>", "URL redirects to metadata", not just "input is rejected".
  • Pin the original vulnerability payload as a regression test for every fixed finding.
  • Keep them fast and deterministic; flaky security tests get disabled, which means they do not exist.
  • Run SAST/SCA plus the abuse tests on every pull request; run fuzzing/DAST on a schedule and before major releases.

Threat-Driven Test Cases

Derive tests from STRIDE per component rather than from a generic checklist (depth in ../../security/SKILL.md).

| Threat | Test an attacker would run | |---|---| | Spoofing | Forged/expired/alg:none tokens; replayed webhooks; session fixation; MFA downgrade | | Tampering | Modified signed state, price/quantity fields, hidden fields, path traversal, mass assignment | | Repudiation | Actions without audit trail; logs missing actor, timestamp, or outcome | | Information disclosure | IDOR across tenants; verbose errors; directory listing; cache-debug headers; SSRF readback; timing oracles | | Denial of service | Oversized bodies, deep JSON, ReDoS, zip bombs, unbounded pagination, expensive unauthenticated endpoints, lock contention | | Elevation of privilege | Role field injection, admin route reachable by normal user, JWT claim trust, SSRF to internal admin APIs |

Also test the boring abuse cases: double-spend/duplicate submission (idempotency), coupon/credit reuse, enum-based state machine skips, concurrent updates losing data, and account enumeration via timing or message differences.

CI Gates and Suppression Policy

| Gate | Stage | Failure mode | |---|---|---| | Secret scan | Commit/PR | Block on any hit; rotate if real | | SAST (high severity) | PR | Block on new findings; baseline legacy with expiry | | SCA (critical/high reachable) | PR + nightly | Block per deployment policy; nightly for base images | | IaC scan | PR for infra dirs | Block on high severity | | Abuse/regression tests | PR | Block | | Fuzzing smoke | PR for parser changes | Block on crashes; longer runs nightly | | DAST | Staging deploy | Block promotion on high findings |

Suppression rules: every ignore needs a reason, an owner, a link to the ticket, and an expiry date (90 days default). Findings on internet-facing code are never suppressed by default. Track metrics that drive behavior: mean time to patch by severity, repeat defect classes, and percent of PRs with security-relevant tests.

Finding Writeup Format

Title:      <class> in <component> allows <impact>
Severity:   critical | high | medium | low (with rationale)
Location:   path/file.ext:line
Actor:      unauthenticated | any user | tenant A | insider
Narrative:  steps from request to impact
Evidence:   payload, test, or tool output
Fix:        concrete change, plus the test that proves it
Prevention: lint/SAST rule, shared helper, or guideline update

Review Sign-Off Checklist

  • [ ] Data flows traced for all new inputs; every sink mapped to a control.
  • [ ] Authorization verified per object and per action, including background jobs and exports.
  • [ ] Failures fail closed; errors and logs do not leak secrets or internals.
  • [ ] Crypto, randomness, and secret handling follow ./04-crypto-secrets.md.
  • [ ] Files, paths, uploads, and outbound fetches follow ./05-validation-encoding.md and ./03-ssrf-deserialization.md.
  • [ ] Dependencies, lockfiles, and CI changes reviewed (./07-dependencies.md).
  • [ ] Abuse tests and a regression test for each fixed finding are present and run in CI.
  • [ ] Residual risk is documented with an owner and an expiry; suppressions expire.