Skip to content

Commit 8c78987

Browse files
authored
Merge branch 'main' into copilot/fix-release-workflow-process
2 parents c4072dd + 806060f commit 8c78987

File tree

6 files changed

+400
-14
lines changed

6 files changed

+400
-14
lines changed
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
---
2+
applyTo: ".github/workflows/**"
3+
---
4+
5+
# Workflow Security: Validating Untrusted User Input
6+
7+
## Overview
8+
9+
Workflows in this repository that are triggered by untrusted user input (issue
10+
bodies, PR descriptions, comments, branch names, etc.) **must** validate that
11+
input for hidden characters and potential prompt injection before processing it.
12+
13+
This is especially important for workflows that pass user content to AI/LLM
14+
systems (e.g. GitHub Copilot agents), but also applies to any automated
15+
processing where a malicious actor could influence the workflow's behavior.
16+
17+
## The Central Validation Script
18+
19+
**`.github/workflows/validate-input.sh`** is the single, authoritative script
20+
for this check. It detects:
21+
22+
| Threat | Description |
23+
|--------|-------------|
24+
| Bidirectional Unicode control characters | Trojan Source attack (CVE-2021-42574) — makes text look different to humans vs. AI |
25+
| Zero-width / invisible characters | Hidden text injected between visible characters, invisible to human reviewers |
26+
| Unicode tag characters (U+E0000–E007F) | Completely invisible; can encode arbitrary ASCII instructions |
27+
| Unicode variation selectors | Can steganographically encode hidden data |
28+
| HTML comment blocks (`<!-- ... -->`) | Stripped by GitHub's renderer but fully visible to LLMs processing raw Markdown |
29+
| Non-printable control characters | Unexpected control bytes that may confuse parsers |
30+
31+
If any of the above are found, the script:
32+
1. **Posts a warning comment** to the issue or PR, listing every finding and
33+
linking back to the workflow run that caught it.
34+
2. **Exits with a non-zero code**, failing the workflow job immediately so that
35+
no further processing occurs on the untrusted content.
36+
37+
## How to Use the Script in a Workflow
38+
39+
Add a validation step **before** any step that reads or processes the untrusted
40+
input. The step must run after the repository is checked out (so the script file
41+
is available), and it needs a `GH_TOKEN` with write access to post comments.
42+
43+
```yaml
44+
- name: Validate <input source> for hidden content
45+
env:
46+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
47+
INPUT_TEXT: ${{ github.event.issue.body }} # ← the untrusted text
48+
ITEM_NUMBER: ${{ github.event.issue.number }} # ← issue or PR number
49+
REPO: ${{ github.repository }}
50+
RUN_ID: ${{ github.run_id }}
51+
SERVER_URL: ${{ github.server_url }}
52+
CONTEXT_TYPE: issue # "issue" or "pr"
53+
FINDINGS_FILE: /tmp/validation-findings.txt
54+
run: bash .github/workflows/validate-input.sh
55+
```
56+
57+
For a pull request body, swap the event expressions and set `CONTEXT_TYPE: pr`:
58+
59+
```yaml
60+
- name: Validate PR body for hidden content
61+
env:
62+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
63+
INPUT_TEXT: ${{ github.event.pull_request.body }}
64+
ITEM_NUMBER: ${{ github.event.pull_request.number }}
65+
REPO: ${{ github.repository }}
66+
RUN_ID: ${{ github.run_id }}
67+
SERVER_URL: ${{ github.server_url }}
68+
CONTEXT_TYPE: pr
69+
FINDINGS_FILE: /tmp/validation-findings.txt
70+
run: bash .github/workflows/validate-input.sh
71+
```
72+
73+
## Deciding Whether a Workflow Needs Validation
74+
75+
Apply the validation step when **all** of the following are true:
76+
77+
1. The workflow is triggered by a user-controllable event:
78+
`issues`, `issue_comment`, `pull_request`, `pull_request_review`,
79+
`pull_request_review_comment`, `discussion`, `discussion_comment`, etc.
80+
2. The workflow reads a **text field** from the event payload that a user wrote:
81+
`.body`, `.title`, `.comment.body`, `.review.body`, branch names, etc.
82+
3. That text is subsequently processed by an automated system (especially an AI).
83+
84+
You do **not** need the script for:
85+
- Purely numeric fields like `issue.number` or `pull_request.number`.
86+
- Internal, trusted triggers (`workflow_dispatch` with controlled inputs,
87+
`push` to protected branches, `schedule`, etc.).
88+
- Metadata-only fields like `pull_request.draft` or `label.name`.
89+
90+
## Permissions
91+
92+
The validation step requires the `issues: write` (or `pull-requests: write`)
93+
permission on the job so the `gh` CLI can post the warning comment:
94+
95+
```yaml
96+
jobs:
97+
my-job:
98+
permissions:
99+
issues: write # needed to post the warning comment
100+
contents: read
101+
```
102+
103+
## Keeping the Script Up to Date
104+
105+
If you discover a new class of hidden-character or injection attack not already
106+
covered, add a new detection block to `.github/workflows/validate-input.sh`
107+
under its clearly-labelled sections. Keep detection logic inside the Python
108+
heredoc so Unicode handling is reliable across all runners.
109+
110+
Document any new threat type with:
111+
- A short comment explaining the attack and why it is dangerous.
112+
- An example of the Unicode code points or patterns being detected.
113+
- A human-readable finding message added to the `findings` list.

.github/skills/azure-storage-loader/package-lock.json

Lines changed: 27 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.github/workflows/actionlint.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: actionlint
2+
3+
on:
4+
push:
5+
paths:
6+
- '.github/workflows/**'
7+
pull_request:
8+
paths:
9+
- '.github/workflows/**'
10+
11+
permissions:
12+
contents: read
13+
pull-requests: write
14+
15+
jobs:
16+
run-actionlint:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- name: Harden the runner (Audit all outbound calls)
20+
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
21+
with:
22+
egress-policy: audit
23+
24+
- name: Checkout code
25+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
26+
27+
- name: Run actionlint
28+
uses: devops-actions/actionlint@469810fd82c015d3c43815cd2b0e4d02eecc4819 # v0.1.11

.github/workflows/check-toolnames.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@ jobs:
2727
with:
2828
ref: main
2929

30+
- name: Validate issue body for hidden content
31+
env:
32+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
33+
INPUT_TEXT: ${{ github.event.issue.body }}
34+
ITEM_NUMBER: ${{ github.event.issue.number }}
35+
REPO: ${{ github.repository }}
36+
RUN_ID: ${{ github.run_id }}
37+
SERVER_URL: ${{ github.server_url }}
38+
CONTEXT_TYPE: issue
39+
FINDINGS_FILE: /tmp/validation-findings.txt
40+
run: bash .github/workflows/validate-input.sh
41+
3042
- name: Check toolnames and post comment
3143
env:
3244
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

0 commit comments

Comments
 (0)