pull_request_target workflow that checks out untrusted code is risky on its
own; combine it with a writable GITHUB_TOKEN and it becomes a full repository
takeover.
Attacker Mind is Pipefort’s correlation layer. It looks across the findings
from a scan and reports toxic combinations: named attack scenarios that
exist only when a set of findings co-occur. Each combination carries:
- a severity —
CRITICALorHIGH(toxic combinations sit above the per-finding severity scale, because the chained impact exceeds any single ingredient); - an attack chain — the ordered stages of the compromise, so you can see what the attack actually looks like;
- the contributing findings with their
file:linelocations; and - a break-the-chain recommendation — the single highest-leverage fix that collapses the whole scenario.
How detection works
Combinations are pure correlation over findings that the scanner already produced, so the same engine powers both the CLI and the web dashboard. Combinations are matched on rule IDs (not OWASP categories), because a category likeCICD-SEC-4 is emitted by more
than one rule (workflow shell-injection and the read-write GITHUB_TOKEN
repository setting) — matching on rule ID keeps each ingredient precise.
A combination only forms from findings that survived your
rule settings: if you disable a rule, it can never be
an ingredient.
Combinations are scoped:
- file-scoped combinations require their anchor ingredient inside one workflow file and are reported once per such file;
- repo-scoped combinations correlate across the whole repository and are reported once.
The shipped combinations
| Combination | Severity | Ingredients (rule IDs) | Breaks when you… |
|---|---|---|---|
| Pwn Request | CRITICAL | cicd-sec-1-ppe-checkout + a writable token (cicd-sec-5-missing-permissions or cicd-sec-4-wperm-write); amplified by cicd-sec-4-ppe-shell-injection, cicd-sec-4-secrets-inherit-pr-target | stop checking out the PR head ref in pull_request_target |
| Poisoned Exfiltration | CRITICAL | cicd-sec-4-ppe-shell-injection + a reachable secret (cicd-sec-6-hardcoded-secrets, cicd-sec-2-long-lived-pat, cicd-sec-7-debug-logging-enabled, or cicd-sec-6-secret-in-run-output) | route untrusted input through an env var instead of the shell |
| Injected Runner Takeover | CRITICAL | cicd-sec-4-ppe-shell-injection + best-prac-3-self-hosted-runners (same workflow); amplified by best-prac-2-missing-timeout | route untrusted input through an env var instead of the shell |
| Untrusted Code on Self-Hosted Runner | HIGH | untrusted fork content (cicd-sec-1-ppe-checkout or cicd-sec-1-workflow-run-artifact-poisoning) + best-prac-3-self-hosted-runners; amplified by cicd-sec-1-checkout-persist-credentials, cicd-sec-5-missing-permissions, cicd-sec-4-wperm-write | stop running untrusted fork content, or use ephemeral GitHub-hosted runners |
| Secret Exposure in Logs | HIGH | cicd-sec-7-debug-logging-enabled + an exposed credential (cicd-sec-6-hardcoded-secrets or cicd-sec-6-secret-in-run-output) | remove the debug-logging env entry |
| Unverifiable Release | HIGH | slsa-build-l2-provenance + cicd-sec-3-unpinned-action; amplified by slsa-build-l2-verify-step | generate a build-provenance attestation for published artifacts |
| Persistent Supply-Chain Foothold | HIGH | cicd-sec-3-unpinned-action + best-prac-3-self-hosted-runners; amplified by best-prac-2-missing-timeout | pin the action to a full commit SHA |
| Untrusted RCE on Infra | HIGH | best-prac-1-pipe-to-shell + best-prac-3-self-hosted-runners; amplified by cicd-sec-4-wperm-write | download, verify, then execute — never pipe to a shell |
| Silent Supply-Chain Tampering | HIGH | cicd-sec-9-download-without-checksum + (cicd-sec-3-unpinned-action or cicd-sec-10-continue-on-error-job) | verify a checksum/signature for every download |
| Open Trigger Secret Leak | HIGH | cicd-sec-8-repository-dispatch-unfiltered + (cicd-sec-7-debug-logging-enabled, cicd-sec-6-hardcoded-secrets, or cicd-sec-6-secret-in-run-output) | add a types: allowlist to the trigger |
GitLab combinations
The same engine correlates GitLab CI findings. These mirror the GitHub scenarios for.gitlab-ci.yml:
| Combination | Severity | Ingredients (rule IDs) | Breaks when you… |
|---|---|---|---|
| GitLab Pwn Request | CRITICAL | cicd-sec-1-gl-mr-target + a reachable secret/injection (cicd-sec-4-gl-shell-injection, cicd-sec-6-gl-hardcoded-secrets, or cicd-sec-7-gl-debug-trace) | don’t run jobs that check out MR source code on merge_request_event |
| GitLab Poisoned Exfiltration | CRITICAL | cicd-sec-4-gl-shell-injection + a reachable secret (cicd-sec-6-gl-hardcoded-secrets, cicd-sec-2-gl-pat-secret, or cicd-sec-7-gl-debug-trace) | route untrusted $CI_* input through an intermediate variable |
| GitLab Persistent Foothold | HIGH | cicd-sec-3-gl-unpinned-include + best-prac-3-gl-self-hosted-tags; amplified by best-prac-2-gl-missing-timeout | pin every include: to an immutable ref |
Where to see them
- CLI — every scan prints an Attacker Mind — Toxic Combinations section
after the findings (and includes them under the
toxic_combinationskey in JSON output). - Web app — the Attacker Mind dashboard aggregates combinations across all your repositories and draws each attack chain.