Skip to content

Remove inputs.ref and sanitize branch name to prevent GITHUB_ENV injection#312

Merged
litlfred merged 5 commits intomainfrom
copilot/switch-triggers-to-pull-request-target
Mar 11, 2026
Merged

Remove inputs.ref and sanitize branch name to prevent GITHUB_ENV injection#312
litlfred merged 5 commits intomainfrom
copilot/switch-triggers-to-pull-request-target

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 11, 2026

inputs.ref allowed callers to supply an arbitrary Git ref, enabling untrusted code execution. After its removal, github.head_ref (user-controlled on pull_request_target) was still written unsanitized into $GITHUB_ENV, allowing newline injection — e.g. a branch named feat%0ASECRET=injected would add SECRET=injected to the environment of all subsequent steps.

Changes

ghbuild.yml

  • Remove ref input from workflow_call and workflow_dispatch
  • Remove trusted-scripts overlay steps (were conditioned on inputs.ref != '')
  • Drop explicit ref: from actions/checkout — uses event context default
  • Sanitize ACTUAL_BRANCH before writing to $GITHUB_ENV:
    ACTUAL_BRANCH="$(printf '%s' "$ACTUAL_BRANCH" | tr -cd 'a-zA-Z0-9._/-')"
    printf 'BRANCH_NAME=%s\n' "$ACTUAL_BRANCH" >> "$GITHUB_ENV"
  • Remove dead "pull_request" event check in Find PR number (workflow never fires directly on pull_request)

ci.yml / pr-preview.yml

  • Remove ref: ${{ github.event.pull_request.head.sha }} from ghbuild.yml call sites
Original prompt

Problem

GitHub Actions workflow runs triggered by commits from the Copilot coding agent (e.g., on copilot/fix-* branches) require manual maintainer approval before running, because the bot's author_association is CONTRIBUTOR. This blocks CI feedback on every Copilot-agent PR.

See: https://github.com/WorldHealthOrganization/smart-base/actions/runs/22950104097

Solution

Switch ci.yml and pr-preview.yml from pull_request to pull_request_target trigger, and implement a split-checkout security model in ghbuild.yml so that:

  • PR branch content (FSH, BPMN, YAML configs, JSON, images, translations) is checked out for building
  • Trusted scripts from main (input/scripts/) are overlaid on top, so only reviewed code is ever executed

This eliminates the maintainer approval gate for Copilot-agent PRs while maintaining security — untrusted PR code is never executed, only built as data.

Changes required

1. ci.yml

  • Change pull_request: trigger to pull_request_target:
  • Pass ref: ${{ github.event.pull_request.head.sha || '' }} to ghbuild.yml
  • Update the deploy condition from github.event_name != 'pull_request' to github.event_name != 'pull_request_target'

2. pr-preview.yml

  • Change pull_request: trigger to pull_request_target:
  • Pass the PR head ref to ghbuild.yml via a new ref input

3. ghbuild.yml

  • Add ref as an optional input to workflow_call
  • After the existing checkout step (which checks out PR content via ref), add a second checkout that uses actions/checkout@v4 with ref: main, sparse-checkout: input/scripts, and path: __trusted_scripts__
  • Add a step that overlays the trusted scripts: rm -rf input/scripts && cp -r __trusted_scripts__/input/scripts input/scripts && rm -rf __trusted_scripts__
  • This overlay step should only run when the event is pull_request_target (for push and workflow_dispatch, the checked-out code is already trusted since the committer has write access)

Security model

The key insight: with pull_request_target, the workflow YAML is read from the base branch (main), so an attacker can't modify the workflow itself. The split-checkout ensures input/scripts/*.py (which get executed by python3) always come from main. The PR branch only provides data files (FSH, BPMN, XML, YAML, JSON, images) that are processed by the trusted scripts but never executed directly.

