Security Hunter
Audit code for security vulnerabilities — places where untrusted input flows into sensitive operations without
validation, secrets are embedded in source, defaults are permissive, or auth checks are missing. The goal: every
trust boundary validates its inputs, no secrets live in code, and the principle of least privilege is applied
throughout.
When to Use
- Reviewing code before production deployment
- Auditing trust boundaries (API endpoints, form handlers, webhooks)
- Preparing for a formal security review or pen test
- Onboarding third-party integrations or new external data sources
- Hardening an application after a security incident
Core Principles
Trust boundaries are the perimeter. Every point where external data enters the system — HTTP requests, file
uploads, environment variables, database reads, third-party API responses — is a trust boundary. Input must be
validated, sanitized, or escaped before it flows into operations that could be exploited.
Secrets belong in the environment, never in code. API keys, passwords, tokens, certificates, and connection
strings must come from environment variables, secret managers, or encrypted config — never from source files,
comments, or committed config.
Default to deny. Defaults should be restrictive: CORS origins explicit not *, auth required not optional,
permissions minimal not broad. Permissive defaults are the most common class of security misconfiguration.
Defense in depth. No single validation layer is sufficient. Validate at the boundary, escape at the output,
enforce at the data layer. If one layer fails, another should catch the exploit.
Fail closed. When auth/authz checks fail or throw, the result should be denial, not access. Error paths must
not bypass security controls.
Type-safe ≠ runtime safe. TypeScript types are erased at runtime. (await response.json()) as User compiles but
does not validate — an attacker controls the wire format. Every external payload must pass schema validation before
it enters typed business logic; derive downstream types from the schema (z.infer<typeof schema>).
What to Hunt
1. Hardcoded Secrets
API keys, passwords, tokens, or connection strings embedded directly in source code.
Signals:
- String literals matching key patterns:
sk_live_, AKIA, ghp_, Bearer , password=
- Connection strings with embedded credentials:
postgres://user:pass@host
- Base64-encoded strings in auth headers or config
.env files or secret-bearing config committed to the repo
- Comments containing credentials ("temporary key: xyz123")
Action: Move to environment variables or a secret manager. Add the pattern to .gitignore. Rotate any committed
secret immediately — it's already compromised if the repo was ever shared.
2. Injection Vulnerabilities
Untrusted input concatenated into strings that are interpreted as code, queries, or commands.
Signals:
- SQL: string concatenation or template literals in query strings instead of parameterized queries
- Command:
exec(), spawn(), system() with user-controlled arguments
- Path traversal: user input in
fs.readFile(), path.join(), or URL construction without sanitization
- Template injection: user input rendered in server-side templates without escaping
- Regex: user input passed directly to
new RegExp() (ReDoS risk)
eval(), Function(), setTimeout(string) with dynamic input
Action: Use parameterized queries, allowlists, path canonicalization, template auto-escaping, or regex escaping.
Never concatenate untrusted input into interpreted strings.
3. Missing Input Validation at Trust Boundaries
Endpoints, handlers, or integration points that accept external data without schema validation or sanitization.
Signals:
- API handlers that destructure
req.body or req.query without validation (no Zod, Joi, class-validator, etc.)
- File upload handlers without type/size/content validation
- Webhook handlers without signature verification
- URL parameters used directly in business logic without parsing/validation
- Form inputs passed to database operations without sanitization
(await response.json()) as T, JSON.parse(...) as T, or req.body as T — compile-time cast with no runtime schema
- Validation schema exists in one layer but downstream functions accept manually duplicated types instead of
z.infer<typeof schema>
Action: Add schema validation at every trust boundary. Validate type, format, range, and length. Reject invalid
input explicitly — don't coerce or default. Parse with Zod/Joi/class-validator and type downstream code from the schema
inference (type User = z.infer<typeof UserSchema>), not a parallel manual interface.
4. Insecure Defaults and Configuration
Permissive defaults that weaken security posture when not explicitly overridden.
Signals:
- CORS:
Access-Control-Allow-Origin: * or credentials: true with wildcard origin
- CSRF: protection disabled or missing on state-changing endpoints
- Cookie: missing
Secure, HttpOnly, or SameSite attributes
- TLS: certificate validation disabled (
rejectUnauthorized: false)
- Rate limiting: absent on auth endpoints or public APIs
- Debug/dev mode enabled in production config
- Permissive CSP headers or missing security headers
Action: Restrict defaults. Require explicit opt-in for permissive settings with justification.
5. Authentication and Authorization Gaps
Missing or inconsistent auth checks that allow unauthorized access or privilege escalation.
Signals:
- Routes/endpoints without auth middleware where peer routes have it
- Authorization checks that compare user IDs with string equality on user-controlled values
- Role checks using client-provided role claims without server-side verification
- Admin functionality accessible by changing a URL parameter or request body field
- Session tokens without expiry, rotation, or revocation
- Password handling: plain-text storage, weak hashing (MD5, SHA1), missing salting
Action: Ensure every endpoint has explicit auth/authz. Check authorization server-side against the session, not
client-provided claims. Use bcrypt/scrypt/argon2 for password hashing.
6. Sensitive Data Exposure
Personal data, secrets, or internal details leaked through logs, errors, responses, or storage.
Signals:
- Logging of request bodies, auth headers, or user PII
- Error responses that include stack traces, internal paths, or database details
- API responses that return more fields than the client needs (no field filtering)
- Sensitive data stored in localStorage, sessionStorage, or unencrypted cookies
- PII in URLs (email, SSN, tokens in query parameters — logged by default in most servers)
Action: Strip sensitive fields from logs and error responses. Filter API responses to only required fields. Use
encrypted/httpOnly storage for sensitive client-side data.
7. Dependency and Supply-Chain Risks
Package dependencies with known vulnerabilities, unvetted postinstall scripts, or lockfile integrity issues.
Signals:
package.json dependencies pinned to * or very wide ranges without a lockfile
- Lockfile (
package-lock.json, pnpm-lock.yaml, yarn.lock) not committed to the repo
- Packages with
postinstall, preinstall, or install lifecycle scripts that execute arbitrary code
- Dependencies with known CVEs (check via
npm audit or equivalent)
- Importing from CDNs or unpinned URLs at runtime
Action: Commit the lockfile. Pin dependency ranges. Run npm audit and address critical/high findings. Review
lifecycle scripts in new dependencies before adding them.
8. Unsafe Code Patterns
Language-level patterns that create exploitable conditions.
Signals:
innerHTML, dangerouslySetInnerHTML, document.write() with dynamic content (XSS)
Object.assign() or spread from untrusted input without allowlisting properties (prototype pollution)
JSON.parse() of untrusted input without a try/catch or schema validation
- Timing-sensitive comparisons of secrets (use constant-time comparison instead)
- Unrestricted file write paths derived from user input
- Deserialization of untrusted data without schema validation
__proto__ or constructor.prototype accessible via user-controlled object keys (prototype pollution)
Action: Use textContent instead of innerHTML, allowlist properties on untrusted objects, validate schemas
after parsing, use crypto.timingSafeEqual() for secret comparison. Use Object.create(null) for lookup maps
populated from user input to prevent prototype pollution.
9. TypeScript-Specific Security Patterns
Leveraging (or failing to leverage) TypeScript's type system for security enforcement.
Signals:
(await response.json()) as T or JSON.parse(untrusted) as T — typed appearance with no runtime validation
as any or as unknown as T bypassing validation at trust boundaries — input flows from any into typed
operations without runtime validation
- Branded types not used for security-sensitive strings (raw
string for SQL fragments, HTML content, URLs,
file paths) — allows accidental mixing of validated and unvalidated values
- Missing type guards at security validation boundaries —
typeof / instanceof checks that should narrow
to validated types but are plain boolean checks instead
strict: false or strictNullChecks: false in tsconfig.json — permissive compiler settings that weaken
security invariants
- Zod/Joi/class-validator schemas that exist but whose inferred types aren't used in downstream function
signatures — validation runs but the type system doesn't enforce it
@ts-ignore or @ts-expect-error suppressing errors in security-sensitive code (auth, validation, crypto)
Action: Replace cast-then-trust with parse-then-narrow: const user = UserSchema.parse(await response.json()).
Use branded types for security-sensitive values (type SafeHtml = string & { __brand: 'SafeHtml' }). Enable
strict: true in tsconfig.json. Use type predicates or assertion functions (asserts input is ValidatedRequest) at
trust boundaries. Derive all downstream types from validation schemas (z.infer<typeof schema>) — one source of truth
for runtime shape and compile-time types.
10. GraphQL and WebSocket Security
Missing protections specific to GraphQL APIs and WebSocket connections.
Signals:
- GraphQL: no query depth limiting (allows deeply nested queries that exhaust server resources)
- GraphQL: no query complexity analysis or cost limiting
- GraphQL: introspection enabled in production (exposes full schema to attackers)
- GraphQL: mutations without authentication or authorization checks in resolvers
- GraphQL: no persisted queries or query allowlisting in production
- WebSocket: missing origin validation on connection upgrade
- WebSocket: no authentication on WebSocket handshake (relying only on initial HTTP auth)
- WebSocket: no rate limiting on message frequency
- WebSocket: user input from messages used in operations without validation
- Server-Sent Events (SSE): no authentication or authorization on event streams
Action: Add query depth and complexity limits (e.g., graphql-depth-limit, graphql-query-complexity).
Disable introspection in production. Validate WebSocket origin headers. Authenticate WebSocket connections on
handshake. Rate-limit WebSocket messages. Validate all user input from WebSocket messages with schema validation.
11. SSRF, Weak Randomness, and JWT Verification Gaps
These are candidate signals, not grep-findings — each nominated site requires validation before it becomes a
finding.
Signals:
- SSRF candidates:
fetch(), axios(), http.request() where the URL is a variable rather than a literal.
A finding requires tracing user-controlled data to the request sink — the grep only nominates sites for
data-flow validation. Confirmed SSRF needs an allowlist of destinations or URL parsing + host validation.
- Weak randomness (nearly direct):
Math.random() used for tokens, session IDs, password-reset codes, or
anything security-bearing. Validate only that the value is security-relevant; the fix is
crypto.randomBytes() / crypto.randomUUID().
- JWT verification gaps (library-aware):
jwt.decode() where verification is expected — in jsonwebtoken,
decode() does not verify the signature (its own docs warn about this); jwt.verify() does. The finding
requires confirming which library and call path is in play — other libraries (jose, framework middleware)
have different APIs. Also check: algorithms not pinned on verify, and secrets accepted from user input.
Action: For each candidate, trace the data flow or library call path first. Report only validated findings;
list unvalidated candidates separately as "requires manual verification" when the trace was inconclusive.
Audit Workflow
Phase 1: Map Trust Boundaries
Resolve audit surface. The prompt may specify the scope as:
- Diff: files changed relative to the base branch — committed, staged, unstaged, and untracked
- Path: specific files, folders, or layers
- Codebase: the entire project (the default when unspecified; set
SCOPE=.)
Party mode: when the orchestrator supplies a scope snapshot (a resolved file list), use it verbatim and do
not re-resolve. The resolution below applies to standalone runs only.
For diff mode, resolve fail-closed:
BASE=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/@@')
if [ -z "$BASE" ]; then
for b in origin/main origin/master main master; do
git rev-parse -q --verify "$b" >/dev/null && BASE=$b && break
done
fi
# If BASE is still empty: STOP. Ask for an explicit base. Do not continue.
SCOPE=$( { git diff --name-only --diff-filter=d "$BASE"...HEAD;
git diff --name-only --diff-filter=d HEAD;
git ls-files --others --exclude-standard; } | sort -u )
DELETED=$( { git diff --name-only --diff-filter=D "$BASE"...HEAD;
git diff --name-only --diff-filter=D HEAD; } | sort -u )
If $SCOPE is empty, run no scans: write the report with "Audit completed: 0 findings — empty diff scope",
listing $DELETED under "Deleted in diff" if non-empty, and stop — deleted files matter here: a deleted
test or authorization check is security-relevant context. If the resolved surface exceeds what can be read
within the context budget, report the file count and ask to narrow or chunk.
Two surfaces. Findings are reported only against the target scope ($SCOPE) — every finding anchors
(file:line) there. Related files may still be read as context: trust-boundary mapping and data-flow
tracing (Phases 1, 3, 4) read the whole project, because in-scope input can reach sinks outside the scope and
vice versa — anchor the finding at its in-scope end.
Identify trust boundaries: API route handlers, webhooks, file upload endpoints, third-party integration points,
CLI argument parsers, config loaders.
Identify auth/authz middleware and where it's applied.
Phase 2: Scan for Security Signals
Run every scan against the target scope (SCOPE=. in codebase mode). Two exclusion profiles apply:
- Secrets and dependency scans use the minimal profile — only
node_modules is excluded. Tests and generated
files stay in scope: a live key in a test fixture is exactly as compromised as one in src/, and generated
artifacts can embed committed credentials too.
- All other scans use the production profile.
# Minimal profile — secrets and dependency scans only
EXCLUDE_MINIMAL='--glob !**/node_modules/**'
# Production profile — everything else
EXCLUDE='--glob !**/node_modules/** --glob !**/dist/** --glob !**/*.generated.* --glob !**/__generated__/** --glob !**/*.g.ts --glob !**/generated/** --glob !**/*.test.* --glob !**/*.spec.* --glob !**/*.e2e.* --glob !**/__tests__/**'
# Hardcoded secrets (common patterns) — minimal profile, all file types
rg --pcre2 '(sk_live_|AKIA|ghp_|password\s*=\s*["\x27]|secret\s*=\s*["\x27])' $EXCLUDE_MINIMAL -- $SCOPE
rg --pcre2 '(postgres|mysql|mongodb)://\w+:\w+@' $EXCLUDE_MINIMAL -- $SCOPE
# Injection: eval, exec, spawn, Function constructor
rg '\beval\s*\(|\bexec\s*\(|\bspawn\s*\(|\bFunction\s*\(' --type ts $EXCLUDE -- $SCOPE
# TypeScript-specific security
rg 'as any|as unknown' --type ts $EXCLUDE -- $SCOPE
rg --pcre2 '\.json\(\)\)\s+as\s|\bJSON\.parse\([^)]+\)\s+as\s' --type ts $EXCLUDE -- $SCOPE
rg 'strict.*false|strictNullChecks.*false' --glob 'tsconfig*.json'
rg '@ts-ignore|@ts-expect-error' --type ts $EXCLUDE -- $SCOPE
# GraphQL security
rg 'introspection|depthLimit|queryComplexity' --type ts $EXCLUDE -- $SCOPE
# WebSocket security
rg 'WebSocket|socket\.io|ws\.' --type ts $EXCLUDE -- $SCOPE
# Injection: string concatenation in queries
rg --pcre2 '(query|sql|execute)\s*\(\s*[`"\x27].*\$\{' --type ts $EXCLUDE -- $SCOPE
# XSS: innerHTML, dangerouslySetInnerHTML, document.write
rg 'innerHTML|dangerouslySetInnerHTML|document\.write' --type ts $EXCLUDE -- $SCOPE
# Insecure config
rg --pcre2 'Access-Control-Allow-Origin.*\*|rejectUnauthorized.*false|secure.*false' $EXCLUDE -- $SCOPE
# Missing auth (route definitions without auth middleware — project-specific, adapt pattern)
rg '(app|router)\.(get|post|put|patch|delete)\s*\(' --type ts $EXCLUDE -- $SCOPE
# Logging of sensitive data
rg --pcre2 'console\.(log|info|debug).*\b(password|token|secret|authorization|cookie)\b' -i $EXCLUDE -- $SCOPE
# Regex from user input
rg 'new RegExp\(' --type ts $EXCLUDE -- $SCOPE
# SSRF candidates (variable URLs — nominate for data-flow validation, §11)
rg --pcre2 '(fetch|axios(\.\w+)?|https?\.request)\(\s*[a-zA-Z_$`]' --type ts $EXCLUDE -- $SCOPE
# Weak randomness for security values (§11)
rg 'Math\.random\(' --type ts $EXCLUDE -- $SCOPE
# JWT: decode without verify (library-aware validation required, §11)
rg '\bjwt\.decode\(|\.decode\(.*token' --type ts $EXCLUDE -- $SCOPE
# Dependency risks: lifecycle scripts in package.json — minimal profile
rg '"(pre|post)?install"' --glob 'package.json' $EXCLUDE_MINIMAL
Phase 3: Trace Data Flows
For each trust boundary found in Phase 1:
- Does untrusted input pass through validation before reaching sensitive operations?
- Are query parameters, body fields, and headers validated for type, format, and range?
- Could a malicious input reach
eval, exec, a database query, or DOM manipulation?
Phase 4: Evaluate Auth Coverage
- List all routes/endpoints. For each: is auth middleware applied?
- Identify authorization logic. Is it server-side? Does it rely on client claims?
- Check session management: expiry, rotation, revocation.
Phase 5: Produce Report
Output Format
Save as YYYY-MM-DD-security-hunter-audit-{model-name}.md — {model-name} is the executing model's short name
(e.g. fable-5) — in the project's docs folder (or project root if no docs folder exists). If the caller specifies
an output path (e.g. the party-hunter orchestrator), it overrides this default.
Severity levels, used for per-finding labels and the Recommendations grouping:
- Critical — exploitable now, causes data loss, or breaks behavior on production paths.
- High — a defect with likely user-visible, security, or reliability impact if left unaddressed.
- Medium — correctness or maintainability risk without imminent impact.
- Low — hygiene; no behavioral risk.
# Security Hunter Audit — {date}
## Scope
- Surface: {diff / path / codebase}
- Files: {count or list}
- Exclusions: {list — note the minimal profile used for secrets/dependency scans}
- {Deleted in diff: {list} — only for diff scope with deletions}
- Audit completed: {N} findings
## Trust Boundary Map
| # | Boundary | Location | Validation | Auth |
| - | -------- | -------- | ---------- | ---- |
| 1 | POST /api/users | file:line | Zod schema | JWT middleware |
| 2 | Webhook /hooks/stripe | file:line | None | None |
## Findings
### Hardcoded Secrets
| # | Location | Pattern | Severity | Action |
| - | -------- | ------- | -------- | ------ |
| 1 | file:line | `sk_live_...` Stripe key | Critical | Move to env, rotate immediately |
### Injection Risks
| # | Location | Type | Untrusted Input | Action |
| - | -------- | ---- | --------------- | ------ |
| 1 | file:line | SQL injection | `req.query.id` concatenated into query | Use parameterized query |
### Missing Input Validation
| # | Location | Boundary | Input | Action |
| - | -------- | -------- | ----- | ------ |
| 1 | file:line | POST /api/orders | `req.body` used without validation | Add Zod/Joi schema |
### Insecure Defaults
| # | Location | Setting | Current | Action |
| - | -------- | ------- | ------- | ------ |
| 1 | file:line | CORS origin | `*` | Restrict to allowed origins |
### Auth/Authz Gaps
| # | Location | Endpoint | Issue | Action |
| - | -------- | -------- | ----- | ------ |
| 1 | file:line | GET /admin/users | No auth middleware | Add admin auth |
### Sensitive Data Exposure
| # | Location | Data | Channel | Action |
| - | -------- | ---- | ------- | ------ |
| 1 | file:line | Auth token | console.log | Remove from logs |
### Unsafe Patterns
| # | Location | Pattern | Risk | Action |
| - | -------- | ------- | ---- | ------ |
| 1 | file:line | `innerHTML = userInput` | XSS | Use `textContent` |
### Dependency / Supply-Chain Risks
| # | Location | Issue | Severity | Action |
| - | -------- | ----- | -------- | ------ |
| 1 | package.json | No lockfile committed | High | Commit lockfile |
### SSRF / Randomness / JWT Candidates (validated)
| # | Location | Candidate | Validation Result | Action |
| - | -------- | --------- | ----------------- | ------ |
| 1 | file:line | `fetch(userUrl)` | User input reaches sink unfiltered | Allowlist destinations |
## Recommendations (Priority Order)
1. **Critical**: {hardcoded secrets to rotate, injection vulnerabilities, auth bypasses, confirmed SSRF}
2. **High**: {missing input validation, insecure defaults, auth gaps, dependency CVEs, weak randomness for tokens}
3. **Medium**: {sensitive data exposure, unsafe patterns, supply-chain hygiene}
4. **Low**: {hardening opportunities without a concrete exploit path}
Operating Constraints
- No code edits. This skill produces an audit report only. Implementation is a separate step.
- No empty finding sections. Include only categories with findings. Omit a heading, table, or list entirely when it would contain zero items — do not include empty tables, placeholder subsections, or negative statements like "no dead exports", "none found", or "no issues". Execution status is exempt: the "Audit completed: N findings" line in the Scope section is always present, even at zero findings.
- Scope: security vulnerabilities only. If a finding doesn't answer "could this be exploited?", it belongs to
another hunter — do not flag it here. Boundary with invariant-hunter: it flags the unsafe cast and loose typing
at trust boundaries; this hunter flags the exploitable boundary and recommends schema +
z.infer<>.
- Evidence required. Every finding must cite
file/path.ext:line with the exact code.
- Severity matters. Use the Critical / High / Medium / Low definitions above. A hardcoded production secret is
not the same severity as a missing
SameSite cookie attribute. Prioritize accordingly.
- Context over pattern-matching. A
new RegExp() with a hardcoded pattern is fine. A new RegExp(userInput) is
a ReDoS risk. Grep finds both — judgment separates them.
- No false confidence. This audit catches common patterns, not all vulnerabilities. It is not a substitute for
professional penetration testing or automated security scanning.
1---2name: security-hunter-ts3description: Audit TypeScript code for security vulnerabilities — hardcoded secrets, injection risks, missing input validation at trust boundaries, insecure defaults, auth gaps, sensitive data exposure, and unsafe patterns like eval or innerHTML. Use when: reviewing TypeScript code before deployment, auditing trust boundaries, preparing for a security review, onboarding third-party integrations, or hardening an application.4---56# Security Hunter78Audit code for **security vulnerabilities** — places where untrusted input flows into sensitive operations without9validation, secrets are embedded in source, defaults are permissive, or auth checks are missing. The goal: **every10trust boundary validates its inputs, no secrets live in code, and the principle of least privilege is applied11throughout.**1213## When to Use1415- Reviewing code before production deployment16- Auditing trust boundaries (API endpoints, form handlers, webhooks)17- Preparing for a formal security review or pen test18- Onboarding third-party integrations or new external data sources19- Hardening an application after a security incident2021## Core Principles22231. **Trust boundaries are the perimeter.** Every point where external data enters the system — HTTP requests, file24 uploads, environment variables, database reads, third-party API responses — is a trust boundary. Input must be25 validated, sanitized, or escaped before it flows into operations that could be exploited.26272. **Secrets belong in the environment, never in code.** API keys, passwords, tokens, certificates, and connection28 strings must come from environment variables, secret managers, or encrypted config — never from source files,29 comments, or committed config.30313. **Default to deny.** Defaults should be restrictive: CORS origins explicit not `*`, auth required not optional,32 permissions minimal not broad. Permissive defaults are the most common class of security misconfiguration.33344. **Defense in depth.** No single validation layer is sufficient. Validate at the boundary, escape at the output,35 enforce at the data layer. If one layer fails, another should catch the exploit.36375. **Fail closed.** When auth/authz checks fail or throw, the result should be denial, not access. Error paths must38 not bypass security controls.39406. **Type-safe ≠ runtime safe.** TypeScript types are erased at runtime. `(await response.json()) as User` compiles but41 does not validate — an attacker controls the wire format. Every external payload must pass schema validation before42 it enters typed business logic; derive downstream types from the schema (`z.infer<typeof schema>`).4344## What to Hunt4546### 1. Hardcoded Secrets4748API keys, passwords, tokens, or connection strings embedded directly in source code.4950**Signals:**5152- String literals matching key patterns: `sk_live_`, `AKIA`, `ghp_`, `Bearer `, `password=`53- Connection strings with embedded credentials: `postgres://user:pass@host`54- Base64-encoded strings in auth headers or config55- `.env` files or secret-bearing config committed to the repo56- Comments containing credentials ("temporary key: xyz123")5758**Action:** Move to environment variables or a secret manager. Add the pattern to `.gitignore`. Rotate any committed59secret immediately — it's already compromised if the repo was ever shared.6061### 2. Injection Vulnerabilities6263Untrusted input concatenated into strings that are interpreted as code, queries, or commands.6465**Signals:**6667- SQL: string concatenation or template literals in query strings instead of parameterized queries68- Command: `exec()`, `spawn()`, `system()` with user-controlled arguments69- Path traversal: user input in `fs.readFile()`, `path.join()`, or URL construction without sanitization70- Template injection: user input rendered in server-side templates without escaping71- Regex: user input passed directly to `new RegExp()` (ReDoS risk)72- `eval()`, `Function()`, `setTimeout(string)` with dynamic input7374**Action:** Use parameterized queries, allowlists, path canonicalization, template auto-escaping, or regex escaping.75Never concatenate untrusted input into interpreted strings.7677### 3. Missing Input Validation at Trust Boundaries7879Endpoints, handlers, or integration points that accept external data without schema validation or sanitization.8081**Signals:**8283- API handlers that destructure `req.body` or `req.query` without validation (no Zod, Joi, class-validator, etc.)84- File upload handlers without type/size/content validation85- Webhook handlers without signature verification86- URL parameters used directly in business logic without parsing/validation87- Form inputs passed to database operations without sanitization88- `(await response.json()) as T`, `JSON.parse(...) as T`, or `req.body as T` — compile-time cast with no runtime schema89- Validation schema exists in one layer but downstream functions accept manually duplicated types instead of90 `z.infer<typeof schema>`9192**Action:** Add schema validation at every trust boundary. Validate type, format, range, and length. Reject invalid93input explicitly — don't coerce or default. Parse with Zod/Joi/class-validator and type downstream code from the schema94inference (`type User = z.infer<typeof UserSchema>`), not a parallel manual interface.9596### 4. Insecure Defaults and Configuration9798Permissive defaults that weaken security posture when not explicitly overridden.99100**Signals:**101102- CORS: `Access-Control-Allow-Origin: *` or `credentials: true` with wildcard origin103- CSRF: protection disabled or missing on state-changing endpoints104- Cookie: missing `Secure`, `HttpOnly`, or `SameSite` attributes105- TLS: certificate validation disabled (`rejectUnauthorized: false`)106- Rate limiting: absent on auth endpoints or public APIs107- Debug/dev mode enabled in production config108- Permissive CSP headers or missing security headers109110**Action:** Restrict defaults. Require explicit opt-in for permissive settings with justification.111112### 5. Authentication and Authorization Gaps113114Missing or inconsistent auth checks that allow unauthorized access or privilege escalation.115116**Signals:**117118- Routes/endpoints without auth middleware where peer routes have it119- Authorization checks that compare user IDs with string equality on user-controlled values120- Role checks using client-provided role claims without server-side verification121- Admin functionality accessible by changing a URL parameter or request body field122- Session tokens without expiry, rotation, or revocation123- Password handling: plain-text storage, weak hashing (MD5, SHA1), missing salting124125**Action:** Ensure every endpoint has explicit auth/authz. Check authorization server-side against the session, not126client-provided claims. Use bcrypt/scrypt/argon2 for password hashing.127128### 6. Sensitive Data Exposure129130Personal data, secrets, or internal details leaked through logs, errors, responses, or storage.131132**Signals:**133134- Logging of request bodies, auth headers, or user PII135- Error responses that include stack traces, internal paths, or database details136- API responses that return more fields than the client needs (no field filtering)137- Sensitive data stored in localStorage, sessionStorage, or unencrypted cookies138- PII in URLs (email, SSN, tokens in query parameters — logged by default in most servers)139140**Action:** Strip sensitive fields from logs and error responses. Filter API responses to only required fields. Use141encrypted/httpOnly storage for sensitive client-side data.142143### 7. Dependency and Supply-Chain Risks144145Package dependencies with known vulnerabilities, unvetted postinstall scripts, or lockfile integrity issues.146147**Signals:**148149- `package.json` dependencies pinned to `*` or very wide ranges without a lockfile150- Lockfile (`package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`) not committed to the repo151- Packages with `postinstall`, `preinstall`, or `install` lifecycle scripts that execute arbitrary code152- Dependencies with known CVEs (check via `npm audit` or equivalent)153- Importing from CDNs or unpinned URLs at runtime154155**Action:** Commit the lockfile. Pin dependency ranges. Run `npm audit` and address critical/high findings. Review156lifecycle scripts in new dependencies before adding them.157158### 8. Unsafe Code Patterns159160Language-level patterns that create exploitable conditions.161162**Signals:**163164- `innerHTML`, `dangerouslySetInnerHTML`, `document.write()` with dynamic content (XSS)165- `Object.assign()` or spread from untrusted input without allowlisting properties (prototype pollution)166- `JSON.parse()` of untrusted input without a try/catch or schema validation167- Timing-sensitive comparisons of secrets (use constant-time comparison instead)168- Unrestricted file write paths derived from user input169- Deserialization of untrusted data without schema validation170- `__proto__` or `constructor.prototype` accessible via user-controlled object keys (prototype pollution)171172**Action:** Use `textContent` instead of `innerHTML`, allowlist properties on untrusted objects, validate schemas173after parsing, use `crypto.timingSafeEqual()` for secret comparison. Use `Object.create(null)` for lookup maps174populated from user input to prevent prototype pollution.175176### 9. TypeScript-Specific Security Patterns177178Leveraging (or failing to leverage) TypeScript's type system for security enforcement.179180**Signals:**181182- `(await response.json()) as T` or `JSON.parse(untrusted) as T` — typed appearance with no runtime validation183- `as any` or `as unknown as T` bypassing validation at trust boundaries — input flows from `any` into typed184 operations without runtime validation185- Branded types not used for security-sensitive strings (raw `string` for SQL fragments, HTML content, URLs,186 file paths) — allows accidental mixing of validated and unvalidated values187- Missing type guards at security validation boundaries — `typeof` / `instanceof` checks that should narrow188 to validated types but are plain boolean checks instead189- `strict: false` or `strictNullChecks: false` in `tsconfig.json` — permissive compiler settings that weaken190 security invariants191- Zod/Joi/class-validator schemas that exist but whose inferred types aren't used in downstream function192 signatures — validation runs but the type system doesn't enforce it193- `@ts-ignore` or `@ts-expect-error` suppressing errors in security-sensitive code (auth, validation, crypto)194195**Action:** Replace cast-then-trust with parse-then-narrow: `const user = UserSchema.parse(await response.json())`.196Use branded types for security-sensitive values (`type SafeHtml = string & { __brand: 'SafeHtml' }`). Enable197`strict: true` in `tsconfig.json`. Use type predicates or assertion functions (`asserts input is ValidatedRequest`) at198trust boundaries. Derive all downstream types from validation schemas (`z.infer<typeof schema>`) — one source of truth199for runtime shape and compile-time types.200201### 10. GraphQL and WebSocket Security202203Missing protections specific to GraphQL APIs and WebSocket connections.204205**Signals:**206207- GraphQL: no query depth limiting (allows deeply nested queries that exhaust server resources)208- GraphQL: no query complexity analysis or cost limiting209- GraphQL: introspection enabled in production (exposes full schema to attackers)210- GraphQL: mutations without authentication or authorization checks in resolvers211- GraphQL: no persisted queries or query allowlisting in production212- WebSocket: missing origin validation on connection upgrade213- WebSocket: no authentication on WebSocket handshake (relying only on initial HTTP auth)214- WebSocket: no rate limiting on message frequency215- WebSocket: user input from messages used in operations without validation216- Server-Sent Events (SSE): no authentication or authorization on event streams217218**Action:** Add query depth and complexity limits (e.g., `graphql-depth-limit`, `graphql-query-complexity`).219Disable introspection in production. Validate WebSocket origin headers. Authenticate WebSocket connections on220handshake. Rate-limit WebSocket messages. Validate all user input from WebSocket messages with schema validation.221222### 11. SSRF, Weak Randomness, and JWT Verification Gaps223224These are **candidate signals, not grep-findings** — each nominated site requires validation before it becomes a225finding.226227**Signals:**228229- **SSRF candidates:** `fetch()`, `axios()`, `http.request()` where the URL is a variable rather than a literal.230 A finding requires tracing user-controlled data to the request sink — the grep only nominates sites for231 data-flow validation. Confirmed SSRF needs an allowlist of destinations or URL parsing + host validation.232- **Weak randomness (nearly direct):** `Math.random()` used for tokens, session IDs, password-reset codes, or233 anything security-bearing. Validate only that the value is security-relevant; the fix is234 `crypto.randomBytes()` / `crypto.randomUUID()`.235- **JWT verification gaps (library-aware):** `jwt.decode()` where verification is expected — in `jsonwebtoken`,236 `decode()` does **not** verify the signature (its own docs warn about this); `jwt.verify()` does. The finding237 requires confirming which library and call path is in play — other libraries (`jose`, framework middleware)238 have different APIs. Also check: `algorithms` not pinned on verify, and secrets accepted from user input.239240**Action:** For each candidate, trace the data flow or library call path first. Report only validated findings;241list unvalidated candidates separately as "requires manual verification" when the trace was inconclusive.242243## Audit Workflow244245### Phase 1: Map Trust Boundaries2462471. **Resolve audit surface.** The prompt may specify the scope as:248 - **Diff**: files changed relative to the base branch — committed, staged, unstaged, and untracked249 - **Path**: specific files, folders, or layers250 - **Codebase**: the entire project (the default when unspecified; set `SCOPE=.`)251252 **Party mode:** when the orchestrator supplies a scope snapshot (a resolved file list), use it verbatim and do253 not re-resolve. The resolution below applies to standalone runs only.254255 For diff mode, resolve fail-closed:256 ```bash257 BASE=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/@@')258 if [ -z "$BASE" ]; then259 for b in origin/main origin/master main master; do260 git rev-parse -q --verify "$b" >/dev/null && BASE=$b && break261 done262 fi263 # If BASE is still empty: STOP. Ask for an explicit base. Do not continue.264265 SCOPE=$( { git diff --name-only --diff-filter=d "$BASE"...HEAD;266 git diff --name-only --diff-filter=d HEAD;267 git ls-files --others --exclude-standard; } | sort -u )268 DELETED=$( { git diff --name-only --diff-filter=D "$BASE"...HEAD;269 git diff --name-only --diff-filter=D HEAD; } | sort -u )270 ```271 If `$SCOPE` is empty, run no scans: write the report with "Audit completed: 0 findings — empty diff scope",272 listing `$DELETED` under "Deleted in diff" if non-empty, and stop — deleted files matter here: a deleted273 test or authorization check is security-relevant context. If the resolved surface exceeds what can be read274 within the context budget, report the file count and ask to narrow or chunk.275276 **Two surfaces.** Findings are reported only against the **target scope** (`$SCOPE`) — every finding anchors277 (file:line) there. Related files may still be *read* as **context**: trust-boundary mapping and data-flow278 tracing (Phases 1, 3, 4) read the whole project, because in-scope input can reach sinks outside the scope and279 vice versa — anchor the finding at its in-scope end.2802. Identify trust boundaries: API route handlers, webhooks, file upload endpoints, third-party integration points,281 CLI argument parsers, config loaders.2823. Identify auth/authz middleware and where it's applied.283284### Phase 2: Scan for Security Signals285286Run every scan against the target scope (`SCOPE=.` in codebase mode). Two exclusion profiles apply:287288- **Secrets and dependency scans use the minimal profile** — only `node_modules` is excluded. Tests and generated289 files stay in scope: a live key in a test fixture is exactly as compromised as one in `src/`, and generated290 artifacts can embed committed credentials too.291- All other scans use the production profile.292293```bash294# Minimal profile — secrets and dependency scans only295EXCLUDE_MINIMAL='--glob !**/node_modules/**'296297# Production profile — everything else298EXCLUDE='--glob !**/node_modules/** --glob !**/dist/** --glob !**/*.generated.* --glob !**/__generated__/** --glob !**/*.g.ts --glob !**/generated/** --glob !**/*.test.* --glob !**/*.spec.* --glob !**/*.e2e.* --glob !**/__tests__/**'299300# Hardcoded secrets (common patterns) — minimal profile, all file types301rg --pcre2 '(sk_live_|AKIA|ghp_|password\s*=\s*["\x27]|secret\s*=\s*["\x27])' $EXCLUDE_MINIMAL -- $SCOPE302rg --pcre2 '(postgres|mysql|mongodb)://\w+:\w+@' $EXCLUDE_MINIMAL -- $SCOPE303304# Injection: eval, exec, spawn, Function constructor305rg '\beval\s*\(|\bexec\s*\(|\bspawn\s*\(|\bFunction\s*\(' --type ts $EXCLUDE -- $SCOPE306307# TypeScript-specific security308rg 'as any|as unknown' --type ts $EXCLUDE -- $SCOPE309rg --pcre2 '\.json\(\)\)\s+as\s|\bJSON\.parse\([^)]+\)\s+as\s' --type ts $EXCLUDE -- $SCOPE310rg 'strict.*false|strictNullChecks.*false' --glob 'tsconfig*.json'311rg '@ts-ignore|@ts-expect-error' --type ts $EXCLUDE -- $SCOPE312313# GraphQL security314rg 'introspection|depthLimit|queryComplexity' --type ts $EXCLUDE -- $SCOPE315316# WebSocket security317rg 'WebSocket|socket\.io|ws\.' --type ts $EXCLUDE -- $SCOPE318319# Injection: string concatenation in queries320rg --pcre2 '(query|sql|execute)\s*\(\s*[`"\x27].*\$\{' --type ts $EXCLUDE -- $SCOPE321322# XSS: innerHTML, dangerouslySetInnerHTML, document.write323rg 'innerHTML|dangerouslySetInnerHTML|document\.write' --type ts $EXCLUDE -- $SCOPE324325# Insecure config326rg --pcre2 'Access-Control-Allow-Origin.*\*|rejectUnauthorized.*false|secure.*false' $EXCLUDE -- $SCOPE327328# Missing auth (route definitions without auth middleware — project-specific, adapt pattern)329rg '(app|router)\.(get|post|put|patch|delete)\s*\(' --type ts $EXCLUDE -- $SCOPE330331# Logging of sensitive data332rg --pcre2 'console\.(log|info|debug).*\b(password|token|secret|authorization|cookie)\b' -i $EXCLUDE -- $SCOPE333334# Regex from user input335rg 'new RegExp\(' --type ts $EXCLUDE -- $SCOPE336337# SSRF candidates (variable URLs — nominate for data-flow validation, §11)338rg --pcre2 '(fetch|axios(\.\w+)?|https?\.request)\(\s*[a-zA-Z_$`]' --type ts $EXCLUDE -- $SCOPE339340# Weak randomness for security values (§11)341rg 'Math\.random\(' --type ts $EXCLUDE -- $SCOPE342343# JWT: decode without verify (library-aware validation required, §11)344rg '\bjwt\.decode\(|\.decode\(.*token' --type ts $EXCLUDE -- $SCOPE345346# Dependency risks: lifecycle scripts in package.json — minimal profile347rg '"(pre|post)?install"' --glob 'package.json' $EXCLUDE_MINIMAL348```349350### Phase 3: Trace Data Flows351352For each trust boundary found in Phase 1:3533541. Does untrusted input pass through validation before reaching sensitive operations?3552. Are query parameters, body fields, and headers validated for type, format, and range?3563. Could a malicious input reach `eval`, `exec`, a database query, or DOM manipulation?357358### Phase 4: Evaluate Auth Coverage3593601. List all routes/endpoints. For each: is auth middleware applied?3612. Identify authorization logic. Is it server-side? Does it rely on client claims?3623. Check session management: expiry, rotation, revocation.363364### Phase 5: Produce Report365366## Output Format367368Save as `YYYY-MM-DD-security-hunter-audit-{model-name}.md` — `{model-name}` is the executing model's short name369(e.g. `fable-5`) — in the project's docs folder (or project root if no docs folder exists). If the caller specifies370an output path (e.g. the party-hunter orchestrator), it overrides this default.371372Severity levels, used for per-finding labels and the Recommendations grouping:373374- **Critical** — exploitable now, causes data loss, or breaks behavior on production paths.375- **High** — a defect with likely user-visible, security, or reliability impact if left unaddressed.376- **Medium** — correctness or maintainability risk without imminent impact.377- **Low** — hygiene; no behavioral risk.378379```md380# Security Hunter Audit — {date}381382## Scope383384- Surface: {diff / path / codebase}385- Files: {count or list}386- Exclusions: {list — note the minimal profile used for secrets/dependency scans}387- {Deleted in diff: {list} — only for diff scope with deletions}388- Audit completed: {N} findings389390## Trust Boundary Map391392| # | Boundary | Location | Validation | Auth |393| - | -------- | -------- | ---------- | ---- |394| 1 | POST /api/users | file:line | Zod schema | JWT middleware |395| 2 | Webhook /hooks/stripe | file:line | None | None |396397## Findings398399### Hardcoded Secrets400401| # | Location | Pattern | Severity | Action |402| - | -------- | ------- | -------- | ------ |403| 1 | file:line | `sk_live_...` Stripe key | Critical | Move to env, rotate immediately |404405### Injection Risks406407| # | Location | Type | Untrusted Input | Action |408| - | -------- | ---- | --------------- | ------ |409| 1 | file:line | SQL injection | `req.query.id` concatenated into query | Use parameterized query |410411### Missing Input Validation412413| # | Location | Boundary | Input | Action |414| - | -------- | -------- | ----- | ------ |415| 1 | file:line | POST /api/orders | `req.body` used without validation | Add Zod/Joi schema |416417### Insecure Defaults418419| # | Location | Setting | Current | Action |420| - | -------- | ------- | ------- | ------ |421| 1 | file:line | CORS origin | `*` | Restrict to allowed origins |422423### Auth/Authz Gaps424425| # | Location | Endpoint | Issue | Action |426| - | -------- | -------- | ----- | ------ |427| 1 | file:line | GET /admin/users | No auth middleware | Add admin auth |428429### Sensitive Data Exposure430431| # | Location | Data | Channel | Action |432| - | -------- | ---- | ------- | ------ |433| 1 | file:line | Auth token | console.log | Remove from logs |434435### Unsafe Patterns436437| # | Location | Pattern | Risk | Action |438| - | -------- | ------- | ---- | ------ |439| 1 | file:line | `innerHTML = userInput` | XSS | Use `textContent` |440441### Dependency / Supply-Chain Risks442443| # | Location | Issue | Severity | Action |444| - | -------- | ----- | -------- | ------ |445| 1 | package.json | No lockfile committed | High | Commit lockfile |446447### SSRF / Randomness / JWT Candidates (validated)448449| # | Location | Candidate | Validation Result | Action |450| - | -------- | --------- | ----------------- | ------ |451| 1 | file:line | `fetch(userUrl)` | User input reaches sink unfiltered | Allowlist destinations |452453## Recommendations (Priority Order)4544551. **Critical**: {hardcoded secrets to rotate, injection vulnerabilities, auth bypasses, confirmed SSRF}4562. **High**: {missing input validation, insecure defaults, auth gaps, dependency CVEs, weak randomness for tokens}4573. **Medium**: {sensitive data exposure, unsafe patterns, supply-chain hygiene}4584. **Low**: {hardening opportunities without a concrete exploit path}459```460461## Operating Constraints462463- **No code edits.** This skill produces an audit report only. Implementation is a separate step.464- **No empty finding sections.** Include only categories with findings. Omit a heading, table, or list entirely when it would contain zero items — do not include empty tables, placeholder subsections, or negative statements like "no dead exports", "none found", or "no issues". Execution status is exempt: the "Audit completed: N findings" line in the Scope section is always present, even at zero findings.465- **Scope: security vulnerabilities only.** If a finding doesn't answer "could this be exploited?", it belongs to466 another hunter — do not flag it here. Boundary with invariant-hunter: it flags the unsafe cast and loose typing467 at trust boundaries; this hunter flags the exploitable boundary and recommends schema + `z.infer<>`.468- **Evidence required.** Every finding must cite `file/path.ext:line` with the exact code.469- **Severity matters.** Use the Critical / High / Medium / Low definitions above. A hardcoded production secret is470 not the same severity as a missing `SameSite` cookie attribute. Prioritize accordingly.471- **Context over pattern-matching.** A `new RegExp()` with a hardcoded pattern is fine. A `new RegExp(userInput)` is472 a ReDoS risk. Grep finds both — judgment separates them.473- **No false confidence.** This audit catches common patterns, not all vulnerabilities. It is not a substitute for474 professional penetration testing or automated security scanning.