← Agent SkillsCONTENT HISTORY

Update to Agent Skills

Snapshot Sep 30, 2026 · 23:17 UTC · version 0.6.10

Collection source: not recorded for this historical snapshot.

WHAT CHANGED · RULE-BASED ANALYSIS

First saved snapshot

No earlier snapshot is available to establish a change.

Compare saved observations

Download comparison JSON
Full technical diff · 0 changed fields
Full snapshot data
{
  "name": "security-and-hardening",
  "description": "Use when code handles untrusted input, authentication, sessions, sensitive data, external integrations, dependency vulnerabilities, or needs a security review against common attack classes.",
  "included_files": [
    {
      "relative_path": "agents/openai.yaml",
      "size_in_bytes": 242
    }
  ],
  "skill_md_contents": "---\nname: security-and-hardening\ndescription: Use when code handles untrusted input, authentication, sessions, sensitive data, external integrations, dependency\n  vulnerabilities, or needs a security review against common attack classes.\n---\n\n# Security and Hardening\n\n## Overview\n\nSecurity-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.\n\n## When to Use\n\n- Building anything that accepts user input\n- Implementing authentication or authorization\n- Storing or transmitting sensitive data\n- Integrating with external APIs or services\n- Adding file uploads, webhooks, or callbacks\n- Handling payment or PII data\n\n## Process: Threat Model First\n\nControls bolted on without a threat model are guesses. Before hardening, spend five minutes thinking like an attacker:\n\n1. **Map the trust boundaries.** Where does untrusted data cross into your system? HTTP requests, form fields, file uploads, webhooks, third-party APIs, message queues, and **LLM output** — plus the local values that look internal because the OS handed them to you: another process's command line or environment, filenames on a shared volume, a path in a job payload. Trust follows who *wrote* a value, not which channel delivered it. Every boundary is attack surface.\n2. **Name the assets.** What's worth stealing or breaking? Credentials, PII, payment data, admin actions, money movement.\n3. **Run STRIDE over each boundary** — a quick lens, not a ceremony:\n\n| Threat | Ask | Typical mitigation |\n|---|---|---|\n| **S**poofing | Can someone impersonate a user/service? | Authentication, signature verification |\n| **T**ampering | Can data be altered in transit or at rest? | Integrity checks, parameterized queries, HTTPS |\n| **R**epudiation | Can an action be denied later? | Audit logging of security events |\n| **I**nformation disclosure | Can data leak? | Encryption, field allowlists, generic errors |\n| **D**enial of service | Can it be overwhelmed? | Rate limiting, input size caps, timeouts |\n| **E**levation of privilege | Can a user gain rights they shouldn't? | Authorization checks, least privilege |\n\n4. **Write abuse cases next to use cases.** For each feature, ask \"how would I misuse this?\" — then make that your first test.\n\nIf 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.\n\n## The Three-Tier Boundary System\n\n### Always Do (No Exceptions)\n\n- **Validate all external input** at the system boundary (API routes, form handlers)\n- **Parameterize all database queries** — never concatenate user input into SQL\n- **Encode output** to prevent XSS (use framework auto-escaping, don't bypass it)\n- **Use HTTPS** for all external communication\n- **Hash passwords** with bcrypt/scrypt/argon2 (never store plaintext)\n- **Set security headers** (CSP, HSTS, X-Frame-Options, X-Content-Type-Options)\n- **Use httpOnly, secure, sameSite cookies** for sessions\n- **Run the detected package manager's native audit** against the committed lockfile before every release\n\n### Ask First (Requires Human Approval)\n\n- Adding new authentication flows or changing auth logic\n- Storing new categories of sensitive data (PII, payment info)\n- Adding new external service integrations\n- Changing CORS configuration\n- Adding file upload handlers\n- Modifying rate limiting or throttling\n- Granting elevated permissions or roles\n\n### Never Do\n\n- **Never commit secrets** to version control (API keys, passwords, tokens)\n- **Never log sensitive data** (passwords, tokens, full credit card numbers)\n- **Never trust client-side validation** as a security boundary\n- **Never disable security headers** for convenience\n- **Never use `eval()` or `innerHTML`** with user-provided data\n- **Never store sessions in client-accessible storage** (localStorage for auth tokens)\n- **Never expose stack traces** or internal error details to users\n\n## OWASP Top 10 Prevention Patterns\n\nThese are prevention patterns, not a ranking. For the 2021 ordering, see the quick-reference table in `../../references/security-checklist.md`.\n\n### Injection (SQL, NoSQL, OS Command)\n\n```typescript\n// BAD: SQL injection via string concatenation\nconst query = `SELECT * FROM users WHERE id = '${userId}'`;\n\n// GOOD: Parameterized query\nconst user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);\n\n// GOOD: ORM with parameterized input\nconst user = await prisma.user.findUnique({ where: { id: userId } });\n```\n\n### Broken Authentication\n\n```typescript\n// Password hashing\nimport { hash, compare } from 'bcrypt';\n\nconst SALT_ROUNDS = 12;\nconst hashedPassword = await hash(plaintext, SALT_ROUNDS);\nconst isValid = await compare(plaintext, hashedPassword);\n\n// Session management\napp.use(session({\n  secret: process.env.SESSION_SECRET,  // From environment, not code\n  resave: false,\n  saveUninitialized: false,\n  cookie: {\n    httpOnly: true,     // Not accessible via JavaScript\n    secure: true,       // HTTPS only\n    sameSite: 'lax',    // CSRF protection\n    maxAge: 24 * 60 * 60 * 1000,  // 24 hours\n  },\n}));\n```\n\n### Cross-Site Scripting (XSS)\n\n```typescript\n// BAD: Rendering user input as HTML\nelement.innerHTML = userInput;\n\n// GOOD: Use framework auto-escaping (React does this by default)\nreturn <div>{userInput}</div>;\n\n// If you MUST render HTML, sanitize first\nimport DOMPurify from 'dompurify';\nconst clean = DOMPurify.sanitize(userInput);\n```\n\n### Broken Access Control\n\n```typescript\n// Always check authorization, not just authentication\napp.patch('/api/tasks/:id', authenticate, async (req, res) => {\n  const task = await taskService.findById(req.params.id);\n\n  // Check that the authenticated user owns this resource\n  if (task.ownerId !== req.user.id) {\n    return res.status(403).json({\n      error: { code: 'FORBIDDEN', message: 'Not authorized to modify this task' }\n    });\n  }\n\n  // Proceed with update\n  const updated = await taskService.update(req.params.id, req.body);\n  return res.json(updated);\n});\n```\n\n### Security Misconfiguration\n\n```typescript\n// Security headers (use helmet for Express)\nimport helmet from 'helmet';\napp.use(helmet());\n\n// Content Security Policy\napp.use(helmet.contentSecurityPolicy({\n  directives: {\n    defaultSrc: [\"'self'\"],\n    scriptSrc: [\"'self'\"],\n    styleSrc: [\"'self'\", \"'unsafe-inline'\"],  // Tighten if possible\n    imgSrc: [\"'self'\", 'data:', 'https:'],\n    connectSrc: [\"'self'\"],\n  },\n}));\n\n// CORS — restrict to known origins\napp.use(cors({\n  origin: process.env.ALLOWED_ORIGINS?.split(',') || 'http://localhost:3000',\n  credentials: true,\n}));\n```\n\n### Sensitive Data Exposure\n\n```typescript\n// Never return sensitive fields in API responses\nfunction sanitizeUser(user: UserRecord): PublicUser {\n  const { passwordHash, resetToken, ...publicFields } = user;\n  return publicFields;\n}\n\n// Use environment variables for secrets\nconst API_KEY = process.env.STRIPE_API_KEY;\nif (!API_KEY) throw new Error('STRIPE_API_KEY not configured');\n```\n\n### Server-Side Request Forgery (SSRF)\n\nAny 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).\n\n```typescript\n// BAD: fetch whatever the user gives you\nawait fetch(req.body.webhookUrl);\n\n// GOOD: allowlist scheme + host, reject if ANY resolved IP is private, forbid redirects\nimport { lookup } from 'node:dns/promises';\nimport ipaddr from 'ipaddr.js';\n\nconst ALLOWED_HOSTS = new Set(['hooks.example.com']);\n\nasync function assertSafeUrl(raw: string): Promise<URL> {\n  const url = new URL(raw);\n  if (url.protocol !== 'https:') throw new Error('https only');\n  if (!ALLOWED_HOSTS.has(url.hostname)) throw new Error('host not allowed');\n  // Resolve ALL records; a single private/reserved address fails the check.\n  const addrs = await lookup(url.hostname, { all: true });\n  if (addrs.some((a) => ipaddr.parse(a.address).range() !== 'unicast')) {\n    throw new Error('private/reserved IP');\n  }\n  return url;\n}\n\nawait fetch(await assertSafeUrl(req.body.webhookUrl), { redirect: 'error' });\n```\n\nThe `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.\n\n**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`).\n\n## Input Validation Patterns\n\n### Schema Validation at Boundaries\n\n```typescript\nimport { z } from 'zod';\n\nconst CreateTaskSchema = z.object({\n  title: z.string().min(1).max(200).trim(),\n  description: z.string().max(2000).optional(),\n  priority: z.enum(['low', 'medium', 'high']).default('medium'),\n  dueDate: z.string().datetime().optional(),\n});\n\n// Validate at the route handler\napp.post('/api/tasks', async (req, res) => {\n  const result = CreateTaskSchema.safeParse(req.body);\n  if (!result.success) {\n    return res.status(422).json({\n      error: {\n        code: 'VALIDATION_ERROR',\n        message: 'Invalid input',\n        details: result.error.flatten(),\n      },\n    });\n  }\n  // result.data is now typed and validated\n  const task = await taskService.create(result.data);\n  return res.status(201).json(task);\n});\n```\n\n### File Upload Safety\n\n```typescript\n// Restrict file types and sizes\nconst ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp'];\nconst MAX_SIZE = 5 * 1024 * 1024; // 5MB\n\nfunction validateUpload(file: UploadedFile) {\n  if (!ALLOWED_TYPES.includes(file.mimetype)) {\n    throw new ValidationError('File type not allowed');\n  }\n  if (file.size > MAX_SIZE) {\n    throw new ValidationError('File too large (max 5MB)');\n  }\n  // Don't trust the file extension — check magic bytes if critical\n}\n```\n\n### Destructive Operations on Derived Paths\n\nA 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.\n\nBefore 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`.\n\nTwo 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.\n\n## Triaging Dependency Audit Results\n\nPackage-manager audits report known advisories; they do not prove a package is trustworthy or that vulnerable code is reachable. Use this decision tree:\n\n```\nThe native package-manager audit reports a vulnerability\n├── Severity: critical or high\n│   ├── Is the vulnerable code reachable in runtime, build, test, or deployment paths?\n│   │   ├── YES --> Fix immediately (update, patch, or replace the dependency)\n│   │   └── NO (confirmed unused across those paths) --> Fix soon, but not a blocker\n│   └── Is a fix available?\n│       ├── YES --> Update to the patched version\n│       └── NO --> Check for workarounds, consider replacing the dependency, or add to allowlist with a review date\n├── Severity: moderate\n│   ├── Reachable in production? --> Fix in the next release cycle\n│   └── Dev-only? --> Fix when convenient, track in backlog\n└── Severity: low\n    └── Track and fix during regular dependency updates\n```\n\n**Key questions:**\n- Is the vulnerable function actually called in your code path?\n- Is the dependency a runtime dependency or dev-only?\n- Is the vulnerability exploitable given your deployment context (e.g., a server-side vulnerability in a client-only app)?\n\nWhen you defer a fix, document the reason and set a review date.\n\n### Supply-Chain Hygiene\n\nDo not assume npm or treat the nearest manifest as the install root. Apply this order:\n\n1. **Find the installation boundary and manager.** Use the workspace root that owns the lockfile, or an independent nested project only when it is outside that workspace. There, corroborate `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`.\n2. **Block dependency scripts before first execution.** Bootstrap with scripts disabled or a documented fail-closed policy, inspect the pending script source, approve only the minimum required packages, commit the policy, then verify with a clean frozen/immutable install. Never blanket-approve scripts.\n\nAudits only find known advisories; they do not catch a newly malicious or typosquatted package. Therefore:\n\n- **Never apply forced audit remediation automatically** (`npm audit fix --force` or equivalent). Preview the remediation, read changelogs, and test each resulting upgrade; forced fixes may cross declared dependency ranges.\n- **Verify registry signatures and provenance where supported** (`npm audit signatures`, `pnpm audit signatures`) and treat absence as a signal to investigate, not automatic proof of compromise.\n- **Review new dependencies, lockfile diffs, and script-policy changes together** — ownership, maintenance, release age, provenance, transitive graph, and typosquats such as `cross-env` vs `crossenv` (OWASP **A06**, **LLM03**).\n\n## Rate Limiting\n\n```typescript\nimport rateLimit from 'express-rate-limit';\n\n// General API rate limit\napp.use('/api/', rateLimit({\n  windowMs: 15 * 60 * 1000, // 15 minutes\n  max: 100,                   // 100 requests per window\n  standardHeaders: true,\n  legacyHeaders: false,\n}));\n\n// Stricter limit for auth endpoints\napp.use('/api/auth/', rateLimit({\n  windowMs: 15 * 60 * 1000,\n  max: 10,  // 10 attempts per 15 minutes\n}));\n```\n\n**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`):\n\n```typescript\nimport { Ratelimit } from '@upstash/ratelimit';\nimport { Redis } from '@upstash/redis';\n\nconst authLimiter = new Ratelimit({\n  redis: Redis.fromEnv(),                       // UPSTASH_REDIS_REST_URL + _TOKEN\n  limiter: Ratelimit.slidingWindow(10, '15 m'), // 10 attempts per 15 minutes, across all instances\n});\nconst { success } = await authLimiter.limit(`login:${req.ip}`);\nif (!success) return res.status(429).end();\n```\n\n## Secrets Management\n\n```\n.env files:\n  ├── .env.example  → Committed (template with placeholder values)\n  ├── .env          → NOT committed (contains real secrets)\n  └── .env.local    → NOT committed (local overrides)\n\n.gitignore must include:\n  .env\n  .env.local\n  .env.*.local\n  *.pem\n  *.key\n```\n\n**Always check before committing:**\n```bash\n# Check for accidentally staged secrets\ngit diff --cached | grep -i \"password\\|secret\\|api_key\\|token\"\n```\n\n**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.\n\n## Data Privacy & Compliance\n\nSecuring 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.\n\n**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:\n\n| Class | Examples | Handling |\n|---|---|---|\n| **Non-personal** | Aggregates, anonymized counts | Normal handling |\n| **Personal (PII)** | Name, email, IP, device/user IDs | Minimize, access-control, include in export/delete |\n| **Sensitive** | Health, finance, location, biometrics, gov IDs, anything about minors | Extra basis to collect, stricter access, often encryption + audit logging |\n\n**Operating rules:**\n- **Minimize and set a purpose.** Collect a field only against a stated use. \"It might be useful later\" is not a purpose — it's latent breach scope. Don't log PII into telemetry (the `observability-and-instrumentation` skill makes the same point from the ops side).\n- **Set retention up front, then actually delete.** Every personal-data store needs a TTL and a working deletion path — including backups, caches, search indexes, and analytics copies. Data with no expiry is a breach scheduled for later.\n- **Support the data-subject rights your jurisdiction requires** (GDPR/CCPA and kin): export, correct, and delete on request. These are engineering features — design the schema so a user's data is *findable* and *erasable*, not smeared irreversibly across systems.\n- **Get consent before collection or third-party sharing**, and make it auditable. Sending PII to an analytics/ad/LLM vendor is \"sharing\" — the user's choice gates it, and the vendor needs a data-processing agreement.\n- **Localize defaults, don't hardcode one region's law.** Data-residency and rules differ by user location; make the policy a configurable boundary, not an assumption.\n\nWhen 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.\n\n## Securing AI / LLM Features\n\nIf 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)](https://genai.owasp.org/llm-top-10/):\n\n- **Treat all model output as untrusted input (LLM05: Improper Output Handling).** Never pass LLM output straight into `eval`, SQL, a shell, `innerHTML`, or a file path. Validate and encode it exactly as you would raw user input.\n- **Assume prompts can be hijacked (LLM01: Prompt Injection).** Untrusted text in the context window — a user message, a fetched web page, a PDF — can carry instructions. The system prompt is not a security boundary; enforce permissions in code, not in the prompt.\n- **Keep secrets and other users' data out of prompts (LLM02 / LLM07).** Anything in the context can be echoed back. Don't put API keys, cross-tenant data, or the full system prompt where the model can repeat it.\n- **Constrain tool and agent permissions (LLM06: Excessive Agency).** Scope tools to the minimum, require confirmation for destructive or irreversible actions, and validate every tool argument.\n- **Bound consumption (LLM10: Unbounded Consumption).** Cap tokens, request rate, and loop/recursion depth so a crafted input can't run up cost or hang the system.\n- **Isolate retrieval data (LLM08: Vector and Embedding Weaknesses).** In RAG, treat the vector store as a trust boundary: partition embeddings per tenant so one user can't retrieve another's data, and validate documents before indexing so poisoned content can't steer answers.\n\n```typescript\n// BAD: trusting model output as a command or as markup\nconst sql = await llm.generate(`Write SQL for: ${userQuestion}`);\nawait db.query(sql);                                   // arbitrary query execution\ncontainer.innerHTML = await llm.reply(userMessage);   // stored XSS, via the model\n\n// GOOD: model output is data — parse defensively, then validate, then encode\nlet intent;\ntry {\n  intent = CommandSchema.parse(JSON.parse(await llm.replyJson(userMessage)));\n} catch {\n  throw new ValidationError('unexpected model output'); // JSON.parse or schema failed\n}\nawait runAllowlistedAction(intent.action, intent.params);\ncontainer.textContent = await llm.reply(userMessage);\n```\n\n## Security Review Checklist\n\n```markdown\n### Authentication\n- [ ] Passwords hashed with bcrypt/scrypt/argon2 (salt rounds ≥ 12)\n- [ ] Session tokens are httpOnly, secure, sameSite\n- [ ] Login has rate limiting\n- [ ] Password reset tokens expire\n\n### Authorization\n- [ ] Every endpoint checks user permissions\n- [ ] Users can only access their own resources\n- [ ] Admin actions require admin role verification\n\n### Input\n- [ ] All user input validated at the boundary\n- [ ] SQL queries are parameterized\n- [ ] HTML output is encoded/escaped\n- [ ] Server-side URL fetches are allowlisted (no SSRF to internal services)\n- [ ] Delete/move/overwrite targets built from data are checked against an allowlisted root, a minimum depth, and ownership evidence read before the operation\n\n### Data\n- [ ] No secrets in code or version control\n- [ ] Sensitive fields excluded from API responses\n- [ ] PII encrypted at rest (if applicable)\n- [ ] Personal data is classified, collected against a stated purpose, and minimized\n- [ ] Personal data has a retention limit and a working deletion path (incl. backups/indexes)\n- [ ] Export/delete (data-subject) requests are supported where required; sharing with third parties has consent\n\n### Infrastructure\n- [ ] Security headers configured (CSP, HSTS, etc.)\n- [ ] CORS restricted to known origins\n- [ ] Dependencies audited for vulnerabilities\n- [ ] Error messages don't expose internals\n\n### Supply Chain\n- [ ] One authoritative lockfile committed; CI uses that manager's frozen/immutable install\n- [ ] Native audit triaged by reachability and fix risk; dependency install scripts blocked unless explicitly approved\n- [ ] New dependencies reviewed (ownership, provenance, release age, transitive graph)\n\n### AI / LLM (if used)\n- [ ] Model output treated as untrusted (no eval/SQL/innerHTML/shell)\n- [ ] Secrets and other users' data kept out of prompts\n- [ ] Tool/agent permissions scoped; destructive actions require confirmation\n```\n## See Also\n\nFor detailed security checklists and pre-commit verification steps, see `../../references/security-checklist.md`.\n\n## Common Rationalizations\n\n| Rationalization | Reality |\n|---|---|\n| \"This is an internal tool, security doesn't matter\" | Internal tools get compromised. Attackers target the weakest link. |\n| \"We'll add security later\" | Security retrofitting is 10x harder than building it in. Add it now. |\n| \"No one would try to exploit this\" | Automated scanners will find it. Security by obscurity is not security. |\n| \"The framework handles security\" | Frameworks provide tools, not guarantees. You still need to use them correctly. |\n| \"It's just a prototype\" | Prototypes become production. Security habits from day one. |\n| \"Threat modeling is overkill here\" | Five minutes of \"how would I attack this?\" prevents the design flaws no control can patch later. |\n| \"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. |\n| \"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. |\n| \"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. |\n| \"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. |\n| \"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. |\n\n## Red Flags\n\n- User input passed directly to database queries, shell commands, or HTML rendering\n- A delete, move, or overwrite whose target comes from a payload, a config value, or another process's command line, guarded only by a shape check on the path\n- Secrets in source code or commit history\n- API endpoints without authentication or authorization checks\n- Missing CORS configuration or wildcard (`*`) origins\n- No rate limiting on authentication endpoints, or an in-memory limiter in front of more than one instance\n- Stack traces or internal errors exposed to users\n- Dependencies with known critical vulnerabilities, competing lockfiles at one installation boundary, non-reproducible installs, or blanket-approved scripts\n- Server fetches user-supplied URLs without an allowlist (SSRF)\n- LLM/model output passed into a query, the DOM, a shell, or `eval`\n- Secrets, PII, or the full system prompt placed inside an LLM context window\n- Personal data collected with no stated purpose, retention limit, or deletion path\n- PII sent to analytics/ad/LLM vendors with no consent or data-processing agreement\n- \"Delete my account\" that only flips a flag while the personal data lingers in stores and backups\n\n## Verification\n\nAfter implementing security-relevant code:\n\n- [ ] The native audit has no unmitigated reachable critical/high findings; CI preserves the authoritative lockfile and blocks unreviewed dependency scripts\n- [ ] No secrets in source code or git history\n- [ ] All user input validated at system boundaries\n- [ ] Destructive filesystem operations resolve symlinks, then verify allowlisted root, minimum depth, and ownership before running\n- [ ] Authentication and authorization checked on every protected endpoint\n- [ ] Security headers present in response (check with browser DevTools)\n- [ ] Error responses don't expose internal details\n- [ ] Rate limiting active on auth endpoints, backed by a shared store when more than one instance serves traffic\n- [ ] Server-side URL fetches validated against an allowlist (no SSRF)\n- [ ] LLM/model output validated and encoded before use (if AI features present)\n- [ ] Personal data is classified, minimized to a stated purpose, and has a retention limit\n- [ ] Deletion and export requests work end-to-end (including backups, caches, and analytics copies)\n"
}

SHA-256: 61b3c1f217358742738ab862a31775d403556e3a97c838389f3cec6259add6ce