Files that are executed and must come from main:

  • input/scripts/*.py — all Python scripts run by python3

Files that are data and safe to come from the PR branch:

  • input/fsh/** — FSH definitions processed by SUSHI
  • input/business-processes/*.bpmn — BPMN XML validated by scripts
  • sushi-config.yaml, dak.json — config files read by scripts
  • input/images/**, input/pagecontent/** — static content
  • input/translations/** — translation files

Important edge case

When a PR is developing new scripts (e.g., the skills infrastructure PR), those new scripts won't run in CI until merged to main. This is the correct security behavior — new executable code must be reviewed and merged before it can run with elevated privileges. Content PRs (the common case) work perfectly.

NOT in scope

  • Do NOT disable any DAK preprocessing or postprocessing steps — those should all remain as-is
  • Do NOT change ghbuild.yml workflow triggers (it should remain workflow_call + workflow_dispatch only)
  • Do NOT modify any Python scripts

The following is the prior conversation context from the user's chat exploration (may be truncated):

User: https://github.com/WorldHealthOrganization/smart-base/actions/runs/22950104097
this is an action waiting on approval by a maintainer. this was an action triggered by a commit to a feature branch by copilot. what can we do so that the maintainer does not need to approve

Assistant: Good question! Here's what's happening and what you can do about it:

Why the workflow is waiting for approval

The workflow run on WorldHealthOrganization/smart-base branch copilot/fix-invalid-workflow-file (PR #311) is in "Waiting for approval" state. This happens because:

  1. The Copilot coding agent (copilot-swe-agent) is the commit author, and its author_association is "CONTRIBUTOR" — not MEMBER/COLLABORATOR/OWNER.
  2. GitHub's default policy for public repos requires maintainer approval for workflow runs from first-time or external contributors.

Options to fix this (ranked by practicality)

Option 1: Change the org-level fork approval setting ✅ (Recommended)

Since these are not fork PRs — the copilot/fix-* branches are in the same repo (WorldHealthOrganization/smart-base), not a fork — this shouldn't normally require approval. But the Copilot bot is treated as an outside contributor.

A WHO org admin can go to:

**Organization Settings → Actions → General → "Approval for running fork pull request workflows from contributors"*...

This pull request was created from Copilot chat.


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

…eckout security model in ghbuild.yml

Co-authored-by: litlfred <662242+litlfred@users.noreply.github.com>
Copilot AI changed the title [WIP] Switch GitHub Actions to use pull_request_target trigger Switch CI/PR-preview to pull_request_target with split-checkout security model Mar 11, 2026
…_request_target

Co-authored-by: litlfred <662242+litlfred@users.noreply.github.com>
Copilot AI changed the title Switch CI/PR-preview to pull_request_target with split-checkout security model Switch CI triggers to pull_request_target with split-checkout security model Mar 11, 2026
Co-authored-by: litlfred <662242+litlfred@users.noreply.github.com>
Copilot AI changed the title Switch CI triggers to pull_request_target with split-checkout security model Remove inputs.ref from ghbuild.yml to eliminate arbitrary-ref injection risk Mar 11, 2026
@litlfred litlfred marked this pull request as ready for review March 11, 2026 15:50
Copilot AI review requested due to automatic review settings March 11, 2026 15:50
Co-authored-by: litlfred <662242+litlfred@users.noreply.github.com>
@litlfred litlfred merged commit df62007 into main Mar 11, 2026
3 checks passed
Copilot AI changed the title Remove inputs.ref from ghbuild.yml to eliminate arbitrary-ref injection risk Remove inputs.ref and sanitize branch name to prevent GITHUB_ENV injection Mar 11, 2026
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 aims to eliminate an arbitrary Git ref injection vector in the reusable GitHub Actions build workflow by removing the ref input and relying on the default event ref, updating the CI and PR preview workflows accordingly.

Changes:

  • Removed the ref input from ghbuild.yml and simplified checkout/branch-detection logic.
  • Switched ci.yml and pr-preview.yml triggers to pull_request_target and updated deploy gating/conditions.
  • Updated PR-number detection in ghbuild.yml to match the new event type.

Reviewed changes

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

File Description
.github/workflows/ghbuild.yml Removes ref input and explicit checkout ref; updates PR detection and branch naming logic.
.github/workflows/ci.yml Switches PR trigger to pull_request_target and adjusts deploy + commit-pot conditions accordingly.
.github/workflows/pr-preview.yml Switches preview trigger to pull_request_target while continuing to call ghbuild.yml for deployment.

Comment thread .github/workflows/ci.yml
Comment on lines +25 to 26
pull_request_target:
branches-ignore:
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

Switching this workflow to pull_request_target changes github.ref to the base branch ref for PR runs. With the current concurrency.group: deploy-${{ github.ref }}, all PRs targeting the same base branch will share a concurrency group and can cancel each other (and potentially cancel a concurrent push build on that base branch). Use a PR-unique concurrency key for pull_request_target (e.g., PR number or github.head_ref) while keeping the push behavior unchanged.

Copilot uses AI. Check for mistakes.
Comment thread .github/workflows/ci.yml
Comment on lines +25 to 26
pull_request_target:
branches-ignore:
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

pull_request_target runs for fork PRs too, but this workflow grants contents: write and pull-requests: write to the called build job. Even if the checkout is “trusted”, this is still elevated permission on attacker-controlled events and can be abused for repo/PR write actions (spam, resource abuse, etc.). Consider skipping fork PRs (as pr-preview does) or reducing permissions for PR-target runs.

Copilot uses AI. Check for mistakes.

on:
pull_request:
pull_request_target:
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

With pull_request_target here and ghbuild.yml now doing a default checkout, the preview deployment will build/deploy the base branch content into a folder named after the PR branch. That makes the preview URL misleading (it won’t contain the PR’s changes). Either restore PR-head checkout in ghbuild.yml for pull_request_target runs (with a safe/split-checkout approach), or keep this workflow on pull_request so the checkout defaults to the PR merge ref.

Suggested change
pull_request_target:
pull_request:

Copilot uses AI. Check for mistakes.
Comment on lines 79 to 82
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}

Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

With actions/checkout@v4 using the default ref, runs invoked via workflow_call from pull_request_target will check out the base branch commit (not the PR head). That means CI/preview builds won’t reflect PR changes, while PR comments/branch metadata still refer to the PR branch. Consider explicitly checking out github.event.pull_request.head.sha for pull_request_target runs (and, if needed, keep the “trusted scripts overlay”/split-checkout mitigation rather than accepting an arbitrary inputs.ref).

Copilot uses AI. Check for mistakes.
Copilot stopped work on behalf of litlfred due to an error March 11, 2026 15:56
Copilot AI added a commit that referenced this pull request Mar 11, 2026
… pull_request for ci.yml

Merge upstream security changes from PR #312 with our Python-based
script injection hardening approach.

Key merge decisions:
- ci.yml: Keep pull_request trigger (NOT pull_request_target).
  CI validation must build the PR code, not the base branch.
  deploy: false on PRs already prevents secrets exposure.
- pr-preview.yml: Adopt pull_request_target from upstream with our
  approve + deploy-preview split (structurally better). Pass PR head
  SHA via ref input so ghbuild.yml builds the PR code after approval.
- ghbuild.yml: Keep our Python scripts approach for sanitization.
  Move ref input from workflow_dispatch to workflow_call (needed by
  pr-preview.yml to pass the PR head SHA for checkout).
- find_pr.py: Handle both pull_request and pull_request_target events.
- resolve_branch.py: Remove INPUT_REF handling — branch name always
  comes from head_ref or ref_name, never from the checkout ref.
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.

3 participants