Security Audit Review
Most security bugs come from the same half-dozen patterns. This skill is the checklist for catching them during review.
When to invoke
- Post-implementation (adversarial review): any PR that touches auth, tokens, secrets, URL construction, subprocess execution, file paths, or external-service calls
- Audit-loop reviewer node: for any pipeline producing an artifact that handles secrets or runs in a privileged context
- Pre-merge on infra / deploy / IAM changes: ruleset modifications, App permissions, Kubernetes RBAC, Cloudsmith / secret-manager access
Audit-loop reviewers are read-only. When invoked as an independent audit-loop reviewer node (parallel review fan-out, typically its own sandbox), audit by reading source, not by building, running, or executing anything. Your sandbox may have different capabilities (network, filesystem, socket permissions) than the environment that produced any evidence pack you're given — an execution failure in your own sandbox is not a security finding about the artifact.
Skip when: the change is purely docs or tests with no secret/auth surface. A README edit doesn't need a security review.
Red flags (cheap checks — stop if any are true)
Checklist (deeper)
Token handling
- Are tokens read from config / env / secrets manager, never inlined as string literals?
- Are they passed to subprocesses via env vars rather than argv? (argv shows up in
ps; env is per-process)
- Are they scrubbed from
stderr_tail, error messages, panic traces, and summary events via an explicit redaction function?
- Does the redaction regex cover all the patterns in use? Canonical patterns to redact:
^[A-Z_]+_TOKEN= (GH_TOKEN, CLOUDSMITH_TOKEN, etc.)
Authorization:\s*(Bearer|token)\s+\S+
password[=:]\s*\S+
- API key formats the service you're talking to uses (Stripe keys start with
sk_, etc.)
Injection surfaces
- Every
exec.Command or /bin/sh -c construction: is every argument user-controlled or literal? If user-controlled, is it passed as a separate argv element (safe) or concatenated into a shell string (unsafe)?
- Every URL constructed with
fmt.Sprintf("%s/%s", base, path) or string concat: is the path component URL-escaped?
- Every SQL query: prepared statements, not format strings
- Every HTML-rendered user content: escaped at output time, not at input time
- Every file-path join with user-controlled segments:
filepath.Clean + directory-traversal check (no .. after cleaning)
Auth path scope
- For GitHub Apps / OAuth: does the App installation have only the permissions needed by the workflow? Over-scoped Apps widen blast radius on compromise.
- For IAM roles / service accounts: principle-of-least-privilege review. List the actions the role can take; match against the actions the workflow needs. Prune the delta.
- For secrets: does each secret flow to only the jobs that need it? A shared secret visible to every job is a wider attack surface than per-job secrets.
- For user-minted tokens: is the token's scope narrower than the user's personal token? (GitHub fine-grained PATs, for example)
Secret exposure in error handling
- On a non-2xx HTTP response: does the code log the whole response body? Response bodies sometimes echo the Authorization header back on GraphQL errors. Redact.
- On a subprocess error: does the code log the whole stderr? If the subprocess was invoked with GH_TOKEN=... in env, some tools echo it back. Redact.
- On a panic / stack trace: do local variables with secret values get surfaced? Go's
%+v format can dump struct fields including tokens. Use %v plus explicit redaction.
- On debug mode: is there a
DEBUG=1 that prints more than production does? Verify its output path doesn't include secrets.
Permissions + ruleset configuration
- Branch protection / merge-queue rulesets: does the rule match the intent? (e.g., enforcing merge-queue ≠ having a merge queue active — two separate knobs, both needed)
- Required status checks: is the list complete? A required check that is never posted never fires, effectively no-op
- Fork-PR safety: is it
pull_request (plain, no secrets on forks) or pull_request_target (runs with repo secrets — dangerous surface)? If _target, is the code running from the base branch, not the PR head?
- Artifact signing: are published artifacts signed? Are checksums verified on consume?
TLS and trust store
- Any
InsecureSkipVerify, -k, rejectUnauthorized: false → justify why, or remove
- Any hardcoded CA fingerprint → verify it's the correct cert and has a rotation plan
- Any
http:// (not https) URL to an external service → verify it's not carrying anything sensitive
Prompt template for adversarial reviewer
Drop this block into any reviewer node prompt for security-sensitive changes:
Security audit review — REQUIRED for any change touching auth,
tokens, secrets, URL/command construction, file paths, external
service calls, or permissions config:
* Read-only as an audit-loop node: verify from source and already-captured
evidence/results; do not build, run, or execute anything to reproduce
claims yourself (your sandbox may lack capabilities — network, sockets —
the evidence-gathering environment had)
* Tokens: read from config/env, passed via env (not argv), scrubbed
from all logs / errors / event fields via an explicit redaction
regex
* Injection: every user-controlled value in exec/URL/SQL/HTML/path
context is escaped or passed as a separate argv element
* Auth scope: every App / IAM role / service account has only the
permissions required for the task (principle of least privilege)
* Error handling: response bodies, subprocess stderr, and panic
traces cannot leak secrets — verify redaction on every error path
* Permissions config: rulesets match intent; pull_request vs.
pull_request_target distinction is correct for the fork-PR use case
* TLS: no InsecureSkipVerify / -k / rejectUnauthorized=false in
production paths
Missing redaction on an error path is an `intent_gap` finding.
Over-scoped permissions are an `intent_gap` finding (tighten scope).
InsecureSkipVerify in production is a `patch` finding (fix now).
Finding classifications
| Finding |
Classification |
| Token leaked into log / error / event field without redaction |
intent_gap |
| Shell injection surface (user input concatenated into command) |
intent_gap |
| Over-scoped App / IAM permissions |
intent_gap |
InsecureSkipVerify / TLS bypass in production |
patch |
| Missing redaction regex pattern (known secret format not covered) |
patch |
| Ruleset enforces X but X isn't actually materialized (e.g., merge_queue rule without merge queue) |
intent_gap |
| Debug/verbose mode leaks secrets |
patch |
Canonical worked examples
PR #22 — release-pr charity-pass (release-workspace, 2026-04-21)
Charity-pass observability introduced a stderr_tail field in summary events that captured the last 500 chars of subprocess stderr on error. Round-2 review caught that the redaction regex was necessary: gh subprocess errors occasionally echoed back GH_TOKEN=... values on GraphQL failures. The fix: a tokenRe = regexp.MustCompile([A-Z_]+_TOKEN)=\S+ applied before stderr_tail was stored.
Lesson: the moment you capture subprocess output into an event field, the redaction pipeline is part of the contract. Review that it covers every token format the subprocess might emit.
PR #709 — Release PR auto-merge permission (release, 2026-04-21)
The release-pr resource called enablePullRequestAutoMerge via gh CLI and received GraphQL: Resource not accessible by integration. The GitHub App backing the resource lacks the enablePullRequestAutoMerge scope on the release repo — a deliberate restriction to prevent the App from bypassing the merge queue.
Lesson: Resource not accessible by integration errors often mean the App is correctly scoped to prevent exactly this action. Before asking for a scope addition, verify the constraint is a bug and not a safety property.
Anti-patterns
- "It's just a log line, who would see the token?" — logs end up in CI UIs, forwarded to log aggregators, emailed in failure notifications. Treat log as public-adjacent.
- Redacting in one place but not another — once you have a redaction function, use it on every path. A single un-redacted path defeats the redaction.
- Asking for broader App scope because today's workflow needs it — first check if the scope restriction is enforcing a safety property (it often is). Solutions that widen scope permanently to solve a one-time problem accumulate.
InsecureSkipVerify with a comment "TODO fix" that's older than the repo — it's never going to be fixed. Either fix it now or budget it as a known risk with a date.
- "We control both ends" arguments against escaping — code changes, ownership changes. The escape is documentation of intent as much as defense.
- Using
pull_request_target without reading its docs first — it's a known footgun. The base-branch-code-runs-with-secrets model is counterintuitive.
Quick reference
| Aspect |
What to check |
| Token flow |
Config → env → subprocess env; scrubbed from all error paths |
| Injection surfaces |
Every user-controlled value escaped at construction time |
| Auth scope |
App/IAM/service-account has minimum required permissions |
| Error leakage |
Response bodies, stderr, panic traces all pass through redaction |
| Permissions config |
Rulesets / branch protection / merge-queue actually materialized |
| TLS / trust |
No InsecureSkipVerify in prod; https everywhere sensitive |
Related skills
observability-review — where stderr_tail and log-field redaction get introduced; this skill audits that they're correct
spec-compliance-review — when the spec describes auth paths or secret handling, this skill verifies the implementation matches
security:triage-cve — for CVE-specific triage (upstream fix availability, VEX authoring); this skill is more general-purpose
- Built-in
/security-review — automated scan of the pending changes; use as a first pass, follow with this skill for deeper manual review
1---2name: security-audit-review3description: Use when reviewing code for security concerns — token handling, injection surfaces, auth paths, secret exposure, and permissions scope. Invoked during adversarial review post-implementation, as an audit-loop reviewer node, or on PRs that touch auth/secrets/deploy surfaces. Distinct from the built-in `/security-review` slash command (which runs an automated scan); this skill guides a human or reviewer-agent through the audit.4---56# Security Audit Review78Most security bugs come from the same half-dozen patterns. This skill is the checklist for catching them during review.910## When to invoke1112- **Post-implementation (adversarial review):** any PR that touches auth, tokens, secrets, URL construction, subprocess execution, file paths, or external-service calls13- **Audit-loop reviewer node:** for any pipeline producing an artifact that handles secrets or runs in a privileged context14- **Pre-merge on infra / deploy / IAM changes:** ruleset modifications, App permissions, Kubernetes RBAC, Cloudsmith / secret-manager access1516**Audit-loop reviewers are read-only.** When invoked as an independent audit-loop reviewer node (parallel review fan-out, typically its own sandbox), audit by reading source, not by building, running, or executing anything. Your sandbox may have different capabilities (network, filesystem, socket permissions) than the environment that produced any evidence pack you're given — an execution failure in your own sandbox is not a security finding about the artifact.1718**Skip when:** the change is purely docs or tests with no secret/auth surface. A README edit doesn't need a security review.1920## Red flags (cheap checks — stop if any are true)2122- [ ] A token or secret variable appears in a log line, error message, or event field without an explicit redaction step23- [ ] An HTTP request or subprocess call builds a URL or command from user input without escaping24- [ ] An `if err != nil { log.Fatal(err) }` where `err` could contain token values (generic "response body" errors leak payloads)25- [ ] A GitHub App / IAM role / service account has broader scope than the task requires26- [ ] `fetch`, `exec.Command`, `os.OpenFile`, or similar takes a string from stdin/params with no validation27- [ ] A config file or environment variable with a secret is read into a map and the whole map gets logged on error28- [ ] `--allow-insecure`, `-k`, `InsecureSkipVerify`, or `NODE_TLS_REJECT_UNAUTHORIZED=0` appears in production code2930## Checklist (deeper)3132### Token handling3334- Are tokens read from config / env / secrets manager, never inlined as string literals?35- Are they passed to subprocesses via env vars rather than argv? (argv shows up in `ps`; env is per-process)36- Are they scrubbed from `stderr_tail`, error messages, panic traces, and summary events via an explicit redaction function?37- Does the redaction regex cover all the patterns in use? Canonical patterns to redact:38 - `^[A-Z_]+_TOKEN=` (GH_TOKEN, CLOUDSMITH_TOKEN, etc.)39 - `Authorization:\s*(Bearer|token)\s+\S+`40 - `password[=:]\s*\S+`41 - API key formats the service you're talking to uses (Stripe keys start with `sk_`, etc.)4243### Injection surfaces4445- Every `exec.Command` or `/bin/sh -c` construction: is every argument user-controlled or literal? If user-controlled, is it passed as a separate argv element (safe) or concatenated into a shell string (unsafe)?46- Every URL constructed with `fmt.Sprintf("%s/%s", base, path)` or string concat: is the path component URL-escaped?47- Every SQL query: prepared statements, not format strings48- Every HTML-rendered user content: escaped at output time, not at input time49- Every file-path join with user-controlled segments: `filepath.Clean` + directory-traversal check (no `..` after cleaning)5051### Auth path scope5253- For GitHub Apps / OAuth: does the App installation have only the permissions needed by the workflow? Over-scoped Apps widen blast radius on compromise.54- For IAM roles / service accounts: principle-of-least-privilege review. List the actions the role can take; match against the actions the workflow needs. Prune the delta.55- For secrets: does each secret flow to only the jobs that need it? A shared secret visible to every job is a wider attack surface than per-job secrets.56- For user-minted tokens: is the token's scope narrower than the user's personal token? (GitHub fine-grained PATs, for example)5758### Secret exposure in error handling5960- On a non-2xx HTTP response: does the code log the whole response body? Response bodies sometimes echo the Authorization header back on GraphQL errors. Redact.61- On a subprocess error: does the code log the whole stderr? If the subprocess was invoked with GH_TOKEN=... in env, some tools echo it back. Redact.62- On a panic / stack trace: do local variables with secret values get surfaced? Go's `%+v` format can dump struct fields including tokens. Use `%v` plus explicit redaction.63- On debug mode: is there a `DEBUG=1` that prints more than production does? Verify its output path doesn't include secrets.6465### Permissions + ruleset configuration6667- Branch protection / merge-queue rulesets: does the rule match the intent? (e.g., enforcing merge-queue ≠ having a merge queue active — two separate knobs, both needed)68- Required status checks: is the list complete? A required check that is never posted never fires, effectively no-op69- Fork-PR safety: is it `pull_request` (plain, no secrets on forks) or `pull_request_target` (runs with repo secrets — dangerous surface)? If `_target`, is the code running from the base branch, not the PR head?70- Artifact signing: are published artifacts signed? Are checksums verified on consume?7172### TLS and trust store7374- Any `InsecureSkipVerify`, `-k`, `rejectUnauthorized: false` → justify why, or remove75- Any hardcoded CA fingerprint → verify it's the correct cert and has a rotation plan76- Any `http://` (not https) URL to an external service → verify it's not carrying anything sensitive7778## Prompt template for adversarial reviewer7980Drop this block into any reviewer node prompt for security-sensitive changes:8182```83Security audit review — REQUIRED for any change touching auth,84tokens, secrets, URL/command construction, file paths, external85service calls, or permissions config:86 * Read-only as an audit-loop node: verify from source and already-captured87 evidence/results; do not build, run, or execute anything to reproduce88 claims yourself (your sandbox may lack capabilities — network, sockets —89 the evidence-gathering environment had)90 * Tokens: read from config/env, passed via env (not argv), scrubbed91 from all logs / errors / event fields via an explicit redaction92 regex93 * Injection: every user-controlled value in exec/URL/SQL/HTML/path94 context is escaped or passed as a separate argv element95 * Auth scope: every App / IAM role / service account has only the96 permissions required for the task (principle of least privilege)97 * Error handling: response bodies, subprocess stderr, and panic98 traces cannot leak secrets — verify redaction on every error path99 * Permissions config: rulesets match intent; pull_request vs.100 pull_request_target distinction is correct for the fork-PR use case101 * TLS: no InsecureSkipVerify / -k / rejectUnauthorized=false in102 production paths103104Missing redaction on an error path is an `intent_gap` finding.105Over-scoped permissions are an `intent_gap` finding (tighten scope).106InsecureSkipVerify in production is a `patch` finding (fix now).107```108109## Finding classifications110111| Finding | Classification |112|---------|---------------|113| Token leaked into log / error / event field without redaction | `intent_gap` |114| Shell injection surface (user input concatenated into command) | `intent_gap` |115| Over-scoped App / IAM permissions | `intent_gap` |116| `InsecureSkipVerify` / TLS bypass in production | `patch` |117| Missing redaction regex pattern (known secret format not covered) | `patch` |118| Ruleset enforces X but X isn't actually materialized (e.g., merge_queue rule without merge queue) | `intent_gap` |119| Debug/verbose mode leaks secrets | `patch` |120121## Canonical worked examples122123**PR #22 — `release-pr` charity-pass (release-workspace, 2026-04-21)**124125Charity-pass observability introduced a `stderr_tail` field in summary events that captured the last 500 chars of subprocess stderr on error. Round-2 review caught that the redaction regex was necessary: `gh` subprocess errors occasionally echoed back `GH_TOKEN=...` values on GraphQL failures. The fix: a `tokenRe = regexp.MustCompile([A-Z_]+_TOKEN)=\S+` applied before `stderr_tail` was stored.126127Lesson: the moment you capture subprocess output into an event field, the redaction pipeline is part of the contract. Review that it covers every token format the subprocess might emit.128129**PR #709 — Release PR auto-merge permission (release, 2026-04-21)**130131The release-pr resource called `enablePullRequestAutoMerge` via gh CLI and received `GraphQL: Resource not accessible by integration`. The GitHub App backing the resource lacks the `enablePullRequestAutoMerge` scope on the release repo — a **deliberate restriction** to prevent the App from bypassing the merge queue.132133Lesson: `Resource not accessible by integration` errors often mean the App is correctly scoped to prevent exactly this action. Before asking for a scope addition, verify the constraint is a bug and not a safety property.134135## Anti-patterns136137- **"It's just a log line, who would see the token?"** — logs end up in CI UIs, forwarded to log aggregators, emailed in failure notifications. Treat log as public-adjacent.138- **Redacting in one place but not another** — once you have a redaction function, use it on every path. A single un-redacted path defeats the redaction.139- **Asking for broader App scope because today's workflow needs it** — first check if the scope restriction is enforcing a safety property (it often is). Solutions that widen scope permanently to solve a one-time problem accumulate.140- **`InsecureSkipVerify` with a comment "TODO fix" that's older than the repo** — it's never going to be fixed. Either fix it now or budget it as a known risk with a date.141- **"We control both ends"** arguments against escaping — code changes, ownership changes. The escape is documentation of intent as much as defense.142- **Using `pull_request_target` without reading its docs first** — it's a known footgun. The base-branch-code-runs-with-secrets model is counterintuitive.143144## Quick reference145146| Aspect | What to check |147|--------|--------------|148| Token flow | Config → env → subprocess env; scrubbed from all error paths |149| Injection surfaces | Every user-controlled value escaped at construction time |150| Auth scope | App/IAM/service-account has minimum required permissions |151| Error leakage | Response bodies, stderr, panic traces all pass through redaction |152| Permissions config | Rulesets / branch protection / merge-queue actually materialized |153| TLS / trust | No `InsecureSkipVerify` in prod; https everywhere sensitive |154155## Related skills156157- `observability-review` — where `stderr_tail` and log-field redaction get introduced; this skill audits that they're correct158- `spec-compliance-review` — when the spec describes auth paths or secret handling, this skill verifies the implementation matches159- `security:triage-cve` — for CVE-specific triage (upstream fix availability, VEX authoring); this skill is more general-purpose160- Built-in `/security-review` — automated scan of the pending changes; use as a first pass, follow with this skill for deeper manual review