---
name: security-and-hardening
description: Threat-model and harden web application code. Use when handling user input, authentication, sessions, file uploads, webhooks, secrets, third-party integrations, or LLM features, and when reviewing code for injection, XSS, SSRF, or access control gaps. Not for launch readiness and rollout: use shipping-and-launch. Not for auditing third-party skill packages: use skill-security-auditor.
license: MIT
metadata:
  author: TechTide AI (Alex Cinovoj)
  provenance: rewritten from patterns in addyosmani/agent-skills (MIT)
  category: Quality & Security
---

# Security and Hardening

Treat every external input as hostile, every secret as compromised the moment it leaks, and every authorization check as mandatory. Security is a constraint on each line that touches user data, not a phase.

## Threat Model First (5 minutes, always)

Controls without a threat model are guesses. Before hardening:

1. **Map trust boundaries.** Where does untrusted data enter? HTTP requests, forms, uploads, webhooks, third-party APIs, queues, and LLM output. Each boundary is attack surface.
2. **Name the assets.** Credentials, PII, payment data, admin actions, money movement.
3. **Run STRIDE per boundary.** Spoofing, Tampering, Repudiation, Information disclosure, Denial of service, Elevation of privilege. One question each, one mitigation each.
4. **Write abuse cases next to use cases.** "How would I misuse this?" becomes your first test.

If you cannot name the trust boundaries, you are not ready to secure the feature.

## The Three Tiers

**Always, no exceptions:** validate all input at the boundary, parameterize every query, encode output (keep framework auto-escaping on), HTTPS everywhere, hash passwords with bcrypt/scrypt/argon2, set security headers (CSP, HSTS, X-Frame-Options), httpOnly + secure + sameSite session cookies, run `npm audit` (or equivalent) before release.

**Ask first (human approval):** new auth flows, new categories of sensitive data, new external integrations, CORS changes, file upload handlers, rate limit changes, elevated roles.

**Never:** secrets in version control, sensitive data in logs, client-side validation as a security boundary, `eval()` or `innerHTML` with user data, auth tokens in localStorage, stack traces exposed to users.

Read references/security-checklist.md for code patterns per vulnerability class (injection, XSS, access control, SSRF, uploads, rate limiting, headers) and the full pre-commit review checklist.

## LLM Features Are Attack Surface

If the app calls a model:

- Treat model output as untrusted input. Never pass it to eval, SQL, a shell, `innerHTML`, or a file path without validation and encoding.
- The system prompt is not a security boundary. Enforce permissions in code. Any untrusted text in the context can carry injected instructions.
- Keep secrets and cross-tenant data out of prompts; anything in context can be echoed back.
- Scope tool permissions to minimum, confirm destructive actions, validate every tool argument.
- Cap tokens, request rate, and recursion depth.
- In RAG, partition embeddings per tenant and validate documents before indexing.

## Dependency Triage

Critical or high finding that is reachable in production: fix now. Fix unavailable: workaround, replace, or allowlist with a review date, in that order. Moderate and reachable: next release. Dev-only or low: backlog with a date. Document every deferral.

`npm audit` misses malicious packages. Also: commit the lockfile, install with `npm ci` in CI, review new dependencies before adding (maintenance, downloads, postinstall scripts), watch for typosquats.

**If a secret is ever committed, rotate it.** Deleting the line is not enough. Revoke and reissue first, purge history second.

## Verification

Run `npm audit --audit-level=high`. Expect exit code 0. If not, triage per the rules above before shipping.

Run `git diff --cached | grep -iE "password|secret|api_key|token"` before every commit touching config. Expect no matches on real values. If a real value matches, unstage, move it to the environment, and rotate it.

Hit a protected endpoint without a token and with another user's token. Expect 401 and 403 respectively. Any 200 is a broken access control bug, fix before anything else.

## Good vs Bad

**Bad:** "It's an internal tool, I'll skip authorization checks and validate on the client." Internal tools get compromised and attackers pivot through the weakest link. Client validation is UX, not security.

**Good:** Same internal tool ships with parameterized queries, per-resource ownership checks, and secrets in the environment. Cost: an hour. Retrofit cost after a breach: weeks plus disclosure.

## Footguns

- **Authenticated but not authorized.** The endpoint checks login, not ownership. Fix: compare `resource.ownerId` to `req.user.id` on every mutating route.
- **SSRF via user-supplied URLs.** Webhooks and "import from URL" aimed at cloud metadata or localhost. Fix: allowlist scheme and host, resolve DNS and reject non-unicast IPs, forbid redirects. Pattern in references/security-checklist.md.
- **Wildcard CORS with credentials.** `origin: '*'` plus cookies leaks sessions. Fix: explicit origin list from the environment.
- **Secrets "removed" by rewriting history.** The key already reached a remote or a fork. Fix: rotate first, always.

## Red Flags: Stop and Re-check

- "This is internal, security doesn't matter."
- "We'll add security later."
- "No one would try to exploit this."
- "The framework handles it."
- "It's just a prototype."
- "It's just LLM output, it's only text."

Each phrase is a rationalization, not a risk assessment. There is no configuration of these excuses that makes skipping a boundary check acceptable. If a control genuinely does not apply, write down why, in the PR, before skipping it.

## Completion Checklist

- [ ] Trust boundaries and assets named for the feature
- [ ] All input validated at the boundary, all queries parameterized
- [ ] Authz (ownership/role) checked on every protected route, verified with a foreign-token request
- [ ] No secrets in code, logs, or prompts; audit clean or deferrals documented
- [ ] Security headers, cookie flags, and CORS origins verified in a real response
- [ ] LLM output validated and encoded before use (if applicable)

Any box unchecked: not done. Fix or say so.
