A GitHub Action that scans the full event payload for common attack vectors — hidden Unicode characters, bidirectional text (Trojan Source), shell injection, path traversal, script injection, template expression injection, and AI prompt injection. Use it in any workflow to detect whether an actor (human or bot) is attempting to inject malicious content through PR titles, branch names, issue bodies, comments, or any other event field.
When a threat is found the step fails with a non-zero exit code and writes a detailed report to the Job Summary.
| Category | What is flagged |
|---|---|
| Homoglyphs | Cyrillic, Greek, and fullwidth Latin letters that look identical to ASCII (e.g. Cyrillic а→a, о→o, р→p; Greek Ο→O; fullwidth A→A). Used in IDN homograph and supply-chain spoofing attacks |
| Hidden Unicode | Zero-width spaces (U+200B/C/D), BOM (U+FEFF), soft hyphen, null byte, LTR/RTL marks, word joiner, line/paragraph separators |
| Bidirectional / Trojan Source | All BIDI control characters (U+202A–202E, U+2066–2069, U+061C) that make malicious content appear benign to reviewers |
| Shell injection | Backtick substitution `cmd`, dollar-paren $(cmd), eval()/exec() code execution, semicolon/pipe chaining to bash, curl, python, etc. |
| Path traversal | ../ and ..\ sequences that can escape intended directories, incl. URL-encoded (%2e%2e%2f), chained (../../..), and traversal reaching sensitive targets (.env, .ssh, /etc/passwd) |
| Script injection | <script>, javascript:, <iframe>, onerror= and other HTML event handler attributes |
| Template/expression injection | ${{ (GitHub Actions context leakage) and {{...}} template expressions |
| Prompt injection | AI/LLM override phrases such as ignore previous instructions, pretend you are, jailbreak, and other directives designed to manipulate AI assistants that process event data |
Findings are classified into two severity tiers:
- Blocking — the step fails (
::error::annotations). This covers all high-signal patterns on every field: hidden Unicode, bidi/Trojan Source, homoglyphs, prompt injection,${{/$(...)/backticks, command chaining,<script>, URL-encoded or chained path traversal, traversal reaching sensitive targets, andexec()/eval()with a command-like payload (e.g.exec("rm -rf …")). - Warning — reported in the step summary and as
::warning::annotations, but the step succeeds. This covers context-free matches in Markdown body fields (pull_request.body, issue bodies, comment bodies,changes.body.from) that are almost always benign prose: a bare..//..\(e.g. a quoted"path": "../pkg"in a changelog) and bareexec()/eval()mentions (e.g. "fix a bug in exec()").
Rationale: Dependabot and Renovate PR bodies embed arbitrary upstream release notes and changelogs, which routinely mention relative paths and functions like exec(). Those mentions never flow into a shell or filesystem context, so failing the job on them produces false positives that block dependency auto-merge — while the blocking tier still catches real attack vectors in the same bodies.
Add the action as an early step in any workflow that handles untrusted input. No required inputs — the action reads the event payload automatically from GITHUB_EVENT_PATH.
name: Security scanon:
pull_request:
issues:
issue_comment:
pull_request_review_comment:
jobs:
scan:
runs-on: ubuntu-latestpermissions:
issues: write # required for post-commentpull-requests: write # required for post-commentsteps:
- name: Scan event payload for attack vectorsuses: devops-actions/secure-action-inputs@v1| Input | Default | Description |
|---|---|---|
show-context | true | Show a collapsible context snippet for each finding — the surrounding lines where the issue was detected, with the matched line highlighted. Set to false to show only the finding summary table. |
post-comment | true | Post (or update) a scan-results comment on the pull request or issue that triggered the workflow. The action checks for write access first and silently skips if unavailable. Set to false to disable. |
github-token | ${{ github.token }} | Token used to post the comment. Must have issues: write and pull-requests: write permissions for the comment feature to work. |
Place this step before any step that interpolates event fields into shell commands or scripts.
jobs:
build:
runs-on: ubuntu-lateststeps:
- name: Scan event payload for attack vectorsuses: devops-actions/secure-action-inputs@v1
- name: Checkoutuses: actions/checkout@v4# Safe: the scan above would have already failed the job# if the branch name contained shell injection characters.
- name: Buildrun: | echo "Building branch: ${{ github.head_ref }}"When no threats are detected the step passes and the Job Summary shows:
✅ No security threats detected in the GitHub Actions event payload.
When threats are detected the step fails and the Job Summary shows a table with per-finding context snippets (collapsed by default):
| Field | Attack Type | Details |
|---|---|---|
pull_request.title | hidden_unicode | Hidden Unicode character: Zero Width Space (U+200B) (×1) |
pull_request.head.ref | homoglyph | Homoglyph attack: Cyrillic letters visually similar to Latin ASCII (×2) |
pull_request.body | bidi_attack | Bidirectional text (Trojan Source) attack: Right-to-Left Override (U+202E) (×1) |
Each finding also includes a collapsible Location block showing the matched line ± 2 lines of context:
📍 Location: pull_request.title — single-line value
▶ Fix login bug[ZWSP]
And each finding is emitted as a workflow error annotation:
::error::[hidden_unicode] pull_request.title: Hidden Unicode character: Zero Width Space (U+200B) (×1)
::error::Security scan failed: 3 potential attack vector(s) found in 3 field(s). See the step summary for details.
When the post-comment input is enabled (default), the same report is posted (or updated) as a comment on the pull request or issue.
- The action reads the JSON event payload from
$GITHUB_EVENT_PATH. - It recursively walks every field in the payload (objects, arrays, strings).
- Each string value is checked against all detector patterns.
- Results are aggregated and written to
$GITHUB_STEP_SUMMARY. - When
show-context: true(default), each finding includes a collapsible snippet showing the matched line ± 2 lines of surrounding context. - When
post-comment: true(default) and the event is a PR or issue, the same report is posted (or updated) as a comment on the PR/issue. - If any blocking findings exist,
process.exitCodeis set to1to fail the step. Warning findings (lower-confidence matches in Markdown body fields) are reported in the summary and as annotations but do not fail the step.
The action has zero runtime dependencies — all logic is bundled into dist/index.js with @vercel/ncc, and GitHub Actions workflow commands are issued directly over stdout to avoid supply-chain risk from transitive dependencies.
MIT