Detect prompt injection risks in AI-integrated GitHub Actions
Project description
gh-prompt-scan
A static analyzer that finds prompt-injection and related vulnerabilities in AI-integrated GitHub Actions workflows.
gh-prompt-scan scans the workflow YAML in a repository's .github/workflows/ directory and flags patterns where untrusted, attacker-controlled input can reach a shell command, an AI agent, or the runner — the class of bugs behind "pwn request" and prompt-injection attacks in CI pipelines.
It is designed to run as a CI gate: it exits non-zero when it finds issues, so a build can fail on a vulnerable workflow.
gh-prompt-scan is a linter for common dangerous patterns, not a proof of safety. A clean report does not guarantee a workflow is secure. See Limitations.
Why this exists
Workflows triggered by events like pull_request_target, issue_comment, or issues run with the base repository's permissions and secrets, but their input (issue titles, PR bodies, comments, branch names) is controlled by anyone on the internet. When that untrusted input is interpolated into a shell command or fed to an AI agent that can run tools, an attacker can inject commands or instructions. gh-prompt-scan looks for these data-flow patterns automatically.
The threat-vector taxonomy (TV4–TV7) is based on the Heimdallr research paper (arXiv:2605.05969).
What it detects
| Vector | Severity | What it flags |
|---|---|---|
| TV4 | CRITICAL | An attacker-controlled context expression (e.g. github.event.issue.title) used directly inside a run: block, where GitHub interpolates it before the shell runs — classic command injection. |
| TV5 | CRITICAL | An AI step receives attacker-controlled input and a step output later flows into a shell command, so model output can drive shell execution. |
| TV6 | CRITICAL | A pull_request_target workflow that checks out the PR head (github.head_ref / pull_request.head.*), giving attacker-controlled code access to the privileged runner context. |
| TV7 | MEDIUM | An AI step reachable by any external user with no actor guard, allowing repeated triggering to exhaust your AI API quota. |
Severity reflects impact: CRITICAL findings can lead to code execution or secret exposure; MEDIUM findings are abuse/cost risks.
Installation
Requires Python 3.10+.
git clone https://github.com/panther-0707/gh-prompt-scan.git
cd gh-prompt-scan
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -e .
This installs the gh-prompt-scan command (via [project.scripts] in pyproject.toml). Dependencies: pyyaml, click.
Usage
Scan the current repository:
gh-prompt-scan scan --path .
Scan a different repository:
gh-prompt-scan scan --path /path/to/repo
Show the version:
gh-prompt-scan version
Exit codes
| Code | Meaning |
|---|---|
0 |
No findings (scan clean, or no workflow files found) |
1 |
One or more findings reported |
The non-zero exit on findings is what lets gh-prompt-scan fail a CI build.
Example output
Scanning 2 workflow file(s) ...
[CRITICAL] TV4 - .github/workflows/triage.yml step 1
Message: Attacker controlled 'github.event.issue.title' which was found directly in run: command
Fix: Pass untrusted values via env: variables instead of ${{ }} in run: blocks
Running gh-prompt-scan in CI
Add a workflow that runs gh-prompt-scan on every push and pull request. Because the scan only reads your workflow files, run it on the safe pull_request trigger:
name: gh-prompt-scan
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -e .
- run: gh-prompt-scan scan --path .
The job fails automatically if gh-prompt-scan reports any findings.
How it works
gh-prompt-scan runs a small pipeline over each workflow file:
- Loader finds
*.yml/*.yamlfiles under.github/workflows/. - Parser loads the YAML into typed objects, normalizing the
on:field (which YAML parses as the booleanTrue, and which may be a string, list, or mapping). - Trigger classifier decides whether the workflow is reachable by untrusted users. Workflows with only "safe" triggers (
push,schedule,workflow_dispatch) are skipped. - AI detector identifies steps that invoke a known AI action or set an AI provider API key.
- Taint analysis walks the steps of each externally-triggerable job and emits findings for TV4–TV7.
Limitations
gh-prompt-scan catches common patterns; it is not exhaustive and can produce both false positives and false negatives. Known gaps in the current version:
- AI-step identification has a narrow gap. A step is recognized as an AI step if it uses a known AI action or sets an AI API key directly on the step. A generic step (a plain
run:or an unknown action) that qualifies as an AI call only because it inherits a key from job- or workflow-levelenvmay not be recognized as one. Attacker input flowing through inherited job/workflowenvinto a known AI step is tracked. - TV5 is a heuristic. It flags when an AI step received tainted input and any later step references a
steps.*.outputs.*value in a shell command, without confirming the referenced output belongs to the AI step. It may over- or under-report. - Renamed AI-key env vars are matched by name only.
is_ai_stepcompares env key names against a known list; a custom-named key whose value references an AI secret (e.g.MY_KEY: ${{ secrets.OPENAI_API_KEY }}) won't be detected. stepnumbers are positions, not file line numbers. A finding'sstepindex is the step's position within its job.- Detection uses substring matching on expression strings, so unusual syntax (such as index form
['title']) may not match. ATTACKER_SOURCESis a curated list, not a complete enumeration of every untrusted GitHub context.
Treat gh-prompt-scan's output as a prompt to review a workflow by hand, not as a security guarantee.
Development
Run the test suite:
pytest -v
Contributions that add test cases for new threat vectors or edge cases are especially welcome.
License
Released under the MIT License. See LICENSE for details.
Acknowledgements
The threat-vector taxonomy is based on the Heimdallr research paper (arXiv:2605.05969).
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file gh_prompt_scan-0.1.5.tar.gz.
File metadata
- Download URL: gh_prompt_scan-0.1.5.tar.gz
- Upload date:
- Size: 7.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
171aeb3bc90f0c40c86b95642c063a38a64ae3853bbb15c4005d9cfd211f3678
|
|
| MD5 |
eacbe2150f30f152223349b16c4b5e80
|
|
| BLAKE2b-256 |
accf0808f5a235e5ca5cca746d1184da4476d4f33c8a1bc0c80379ed904a249b
|
File details
Details for the file gh_prompt_scan-0.1.5-py3-none-any.whl.
File metadata
- Download URL: gh_prompt_scan-0.1.5-py3-none-any.whl
- Upload date:
- Size: 9.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
71d718da0f93fd78c51394e015338847fc2df11dd032c351feddfee01346ca67
|
|
| MD5 |
493ed1928998190f0840e10f8f7b0139
|
|
| BLAKE2b-256 |
68564248636c5ca6d0a2bfa98fdc49fce7ccf329c4e726b47e858c39faf16995
|