npx skills add ...
npx skills add addyosmani/agent-skills --skill security-and-hardening
Hardens code against vulnerabilities. Use when auditing an input handler for vulnerabilities, when handling user input, authentication, data storage, or external integrations, or when checking a login flow is safe against the OWASP Top Ten. Use when building any feature that accepts untrusted data, manages user sessions, or interacts with third-party services. Use when auditing dependencies for known vulnerabilities, triaging package-manager audit findings, or assessing supply-chain risk in a new package. Use when personal data or privacy compliance (GDPR, CCPA) is involved.
npx skills add addyosmani/agent-skills --skill security-and-hardening
Security-first development practices for web applications. Treat every external input as hostile, every secret as sacred, and every authorization check as mandatory. Security isn't a phase — it's a constraint on every line of code that touches user data, authentication, or external systems.
Controls bolted on without a threat model are guesses. Before hardening, spend five minutes thinking like an attacker:
| Threat | Ask | Typical mitigation |
|---|---|---|
| Spoofing | Can someone impersonate a user/service? | Authentication, signature verification |
| Tampering | Can data be altered in transit or at rest? | Integrity checks, parameterized queries, HTTPS |
| Repudiation | Can an action be denied later? | Audit logging of security events |
| Information disclosure | Can data leak? | Encryption, field allowlists, generic errors |
| Denial of service | Can it be overwhelmed? | Rate limiting, input size caps, timeouts |
| Elevation of privilege | Can a user gain rights they shouldn't? | Authorization checks, least privilege |
If you can't name the trust boundaries for a feature, you're not ready to secure it. This is OWASP A04: Insecure Design — most breaches begin in design, not code.
eval() or innerHTML with user-provided dataThese are prevention patterns, not a ranking. For the 2021 ordering, see the quick-reference table in ../../references/security-checklist.md.
Any time the server fetches a URL the user influenced — webhooks, "import from URL", image proxies, link previews — an attacker can aim it at internal services (cloud metadata, localhost, private IPs).
The range() !== 'unicast' check covers loopback, link-local 169.254.169.254 (cloud metadata, the #1 SSRF target), private, and unique-local ranges across IPv4 and IPv6.
Caveat — this still has a TOCTOU gap. fetch resolves DNS again after the check, so an attacker using a short-TTL record can rebind to an internal IP between validation and connection. For high-risk surfaces, resolve once and connect to the pinned IP, or put a filtering agent in front (request-filtering-agent / ssrf-req-filter).
A delete, move, or overwrite is only as safe as the value that names its target. Reading that value from the kernel, a job payload, or a sibling service proves where it arrived from, not who wrote it — another process's command line is as attacker-controlled as a form field. A shape check ("absolute path, at least one directory deep") proves well-formedness and gets mistaken for authorization; that is how a cleanup routine deletes the root instead of the leaf.
Before a destructive call, require all three: the resolved target sits under an allowlisted root (compare after resolving symlinks, never on the raw string); it is at least one level below that root, so a root is never itself the target; and it carries evidence that it is yours, read before the operation and before any teardown that removes it — otherwise "absent" and "not mine" are indistinguishable. On refusal, log the rejected target and stop: a cleanup that falls back to a broader default path is the failure this guards against. Worked example in ../../references/security-checklist.md.
Two limits, because the check reads stronger than it is. A marker inside the tree is self-attestation — anything that can write there can write the marker — so the expected owner has to come from authenticated state, and the marker needs integrity protection (restrictive ownership, or a MAC) before it counts as authorization. And resolving a path and then operating on the name is a check/use race wherever an untrusted process can swap an ancestor: on a shared volume, hold the target by descriptor and use no-follow, beneath-the-root operations, or make sure the hierarchy cannot change for the duration.
Package-manager audits report known advisories; they do not prove a package is trustworthy or that vulnerable code is reachable. Use this decision tree:
Key questions:
When you defer a fix, document the reason and set a review date.
Do not assume npm or treat the nearest manifest as the install root. Apply this order:
packageManager (when present), the lockfile, and CI; stop on disagreement or competing lockfiles. Pin the manager version and use the matrix in ../../references/security-checklist.md.Audits only find known advisories; they do not catch a newly malicious or typosquatted package. Therefore:
npm audit fix --force or equivalent). Preview the remediation, read changelogs, and test each resulting upgrade; forced fixes may cross declared dependency ranges.npm audit signatures, pnpm audit signatures) and treat absence as a signal to investigate, not automatic proof of compromise.cross-env vs crossenv (OWASP A06, LLM03).Count in a shared store once there is more than one process. express-rate-limit keeps its counters in process memory by default. Behind a load balancer each instance holds its own count, so the effective limit is max × instances; on serverless or edge runtimes a fresh invocation starts from zero, so the auth limit above may never fire. Pass a shared store (Redis via rate-limit-redis), or use an HTTP-based limiter that works where a long-lived TCP connection does not (for example @upstash/ratelimit):
Always check before committing:
If a secret is ever committed, rotate it. Deleting the line or rewriting history is not enough — assume it's compromised the moment it reaches a remote. Revoke and reissue the key first, then purge it from history.
Securing data is "can an attacker read it?" Privacy is "should we even hold it, and for how long?" — a separate question that hardening doesn't answer. The cheapest data to protect, breach, and comply over is the data you never collected. Treat personal data as a liability to minimize, not an asset to hoard.
Know what you hold. You can't protect or honor a deletion request for data you can't find. Classify fields as you add them:
| Class | Examples | Handling |
|---|---|---|
| Non-personal | Aggregates, anonymized counts | Normal handling |
| Personal (PII) | Name, email, IP, device/user IDs | Minimize, access-control, include in export/delete |
| Sensitive | Health, finance, location, biometrics, gov IDs, anything about minors | Extra basis to collect, stricter access, often encryption + audit logging |
Operating rules:
observability-and-instrumentation skill makes the same point from the ops side).When data crosses a trust boundary, validate it as untrusted (see Input Validation above); when a privacy incident exposes personal data, the breach-notification clock is part of the postmortem — follow the debugging-and-error-recovery skill.
If your app calls an LLM — chatbots, summarizers, agents, RAG — it inherits a new attack surface. Map it to the OWASP Top 10 for LLM Applications (2025):
eval, SQL, a shell, innerHTML, or a file path. Validate and encode it exactly as you would raw user input.For detailed security checklists and pre-commit verification steps, see ../../references/security-checklist.md.
| Rationalization | Reality |
|---|---|
| "This is an internal tool, security doesn't matter" | Internal tools get compromised. Attackers target the weakest link. |
| "We'll add security later" | Security retrofitting is 10x harder than building it in. Add it now. |
| "No one would try to exploit this" | Automated scanners will find it. Security by obscurity is not security. |
| "The framework handles security" | Frameworks provide tools, not guarantees. You still need to use them correctly. |
| "It's just a prototype" | Prototypes become production. Security habits from day one. |
| "Threat modeling is overkill here" | Five minutes of "how would I attack this?" prevents the design flaws no control can patch later. |
| "It's just LLM output, it's only text" | That "text" can be a SQL statement, a script tag, or a shell command. Treat it like any untrusted input. |
| "The audit passed, so the dependency is safe" | Audits match known advisories. They do not detect a newly malicious package or make unreviewed install scripts safe to execute. |
| "Collect it now, we might need it later" | Data you don't hold can't be breached, subpoenaed, or mis-deleted. "Might need it" is breach scope, not a purpose. |
| "We'll handle deletion requests manually" | Manual erasure misses backups, caches, and analytics copies. If the schema can't find a user's data, you can't honor the request — design for it. |
| "Compliance is legal's problem, not ours" | Export, deletion, retention, and consent are schema and code. Legal can't bolt them on after you've smeared PII across ten systems. |
*) originsevalAfter implementing security-relevant code: