Skip to content

Commit c55a6ef

Browse files
committed
security(actions): harden winget-submit against PowerShell injection
The `Resolve tag` and `Submit PR` steps interpolated `${{ github.event. release.tag_name }}` and `${{ github.event.inputs.tag }}` directly into the PowerShell `run:` body. A malicious tag like `v1.0.0';evil-code;#` could break out of the surrounding quotes and execute arbitrary code in the runner — which has access to `WINGET_PAT` (publish-to-WinGet PAT). Funnel all attacker-controllable contexts through `env:` blocks so the shell receives them as opaque env values that can't escape into syntax. Also tighten the validation from `StartsWith('v')` to a strict `^v\d+\.\d+\.\d+(-[A-Za-z0-9.-]+)?$` regex as defence-in-depth. Audit confirmed no breach: every tag is a clean `vMAJOR.MINOR.PATCH`, all workflow runs are documented manual `workflow_dispatch` from main, no `release: published` triggers. No secret rotation needed.
1 parent c851d40 commit c55a6ef

1 file changed

Lines changed: 28 additions & 11 deletions

File tree

.github/workflows/winget-submit.yml

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,25 @@ jobs:
2323
- name: Resolve tag
2424
id: tag
2525
shell: pwsh
26+
# Funnel attacker-controllable inputs (tag names, dispatch inputs)
27+
# through env vars instead of `${{ … }}` template substitution into
28+
# the script body. PowerShell treats env values as opaque strings;
29+
# template substitution would let a tag like `v1';evil-code;#` break
30+
# out of the surrounding quotes and execute arbitrary commands with
31+
# access to WINGET_PAT.
32+
# https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions
33+
env:
34+
EVENT_NAME: ${{ github.event_name }}
35+
EVENT_TAG: ${{ github.event.release.tag_name }}
36+
INPUT_TAG: ${{ github.event.inputs.tag }}
2637
run: |
27-
if ('${{ github.event_name }}' -eq 'release') {
28-
$t = '${{ github.event.release.tag_name }}'
29-
} else {
30-
$t = '${{ github.event.inputs.tag }}'
38+
$t = if ($env:EVENT_NAME -eq 'release') { $env:EVENT_TAG } else { $env:INPUT_TAG }
39+
# Strict tag-name validation — accept only `vMAJOR.MINOR.PATCH`
40+
# optionally followed by a prerelease suffix. Defense-in-depth in
41+
# case the env-var funnel is ever bypassed.
42+
if ($t -notmatch '^v\d+\.\d+\.\d+(-[A-Za-z0-9.-]+)?$') {
43+
throw "Tag '$t' must match vMAJOR.MINOR.PATCH (optionally -prerelease)"
3144
}
32-
if (-not $t.StartsWith('v')) { throw "Tag must start with v" }
3345
$ver = $t.Substring(1)
3446
echo "TAG=$t" >> $env:GITHUB_OUTPUT
3547
echo "VERSION=$ver" >> $env:GITHUB_OUTPUT
@@ -43,13 +55,18 @@ jobs:
4355
4456
- name: Submit PR
4557
shell: pwsh
58+
env:
59+
# Same hardening for the second `run:` body — env-var everything
60+
# that gets substituted into the script.
61+
TAG: ${{ steps.tag.outputs.TAG }}
62+
VERSION: ${{ steps.tag.outputs.VERSION }}
63+
REPOSITORY: ${{ github.repository }}
64+
WINGET_PAT: ${{ secrets.WINGET_PAT }}
4665
run: |
47-
$tag = "${{ steps.tag.outputs.TAG }}"
48-
$ver = "${{ steps.tag.outputs.VERSION }}"
49-
$url = "https://github.com/${{ github.repository }}/releases/download/$tag/$env:ASSET_NAME"
50-
Write-Host "Submitting $env:PACKAGE_IDENTIFIER v$ver from $url"
66+
$url = "https://github.com/$($env:REPOSITORY)/releases/download/$($env:TAG)/$($env:ASSET_NAME)"
67+
Write-Host "Submitting $env:PACKAGE_IDENTIFIER v$($env:VERSION) from $url"
5168
./wingetcreate.exe update $env:PACKAGE_IDENTIFIER `
52-
--version $ver `
69+
--version $env:VERSION `
5370
--urls $url `
5471
--submit `
55-
--token "${{ secrets.WINGET_PAT }}"
72+
--token $env:WINGET_PAT

0 commit comments

Comments
 (0)