Skip to content

ci(security): use workflow_run mitigation#133

Merged
sethrylan merged 5 commits intomainfrom
sec-demo
Feb 24, 2026
Merged

ci(security): use workflow_run mitigation#133
sethrylan merged 5 commits intomainfrom
sec-demo

Conversation

@sethrylan
Copy link
Copy Markdown
Owner

@sethrylan sethrylan commented Feb 24, 2026

Summary

Replaces the single demo.yml workflow with a three-workflow architecture to mitigate a pwn request vulnerability (code scanning alerts #4, #6). The /demo comment UX is preserved.

Vulnerability

The original demo.yml workflow combined an issue_comment trigger with an explicit checkout of the PR head ref. This is a pwn request — the workflow ran in a privileged context (contents: write, access to CI_BOT_APP_ID / CI_BOT_APP_PRIVATE_KEY secrets) while executing attacker-controlled code (docs/demo-setup.sh, docs/demo.tape). Any GitHub user who could comment on a PR could trigger it with /demo.

Risk factors:

  • Untrusted code execution with secrets: Scripts from the PR ran with access to the GitHub App private key and a write-capable GITHUB_TOKEN
  • Token persistence: The default actions/checkout stores credentials on disk (persist-credentials: true), making the token available to any process in subsequent steps
  • No access control: Any commenter could trigger the workflow, not just collaborators

Solution

Split into three workflows, each with a single responsibility and minimal privilege:

sequenceDiagram
    participant U as Collaborator
    participant D as demo-dispatch.yml<br/>issue_comment<br/>🔒 actions: write
    participant G as demo-generate.yml<br/>workflow_dispatch<br/>🔒 contents: read
    participant P as demo-publish.yml<br/>workflow_run<br/>🔓 contents: write + secrets

    U->>D: Comments "/demo" on PR
    Note over D: Validates author_association<br/>(OWNER, MEMBER, COLLABORATOR)<br/>No checkout, no code execution
    D->>G: gh workflow run -f pr_number=N
    Note over G: Checks out PR code<br/>(persist-credentials: false)<br/>Runs demo-setup.sh + VHS<br/>⚠️ Untrusted code runs here
    G-->>G: Uploads GIF + PR metadata artifacts
    G->>P: workflow_run (completed)
    Note over P: Downloads & validates artifacts<br/>Creates GitHub App token<br/>Commits GIF, posts comment<br/>✅ No untrusted code
    P-->>U: Sticky PR comment with demo GIF
Loading
Workflow Trigger Permissions Role
demo-dispatch.yml issue_comment actions: write Zero-privilege shim: validates commenter is OWNER/MEMBER/COLLABORATOR, dispatches demo-generate via workflow_dispatch. No checkout, no code execution, no secrets.
demo-generate.yml workflow_dispatch contents: read, pull-requests: read Checks out PR code with persist-credentials: false, runs demo-setup.sh and VHS, uploads GIF + PR metadata as artifacts. No access to app secrets. Can only be invoked by users with write access (or by the dispatch shim).
demo-publish.yml workflow_run contents: write, pull-requests: write Downloads and validates artifacts (PR number must be numeric, head ref must match ^[a-zA-Z0-9._/-]+$), creates GitHub App token, commits GIF to PR branch, posts sticky comment. Never executes untrusted code.

Why three workflows instead of two?

The previous iteration used issue_comment directly on demo-generate.yml with an author_association guard. While this prevented untrusted users from triggering the workflow, issue_comment still runs in a privileged context — the GITHUB_TOKEN has whatever permissions are declared, and those permissions are available to untrusted code after checkout.

By adding a zero-privilege dispatch shim:

  • The issue_comment handler has zero attack surface — it never checks out code, never runs scripts, and its only permission is actions: write (dispatch other workflows)
  • demo-generate.yml uses workflow_dispatch, which GitHub restricts to users with write access — the trust boundary is enforced by GitHub's permission model, not by an if condition
  • Even if the dispatch shim were somehow compromised, it can only trigger demo-generate.yml which has no secrets

Security properties

Property Before (demo.yml) After (three workflows)
Who can trigger? Any commenter Only OWNER/MEMBER/COLLABORATOR (dispatch shim) or users with write access (direct workflow_dispatch)
Secrets during untrusted code execution App private key + write GITHUB_TOKEN available and persisted on disk No app secrets; GITHUB_TOKEN is read-only and persist-credentials: false prevents disk persistence
Blast radius if PR script is malicious Full repo compromise, secret exfiltration Read-only access to public repo contents; no secrets to exfiltrate
Artifact trust N/A PR number and head ref are validated before use in privileged context
Token persistence Default (true) — stored on disk persist-credentials: false — not stored on disk

Files changed

File Change
.github/workflows/demo.yml Deleted — replaced by three workflows below
.github/workflows/demo-dispatch.yml New — zero-privilege issue_comment shim
.github/workflows/demo-generate.yml New — low-privilege workflow_dispatch GIF generator
.github/workflows/demo-publish.yml New — privileged workflow_run publisher
docs/readme.md Updated to document three-workflow architecture with mermaid diagram

References

Testing

  • Comment /demo on a test PR as a collaborator → verify GIF is generated, committed, and comment is posted
  • Comment /demo as a non-collaborator → verify workflow does not trigger
  • Run demo-generate.yml directly via Actions tab or gh workflow run → verify it works independently of the comment shim
  • Verify demo-publish.yml rejects artifacts with non-numeric PR numbers or invalid head refs

@sethrylan sethrylan marked this pull request as ready for review February 24, 2026 00:56
@sethrylan sethrylan self-assigned this Feb 24, 2026
@sethrylan sethrylan requested a review from Copilot February 24, 2026 00:56
@sethrylan sethrylan changed the title sec: use workflow_run mitigation ci(security): use workflow_run mitigation Feb 24, 2026
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a security mitigation for the demo workflow by splitting it into two separate workflows following the workflow_run pattern to prevent "pwn request" attacks. The original single workflow that executed untrusted PR code with write permissions has been replaced with a secure two-workflow architecture: a low-privilege workflow that executes untrusted code from PRs, and a high-privilege workflow that only handles validated artifacts.

Changes:

  • Split the single demo.yml workflow into demo-generate.yml (low-privilege, executes untrusted code) and demo-publish.yml (high-privilege, handles artifacts only)
  • Added comprehensive documentation explaining the security challenges and mitigation strategies for workflows that write to pull requests
  • Implemented artifact validation in the publish workflow to prevent malicious artifact injection

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.

File Description
.github/workflows/demo.yml Removed original monolithic workflow that had security vulnerabilities
.github/workflows/demo-generate.yml New low-privilege workflow that executes untrusted PR code with restricted permissions and persist-credentials: false
.github/workflows/demo-publish.yml New high-privilege workflow triggered by workflow_run that validates artifacts and commits results
docs/readme.md Added extensive documentation explaining the security pitfalls and mitigation strategies for PR-writing workflows
Comments suppressed due to low confidence (2)

.github/workflows/demo-publish.yml:47

  • The unzip commands do not validate the integrity or structure of the extracted files. An attacker could potentially craft malicious zip files with path traversal attacks (e.g., files with ../../ in their names). Consider using unzip -q with additional safeguards, or verify that extracted files only contain expected filenames before proceeding. You could add validation like checking that demo-gif/demo.gif exists and pr-metadata/number and pr-metadata/head_ref exist with no other unexpected files.
        run: |
          mkdir -p demo-gif pr-metadata
          unzip -d demo-gif/ demo-gif.zip
          unzip -d pr-metadata/ pr-metadata.zip

.github/workflows/demo-publish.yml:81

  • After extracting the artifacts, the workflow immediately copies the demo.gif file without verifying it exists or is a valid file. Consider adding validation to check that demo-gif/demo.gif exists and has a reasonable size before copying it. This would catch potential issues with corrupted artifacts or unexpected archive structures.
      - name: Copy demo GIF and commit
        run: |
          cp demo-gif/demo.gif docs/demo.gif

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

sethrylan and others added 3 commits February 23, 2026 20:02
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@sethrylan sethrylan requested a review from Copilot February 24, 2026 01:18
@sethrylan sethrylan merged commit 35fa181 into main Feb 24, 2026
11 checks passed
@sethrylan sethrylan deleted the sec-demo branch February 24, 2026 01:23
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (1)

docs/readme.md:67

  • Inconsistent terminology: This section refers to demo-dispatch.yml as "the trigger shim", but the document header calls it "Zero-privilege dispatcher". Consider using consistent terminology throughout the document, preferably "dispatcher" since that matches the filename and is more descriptive.
- Triggered by `workflow_dispatch` with a `pr_number` input — can only be invoked by users with write access or by the trigger shim

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


This is a common attack pattern known as a "pwn request"; GitHub's default PR security has mitigations to prevent this for `pull_request` workflows by disabling write permissions. But for PRs that need additional write permissions (e.g., to commit the generated GIF), it's important to implement additional protections.

The demo workflows use `workflow_run`, which was introduced to enable scenarios that require building untrusted code and also writing to update the PR (e.g., committing the generated GIF, or code coverage reports).
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The statement "The demo workflows use workflow_run" is slightly misleading. The solution uses both workflow_dispatch (for controlled dispatching from demo-dispatch to demo-generate) and workflow_run (for triggering demo-publish after demo-generate completes). Consider clarifying this to say "The demo workflows use a combination of workflow_dispatch and workflow_run" to better reflect the two-stage dispatch mechanism.

This issue also appears on line 67 of the same file.

Suggested change
The demo workflows use `workflow_run`, which was introduced to enable scenarios that require building untrusted code and also writing to update the PR (e.g., committing the generated GIF, or code coverage reports).
The demo workflows use a combination of `workflow_dispatch` and `workflow_run`, which were introduced to enable scenarios that require building untrusted code and also writing to update the PR (e.g., committing the generated GIF, or code coverage reports).

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,24 @@
name: demo-trigger
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow name is "demo-trigger" but the filename is "demo-dispatch.yml". This inconsistency could cause confusion. Consider renaming the workflow name to "demo-dispatch" to match the filename, or vice versa. The documentation in docs/readme.md refers to this as "demo-dispatch.yml".

Suggested change
name: demo-trigger
name: demo-dispatch

Copilot uses AI. Check for mistakes.
exit 1
fi

if ! echo "$head_ref" | grep -qE '^[a-zA-Z0-9._/-]+$'; then
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The head_ref validation regex could be more restrictive to prevent edge cases. Consider disallowing branch names that start with dots, slashes, or contain consecutive slashes, as these could potentially cause issues in git operations or URL construction. For example: ^[a-zA-Z0-9][a-zA-Z0-9._-]*(/[a-zA-Z0-9._-]+)*$. This ensures branch names start with an alphanumeric character and don't have leading/trailing slashes or consecutive slashes.

Suggested change
if ! echo "$head_ref" | grep -qE '^[a-zA-Z0-9._/-]+$'; then
if ! echo "$head_ref" | grep -qE '^[a-zA-Z0-9][a-zA-Z0-9._-]*(/[a-zA-Z0-9._-]+)*$'; then

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +24
- name: Trigger demo generation
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh workflow run demo-generate.yml \
--repo ${{ github.repository }} \
-f pr_number=${{ github.event.issue.number }}
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a reaction to the comment to provide immediate feedback to the user that their request was received. The previous demo.yml workflow had an "eyes" reaction step that gave users confirmation the workflow was triggered. Without this, users may be unsure if their /demo command was processed.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants