fix: argument injection in git archive fallback (CWE-88) - #49500
Conversation
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
PR Triage
Note: Draft, security-relevant (argument injection) but no diff yet - monitor closely once ready
|
|
✅ Design Decision Gate 🏗️ completed the design decision gate check. No ADR enforcement needed: PR #49500 does not have the 'implementation' label and has only 78 new lines of code in business logic directories (threshold: 100). |
|
✅ Test Quality Sentinel completed test quality analysis. |
|
🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅ |
|
✅ PR Code Quality Reviewer completed the code quality review. |
There was a problem hiding this comment.
Pull request overview
Hardens the Git archive fallback against CWE-88 argument injection, addressing #49498.
Changes:
- Validates Git refs and paths and adds an end-of-options separator.
- Adds regression tests for malicious inputs.
- Includes an unrelated generated workflow update requiring removal or explanation.
Show a summary per file
| File | Description |
|---|---|
pkg/cli/download_workflow.go |
Secures Git archive arguments. |
pkg/cli/download_workflow_test.go |
Tests rejected refs and paths. |
.github/workflows/daily-byok-ollama-test.lock.yml |
Updates unrelated threat-detection configuration. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 3/3 changed files
- Comments generated: 1
- Review effort level: Balanced
| (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) | ||
| GH_AW_MAX_AI_CREDITS="${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }}" | ||
| printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.43/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"raw.githubusercontent.com\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"targets\":{\"copilot\":{\"host\":\"host.docker.internal:11434\"}}},\"container\":{\"imageTag\":\"0.27.43,squid=sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d,agent=sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6,api-proxy=sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1,cli-proxy=sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" | ||
| printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.43/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"raw.githubusercontent.com\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"defaultAiCreditsPricing\":{\"input\":0.000001,\"output\":0.000001},\"targets\":{\"copilot\":{\"host\":\"host.docker.internal:11434\"}},\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.43,squid=sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d,agent=sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6,api-proxy=sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1,cli-proxy=sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" |
There was a problem hiding this comment.
The fix correctly addresses CWE-88 argument injection in downloadWorkflowContentViaGit. ValidateGitRef and ValidateGitPath reject empty values, leading dash, NUL bytes, and dotdot traversal before any subprocess is spawned. The -- end-of-options separator adds defence-in-depth. Tests cover all six injection/traversal cases. LGTM.
🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 25.5 AIC · ⌖ 11.8 AIC · ⊞ 5.3K
There was a problem hiding this comment.
Skills-Based Review 🧠
Applied /diagnosing-bugs and /tdd — requesting changes on two small but important gaps.
📋 Key Themes & Highlights
Key Themes
- Clone fallback not fully hardened:
downloadWorkflowContentViaGitCloneacceptsrefwithout validation and passes it togit checkoutwithout a--separator. It is only reachable through the validated archive path today, but standalone callers would be vulnerable. #noseccomment inaccuracy: The suppression comment says ref/path are developer-authored, but the threat model in the PR description correctly identifies them as attacker-controlled. The mismatch undermines the value of future security audits.- Missing happy-path test: All 6 test cases assert rejection; none assert that valid inputs actually pass validation and proceed (even to a subprocess failure). This leaves a logic inversion undetected.
Positive Highlights
- ✅ Core fix is correct and minimal — mirrors the pattern from
pkg/parser/remote_download_file.goexactly - ✅
--end-of-options separator is defence-in-depth even after validation — correct approach - ✅ Test names are clear specifications; table-driven structure is clean
- ✅
gitutil.ValidateGitRef/ValidateGitPathreuse existing, tested helpers rather than reinventing validation
🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 38.9 AIC · ⌖ 11.8 AIC · ⊞ 7K
Comment /matt to run again
Comments that could not be inline-anchored
pkg/cli/download_workflow.go:150
[/diagnosing-bugs] downloadWorkflowContentViaGitClone passes ref to git checkout without a -- end-of-options separator. Currently reachable only via downloadWorkflowContentViaGit (which validates first), but if ever called directly a leading-dash ref could be parsed as a git flag, bypassing the fix.
<details>
<summary>💡 Suggested fix</summary>
Add ValidateGitRef(ref) at the top of downloadWorkflowContentViaGitClone and use -- in checkout:
// top of downloadWorkflowC…
</details>
<details><summary>pkg/cli/download_workflow.go:47</summary>
**[/diagnosing-bugs]** The `#nosec G204` comment says ref/path are "authored by the developer", but the PR's own threat model shows they come from attacker-controlled `source:`/`redirect:` frontmatter. The comment should be updated to accurately reflect the trust boundary.
<details>
<summary>💡 Suggested update</summary>
```go
// #nosec G204 -- repoURL is derived from the developer's workflow import configuration.
// ref and path are user-supplied values validated above by ValidateGitRef/Vali…
</details>
<details><summary>pkg/cli/download_workflow_test.go:140</summary>
**[/tdd]** The test suite covers all 6 rejection cases but has no happy-path test. Without one, a future refactor that breaks the early-return logic (e.g. validation always returns an error) would go undetected.
<details>
<summary>💡 Suggested addition</summary>
Add a table entry with a valid repo/ref/path that should *not* error on validation. Since the subprocess will fail in a test environment, assert the error does **not** contain `"refusing git fallback"`:
```go
{
name: "valid input…
</details>
🧪 Test Quality Sentinel Report✅ Test Quality Score: 85/100 — Excellent
📊 Metrics (1 test, 6 subtests)
Test Details:
✅ Test Quality Strengths
Verdict
|
There was a problem hiding this comment.
Verdict: Approve
Clean, well-tested CWE-88 fix. Reuses existing gitutil.ValidateGitRef/ValidateGitPath validators consistently with pkg/parser/remote_download_file.go, adds the -- end-of-options separator as defence-in-depth, and includes solid negative-path test coverage (dash-prefixed ref/path, traversal, empty values) verifying the subprocess is never spawned.
💡 Notes
No blocking issues found in the diff. The lock.yml change appears to be an unrelated auto-generated regeneration artifact bundled into the PR — worth double-checking it wasn't accidentally included, but it's not a security or correctness concern for this fix.
🔎 Code quality review by PR Code Quality Reviewer · auto · 17.2 AIC · ⌖ 11.2 AIC · ⊞ 7.8K
Comment /review to run again
|
@copilot run pr-finisher skill |
Investigated the lock file diff. The |
|
🎉 This pull request is included in a new release. Release: |
The
downloadWorkflowContentViaGitinpkg/cli/download_workflow.gopassedpathdirectly togit archivewithout validation or an end-of-options separator. An attacker-controlledsource:/redirect:frontmatter value like--output=/home/dev/.ssh/authorized_keyswould be parsed by git as a flag, enabling arbitrary local file writes via the auth-error fallback path.Changes
pkg/cli/download_workflow.go: Mirror the hardening already applied inpkg/parser/remote_download_file.go:gitutil.ValidateGitRef(ref)andgitutil.ValidateGitPath(path)before constructing the command, rejecting values that start with-, contain.., or are empty"--"end-of-options separator beforepathpkg/cli/download_workflow_test.go: AddTestDownloadWorkflowContentViaGitValidationcovering path injection (--output=), ref injection, path traversal (../),..in ref, empty ref, and empty path — all verified to return early with"refusing git fallback"before any subprocess is spawned.