SKILL.md
Input-Validation & Trust-Boundary Audit + Hardening Plan
Degree of freedom: HIGH — map trust boundaries, score gaps, emit a plan. Stay plan-only. No Zod, sanitizers, or webhook edits until approved.
This skill vs neighbors
| Skill | Owns |
|---|---|
| plan-input-validation (this) | Trust-boundary / injection plan |
enhance-web-forms |
Apply production form quality |
plan-security-audit |
OWASP umbrella burndown |
Role: Senior application security engineer (trust-boundary lens).
Task: Map every point untrusted data enters, score validate/sanitize/authenticate gaps, phase remediations, emit plan-input-validation.md. Audit & plan only — no code changes until each phase is approved.
Walk every boundary. Find what's trusted that shouldn't be. Change nothing until approved.
How to reason (every plan item)
- Propose — schema, sanitize, signature verify, or allowlist
- Risk — forgeable money/data, stored XSS, or injection
- Keep-working — boundaries that already validate + authenticate
- Phase — Forgeable paths → XSS → Schema pass → Uploads (do not execute)
Worked example
Propose: verify the Stripe webhook with
constructEventon the raw body; reject empty secrets; return 400 on bad signatures.
Risk: empty-signing-secret bypass — anyone can forgeinvoice.paidand credit quota.
Keep-working: checkout session creation already uses the server-side secret.
Phase: Phase 1 — Forgeable money/data paths.
Note: origin proof ≠ safe to interpolate into SQL/HTML.
AI agents write code that works on the inputs you showed them. Two signature patterns recur: dangerouslySetInnerHTML without DOMPurify (XSS), and webhook handlers without real signature verification — the reported empty-signing-secret bypass class, where an empty secret lets any attacker forge valid signatures and credit unlimited quota without payment.
This skill is the audit-and-plan half. Execution goes to backend-patterns / backend-error-handling / audit-security after you approve each phase.
When this fires
Trigger phrases: "validate my inputs", "is this injection-safe", "check my forms / API", "XSS", "dangerouslySetInnerHTML", "my Stripe webhook", "can requests be forged", "sanitize user content", "pre-launch input hardening".
Do not fire for: row-level access (plan-rls-audit), credential exposure (plan-secrets-audit), or broad architecture review (plan-security-audit). This skill owns the boundary where untrusted data enters.
The four boundary classes [HIGH freedom]
1 · Form & API input
- No schema validation — bodies/params/query without Zod (or equivalent).
- Type-coerced trust —
Number(req.body.amount)with no bounds. - Missing field-level checks — email format, length caps, enum membership.
- Mass assignment — spreading
req.bodyinto DB insert/update (role,
is_admin, credits).
- SQL/RPC injection — string-interpolated queries or raw user input into SQL.
2 · Rendered untrusted content (XSS)
dangerouslySetInnerHTML/v-html/innerHTMLwithout DOMPurify.- URL/attribute injection —
javascript:URIs, unvalidated redirects. - Stored XSS — content saved now, rendered raw later.
3 · Webhooks & forged requests (Stripe-aware)
- Signature not verified — or verified against empty secret
(empty-secret bypass class).
- Raw-body mistake —
JSON.stringify(req.body)instead of raw bytes. - No idempotency — Stripe at-least-once retries double-process.
- Cross-gateway trust — fulfilling without checking callback source.
- Missing timestamp tolerance — replay window open.
- Returns 500 not 400 on bad signature → infinite Stripe retries.
4 · File uploads & other boundaries
- No type/size/MIME validation; trusting client content-type.
- Path traversal in filenames; SSRF in user-supplied URLs.
- Concurrency — read-modify-write races without DB constraints.
Procedure [HIGH freedom — plan only]
- Map boundaries. Enumerate every untrusted entry point. Skip absent ones.
- Test each. For every boundary: validated? sanitized? authenticated?
- Score. Severity = reachability × impact.
- Phase into shippable groups mapped to execution skills.
- Emit
plan-input-validation.md. End the turn. Do not edit code.
Guardrails
- Plan only. No Zod schemas, sanitizers, or webhook config changes.
- Validate at the boundary, not after.
- Signature ≠ safety. Origin proof ≠ safe to interpolate into SQL/HTML.
- Don't trust the client copy. Browser-only checks are UX, not security.
- Stack-specific raw-body note. Call out Next.js + Stripe raw-body requirement.
- Minimal quoting of source.
Self-critique before the burndown [LOW freedom — do not skip]
- evidenced-not-assumed — every boundary cites path:line; skip classes that do not exist
- plan-only — no Zod schemas, DOMPurify, or webhook config
- severity/phase justified — unauthenticated write / forgeable money is Critical
- right-owner — RLS row access →
plan-rls-audit; leaked keys →plan-secrets-audit; OWASP umbrella →plan-security-audit - no-false-safety — browser-only checks are UX; signature ≠ sanitization; empty webhook secret is a bypass
Report template — plan-input-validation.md
# Input-Validation & Trust-Boundary Audit — <repo>
_Audit-only. Nothing changes until each phase is approved._
## Scope
- Boundaries found: forms / API / webhooks / uploads / rendered content
- Stack: Supabase ☐ Stripe ☐ Next.js ☐ | Assumptions: …
## Verdict
| Boundary class | Findings | Critical | Unauthenticated write reachable? |
|----------------|----------|----------|----------------------------------|
| Form & API | n | n | … |
| Rendered (XSS) | n | n | … |
| Webhooks | n | n | … |
| Uploads/other | n | n | … |
## Findings
| # | Boundary | path:line | Missing: validate/sanitize/authenticate | Sev | Direction |
|---|----------|-----------|-----------------------------------------|-----|-----------|
| I1 | Stripe webhook | api/webhooks/stripe.ts:12 | authenticate (no constructEvent) | Crit | verify raw body against secret; 400 on fail |
| I2 | comment render | Comment.tsx:30 | sanitize (dangerouslySetInnerHTML) | High | DOMPurify before render |
| I3 | profile update | actions/profile.ts:8 | validate (mass assignment) | High | Zod allowlist; drop role/credits |
## Phased burndown
- **Phase 1 — Forgeable money/data paths** → `backend-patterns` — webhooks, mass-assign
- **Phase 2 — XSS / rendered content** → `backend-error-handling` / `enhance-web-ux` — I2…
- **Phase 3 — Schema validation pass** → `backend-patterns` — Zod at every boundary
- **Phase 4 — Uploads & concurrency** → `audit-security` — files, races
## Execution handoff
Approve a phase to run it. Re-run after; for webhooks, verify with Stripe CLI
fixtures (real signed events) not mocked payloads.
Chains with
- Security spine — entry layer (this skill); data access:
plan-rls-audit;
credentials: plan-secrets-audit.
- Execution:
backend-patterns,backend-error-handling,audit-security,
audit-fe-api.
- Verify:
test-red-team+ Stripe CLI signed webhook fixtures.
Plan with a strong model; execute with
composer-2.5-execution.mdcriding
along. The plan says which boundaries are open; the rule constrains how
they're closed.