Fix GraphQL ID injection in project_command.go (Semgrep #627/#628, CWE-89)#43467
Conversation
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
…eProject and linkProjectToRepo Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
|
|
|
✅ Test Quality Sentinel completed test quality analysis. No test files were added or modified in this PR. Test Quality Sentinel skipped. |
|
✅ Design Decision Gate 🏗️ completed the design decision gate check. No ADR enforcement needed: PR does not have the 'implementation' label and has ≤100 new lines of code in business logic directories (56 additions detected). |
|
🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅ |
There was a problem hiding this comment.
Pull request overview
This PR addresses Semgrep-reported GraphQL argument injection by removing -f key=value concatenation of node IDs/titles and instead sending a JSON {"query": "...", "variables": {...}} request body to gh api graphql via stdin (--input -). To support this safely (and with consistent UX), it adds a new workflow helper that runs gh with a spinner while piping an io.Reader into stdin.
Changes:
- Add
workflow.RunGHInputContextto executeghcommands with context + spinner while piping stdin from anio.Reader. - Refactor
createProjectandlinkProjectToRepotojson.MarshalGraphQL{query, variables}and pass it via--input -(no raw concatenation of IDs/titles). - Update multiple compiled workflow
.lock.ymlfiles (embedded firewall config/model lists).
Show a summary per file
| File | Description |
|---|---|
| pkg/workflow/github_cli.go | Adds RunGHInputContext helper to run gh with spinner while piping stdin. |
| pkg/workflow/github_cli_wasm.go | Adds wasm stub for RunGHInputContext. |
| pkg/cli/project_command.go | Builds GraphQL request JSON with variables and pipes it via --input - to avoid CLI arg injection. |
| .github/workflows/workflow-health-manager.lock.yml | Compiled workflow lock updated (embedded awf/firewall config content changed). |
| .github/workflows/schema-consistency-checker.lock.yml | Compiled workflow lock updated (embedded awf/firewall config content changed). |
| .github/workflows/safe-output-health.lock.yml | Compiled workflow lock updated (embedded awf/firewall config content changed). |
| .github/workflows/pr-triage-agent.lock.yml | Compiled workflow lock updated (embedded awf/firewall config content changed). |
| .github/workflows/poem-bot.lock.yml | Compiled workflow lock updated (embedded awf/firewall config content changed). |
| .github/workflows/gpclean.lock.yml | Compiled workflow lock updated (embedded awf/firewall config content changed). |
| .github/workflows/daily-team-evolution-insights.lock.yml | Compiled workflow lock updated (embedded awf/firewall config content changed). |
| .github/workflows/daily-safe-output-optimizer.lock.yml | Compiled workflow lock updated (embedded awf/firewall config content changed). |
| .github/workflows/daily-regulatory.lock.yml | Compiled workflow lock updated (embedded awf/firewall config content changed). |
| .github/workflows/daily-observability-report.lock.yml | Compiled workflow lock updated (embedded awf/firewall config content changed). |
| .github/workflows/daily-max-ai-credits-test.lock.yml | Compiled workflow lock updated (embedded awf/firewall config content changed). |
| .github/workflows/daily-credit-limit-test.lock.yml | Compiled workflow lock updated (embedded awf/firewall config content changed). |
| .github/workflows/daily-compiler-quality.lock.yml | Compiled workflow lock updated (embedded awf/firewall config content changed). |
| .github/workflows/daily-cli-tools-tester.lock.yml | Compiled workflow lock updated (embedded awf/firewall config content changed). |
| .github/workflows/daily-cache-strategy-analyzer.lock.yml | Compiled workflow lock updated (embedded awf/firewall config content changed). |
| .github/workflows/copilot-centralization-optimizer.lock.yml | Compiled workflow lock updated (embedded awf/firewall config content changed). |
| .github/workflows/copilot-centralization-drilldown.lock.yml | Compiled workflow lock updated (embedded awf/firewall config content changed). |
| .github/workflows/code-simplifier.lock.yml | Compiled workflow lock updated (embedded awf/firewall config content changed). |
| .github/workflows/ci-coach.lock.yml | Compiled workflow lock updated (embedded awf/firewall config content changed). |
| .github/workflows/agentic-token-optimizer.lock.yml | Compiled workflow lock updated (embedded awf/firewall config content changed). |
| .github/workflows/agent-performance-analyzer.lock.yml | Compiled workflow lock updated (embedded awf/firewall config content changed). |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 83/260 changed files
- Comments generated: 1
- Review effort level: Low
| @@ -916,7 +916,7 @@ jobs: | |||
| export GH_AW_NODE_BIN | |||
| export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" | |||
| (umask 177 && touch /tmp/gh-aw/agent-stdio.log) | |||
| printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.27.22/awf-config.schema.json","network":{"allowDomains":["*.grafana.net","*.sentry.io","api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","crl.geotrust.com","crl.globalsign.com","crl.identrust.com","crl.sectigo.com","crl.thawte.com","crl.usertrust.com","crl.verisign.com","crl3.digicert.com","crl4.digicert.com","crls.ssl.com","github.com","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.com","ocsp.digicert.com","ocsp.geotrust.com","ocsp.globalsign.com","ocsp.identrust.com","ocsp.sectigo.com","ocsp.ssl.com","ocsp.thawte.com","ocsp.usertrust.com","ocsp.verisign.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","ppa.launchpad.net","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","telemetry.enterprise.githubcopilot.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"],"isolation":true,"topologyAttach":["awmg-mcpg","awmg-cli-proxy"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxCacheMisses":5,"maxAiCredits":1500,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"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-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*"],"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":["fable","sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mai-code":["copilot/MAI-Code*","copilot/mai-code*","openai/MAI-Code*"],"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"],"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-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"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.22,squid=sha256:3cdcc1e2b4b4fe602ba69fd3e21aac7ac512d5c1fce24df4ce69dc4f98164b59,agent=sha256:55f06588411008b7148eb64b8dfe28602a0cce3675b36c6b190b54aca138468e,api-proxy=sha256:afb9ff9140b17d38871dfb9dbac5ff8689ea634c2f91c435da2825192d4881c1,cli-proxy=sha256:e23e1604241f579b418e6522d938285b57ada31bc27742a65c90ee2250b1755c"},"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.22/awf-config.schema.json","network":{"allowDomains":["*.grafana.net","*.sentry.io","api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","crl.geotrust.com","crl.globalsign.com","crl.identrust.com","crl.sectigo.com","crl.thawte.com","crl.usertrust.com","crl.verisign.com","crl3.digicert.com","crl4.digicert.com","crls.ssl.com","github.com","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.com","ocsp.digicert.com","ocsp.geotrust.com","ocsp.globalsign.com","ocsp.identrust.com","ocsp.sectigo.com","ocsp.ssl.com","ocsp.thawte.com","ocsp.usertrust.com","ocsp.verisign.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","ppa.launchpad.net","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","telemetry.enterprise.githubcopilot.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"],"isolation":true,"topologyAttach":["awmg-mcpg","awmg-cli-proxy"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxCacheMisses":5,"maxAiCredits":1500,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"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-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*"],"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":["fable","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*"],"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"],"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-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"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.22,squid=sha256:3cdcc1e2b4b4fe602ba69fd3e21aac7ac512d5c1fce24df4ce69dc4f98164b59,agent=sha256:55f06588411008b7148eb64b8dfe28602a0cce3675b36c6b190b54aca138468e,api-proxy=sha256:afb9ff9140b17d38871dfb9dbac5ff8689ea634c2f91c435da2825192d4881c1,cli-proxy=sha256:e23e1604241f579b418e6522d938285b57ada31bc27742a65c90ee2250b1755c"},"logging":{"proxyLogsDir":"/tmp/gh-aw/sandbox/firewall/logs","auditDir":"/tmp/gh-aw/sandbox/firewall/audit"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" | |||
Review Summary 🧠Applied Verdict: REQUEST_CHANGES — one remaining injection surface and missing regression tests. 📋 Issues by Priority🔴 Blocking
🟡 Important
🟢 Minor
@copilot please address the review comments above.
|
There was a problem hiding this comment.
Skills-Based Review 🧠
Applied /diagnosing-bugs, /tdd, and /codebase-design — requesting changes on two correctness/security issues.
📋 Key Themes & Highlights
Key Themes
- Incomplete fix: The prerequisite
repoIdQueryinsidelinkProjectToRepostill passesownerandnameas raw-farguments — the same injection surface the PR was intended to close. This is a blocking issue. - Missing regression tests: Neither
createProject/linkProjectToRepoinproject_command_test.gonorRunGHInputContextingithub_cli_test.gohave tests that would catch a regression to the vulnerable pattern. - Spinner safety:
RunGHInputContextcallsspinner.Stop()manually rather than viadefer, leaving a gap ifcmd.Output()panics. - Pattern fragmentation:
updateSingleSelectFieldOptions(pre-existing) still uses the lower-levelExecGH(...).Stdinpath; this PR is a good opportunity to migrate it toRunGHInputContextfor a single canonical approach.
Positive Highlights
- ✅ Clean fix for the two Semgrep-flagged mutations (
createProject,linkProjectToRepo) —json.Marshal+--input -eliminates the raw concatenation correctly. - ✅
RunGHInputContextfollows the establishedrunGHWithSpinnerContextpattern, making it easy to audit. - ✅ Wasm stub is present and consistent with the rest of the wasm build file.
- ✅ The PR description is clear, includes before/after snippets, and references the existing safe pattern.
🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 112.1 AIC · ⌖ 6.31 AIC · ⊞ 6.6K
Comment /matt to run again
Comments that could not be inline-anchored
pkg/workflow/github_cli.go:185
[/diagnosing-bugs] Spinner is not stopped if cmd.Output() panics — consider using defer spinner.Stop() to ensure the spinner always clears.
<details>
<summary>💡 Suggested change</summary>
spinner := console.NewSpinner(spinnerMessage)
spinner.Start()
defer spinner.Stop()
output, err := cmd.Output()
return output, enrichGHError(err)The existing runGHWithSpinnerContext calls spinner.Stop() manually without defer, so neither is currently panic-safe. Since `RunGHInputCon…
pkg/cli/project_command.go:371
[/diagnosing-bugs] The repoIdQuery call in linkProjectToRepo still passes owner and name as raw -f arguments — the same injection surface Semgrep flagged. The PR fixed the mutation that follows, but not this prerequisite query.
<details>
<summary>💡 Suggested change</summary>
Replace the raw -f calls with a json.Marshal + --input - pattern consistent with the rest of this function:
repoIdQueryBody, err := json.Marshal(map[string]any{
"query": repoIdQuery,
"v…
</details>
<details><summary>pkg/cli/project_command.go:317</summary>
**[/tdd]** No regression test covers `createProject` using the new `--input -` path. If the `json.Marshal` + `RunGHInputContext` wiring regresses, the bug would be silent (no Semgrep alert, no test failure).
<details>
<summary>💡 What to test</summary>
A table-driven test in `project_command_test.go` could mock `RunGHInputContext` (or use an `httptest` server via `GH_HOST`) and assert:
1. The request body sent to stdin is valid JSON with `query` and `variables` keys.
2. `ownerId` and `title` …
</details>
<details><summary>pkg/workflow/github_cli.go:176</summary>
**[/tdd]** `RunGHInputContext` is not covered in `github_cli_test.go` — there are tests for `RunGH` / `RunGHCombined` consistency but none for the new stdin-piping function.
<details>
<summary>💡 Suggested tests</summary>
Add a test that:
1. Calls `RunGHInputContext` with a known `bytes.NewReader` payload and verifies stdin was passed through (e.g. via a helper command that echoes stdin).
2. Verifies the function returns the correct error when `cmd.Output()` fails and stderr is populated (mir…
</details>
<details><summary>pkg/cli/project_command.go:404</summary>
**[/codebase-design]** `updateSingleSelectFieldOptions` uses `workflow.ExecGH(...).Stdin = ...` directly (bypassing the spinner), while the two new call sites use `RunGHInputContext`. The same functional pattern is now expressed three ways in the same file — consider migrating `updateSingleSelectFieldOptions` to `RunGHInputContext` to consolidate the pattern.
<details>
<summary>Context</summary>
- `createProject` → `RunGHInputContext` (new, with spinner + context)
- `linkProjectToRepo` → `Run…
</details>
<details><summary>pkg/workflow/github_cli_wasm.go:47</summary>
**[/codebase-design]** The wasm stub silently returns `nil, errors.New("gh CLI not available in Wasm")` without using the `input io.Reader` parameter at all. Go will not warn about the unused parameter, but it is worth noting in the function comment that the `input` argument is intentionally ignored in this build target.
<details>
<summary>💡 Suggested doc comment addition</summary>
```go
// RunGHInputContext is a no-op stub for Wasm builds. The input reader is not used.
func RunGHInputContex…
</details>There was a problem hiding this comment.
Security Fix Review — GraphQL ID Injection (CWE-89, Semgrep #627/#628)
Overall assessment: Correct and well-targeted fix. The two primary injection sites (createProject, linkProjectToRepo mutation) are properly remediated using the RunGHInputContext + --input - pattern with json.Marshal. The approach matches the existing updateSingleSelectFieldOptions pattern in the same file.
What's good
createProjectandlinkProjectToRepomutations now use a typed JSON body viajson.Marshal+bytes.NewReader, eliminating raw string concatenation of API-returned node IDs (ownerId,title,projectId,repositoryId).RunGHInputContextis a clean, minimal abstraction that mirrorsRunGHContextand correctly handles stdin piping and spinner lifecycle.- The wasm stub correctly returns
errors.New("gh CLI not available in Wasm"), consistent with other stubs. - Mutation queries use typed variables (
$ownerId: ID!,$title: String!, etc.) which are correctly parameterized.
Minor observation (non-blocking)
One inline comment on the repoIdQuery call site (line 371): it still uses -f owner= / -f name= with user-provided values. The query uses typed GraphQL variables so there is no injection risk — but for consistency with the new pattern and to avoid a future Semgrep flag, it could be migrated too.
Out-of-scope remaining -f usages (all safe)
validateOwner/getOwnerNodeId(lines 240, 275): useescapeGraphQLStringfor owner literals inline in query strings — not changed by this PR.createViewREST call (line 514): uses-f name=/-f layout=for a REST POST body (not GraphQL) — gh API handles form encoding correctly.getProjectFields(lines 680, 687): inline query strings with escaped owner login — not changed by this PR.
🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 130.9 AIC · ⌖ 6.17 AIC · ⊞ 4.9K
Comments that could not be inline-anchored
pkg/cli/project_command.go:371
The repoIdQuery call passes user-provided repoOwner and repoName values via -f owner= / -f name= string concatenation. This is technically safe because the query uses typed GraphQL variables ($owner: String!, $name: String!) and gh CLI properly binds them, but it is inconsistent with the new RunGHInputContext + --input - pattern adopted for the mutations below, and will likely trigger a future Semgrep flag on this call site.
For full consistency, convert to the JSON-body ap…
There was a problem hiding this comment.
The security fix is correct: both mutations now properly use typed JSON variables via json.Marshal + --input - instead of raw string concatenation, fully resolving Semgrep #627/#628. The 257 lock-file changes are automated recompile noise (adding "lyria" model alias) and are fine.
Issues found
Non-blocking issues (2 medium)
-
Missing
defer spinner.Stop()(pkg/workflow/github_cli.go:185) — the newRunGHInputContextcallsspinner.Stop()directly aftercmd.Output(). A panic insideOutput()will leave the terminal with an orphaned spinner. Fixing withdeferis trivial. -
Spinner logic duplicated (
pkg/workflow/github_cli.go:176) —RunGHInputContextis a fourth copy of the same TTY/spinner block. This should instead extendrunGHWithSpinnerContextwith an optionalio.Readerstdin parameter so future spinner-behavior changes propagate everywhere automatically.
Neither issue is a correctness or security bug — the core injection fix is sound. Both are straightforward to address before or in a follow-up.
🔎 Code quality review by PR Code Quality Reviewer · 208.9 AIC · ⌖ 5.18 AIC · ⊞ 5.4K
Comment /review to run again
| spinner.Start() | ||
| output, err := cmd.Output() | ||
| err = enrichGHError(err) | ||
| spinner.Stop() |
There was a problem hiding this comment.
spinner.Stop() is not deferred and will be skipped if cmd.Output() panics, leaving the terminal with an orphaned spinner.
💡 Suggested fix
Replace the explicit spinner.Stop() call with a defer:
spinner := console.NewSpinner(spinnerMessage)
spinner.Start()
defer spinner.Stop()
output, err := cmd.Output()
return output, enrichGHError(err)Using defer costs nothing here and protects against terminal corruption if cmd.Output() panics (e.g., OS out of file descriptors). This pattern gap already exists in runGHWithSpinnerContext, RunGHWithHost, and RunGHContextWithHost throughout this file — this new function perpetuates rather than fixes the pattern.
| // Usage: | ||
| // | ||
| // output, err := RunGHInputContext(ctx, "Creating project...", bytes.NewReader(jsonBody), "api", "graphql", "--input", "-") | ||
| func RunGHInputContext(ctx context.Context, spinnerMessage string, input io.Reader, args ...string) ([]byte, error) { |
There was a problem hiding this comment.
RunGHInputContext duplicates the full spinner-management pattern from runGHWithSpinnerContext rather than extending that private helper to accept an optional io.Reader stdin.
💡 Why this matters
The codebase already has four copies of the same spinner management block (runGHWithSpinnerContext, RunGHWithHost, RunGHContextWithHost, and now RunGHInputContext). Every time the spinner behavior needs to change (e.g., always-defer above, context-cancel propagation, non-TTY fallback), all four copies must be updated in sync.
A more maintainable approach would be to extend runGHWithSpinnerContext with an io.Reader parameter (or a functional-options pattern) so that all callers benefit from a single implementation:
func runGHWithSpinnerContext(ctx context.Context, msg string, combined bool, stdin io.Reader, args ...string) ([]byte, error) {
cmd := ExecGHContext(ctx, args...)
if stdin != nil {
cmd.Stdin = stdin
}
// single spinner block
}This is a maintainability concern, not a correctness bug — but the duplication will compound with every future helper added.
There was a problem hiding this comment.
Addressed in f9e0f79 — runGHWithSpinnerContext now accepts an optional io.Reader stdin parameter. RunGHInputContext delegates to it (one line), and RunGHContext/RunGHCombinedContext pass nil. All four spinner-using functions now share a single implementation path.
|
@copilot invoke pr-finisher skill |
|
@copilot please run the
|
…Context; fix repoIdQuery injection; defer spinner.Stop() Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
…clarify error message Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
🤖 PR Triage — Run #7
Score breakdown: impact 40 + urgency 28 + quality 12 Fixes GraphQL ID injection (Semgrep #627/#628, CWE-89) in
|
|
@copilot please run the
|
…w (process all PRs) Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
Done. The only open item was the CI failure in Local validation: |
|
@copilot please run the
|
Done. Current state:
Hand-off: CI must be re-triggered by a maintainer (close/reopen PR or |
🤖 PR Triage — Run §28741077056
Rationale: CWE-89 GraphQL injection fix in critical-path CLI file. Carry-over from prior run — needs human review ASAP.
|
|
@copilot please run the
|
…nce-fix-graphql-id-injection # Conflicts: # pkg/cli/pr_sous_chef_workflow_contract_test.go Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
Done. Merged |
Semgrep alerts #627/#628 flagged
createProjectandlinkProjectToRepofor concatenating API-returned node IDs directly intogh api graphql -f key=valuearguments without sanitization.Changes
pkg/workflow/github_cli.go— addsRunGHInputContext: a new helper mirroringRunGHContextbut accepting anio.Readerfor stdin, enabling safe use ofgh api graphql --input -with spinner support.pkg/workflow/github_cli_wasm.go— adds matching no-op stub for wasm builds.pkg/cli/project_command.go— refactors both mutation call sites to build a typed{"query": ..., "variables": {...}}body viajson.Marshaland pipe it throughRunGHInputContext(..., "--input", "-"), eliminating raw string concatenation forownerId,title,projectId, andrepositoryId.Before (vulnerable pattern):
After:
This matches the approach already used by
updateSingleSelectFieldOptionsin the same file.