Skip to content

Fix GraphQL ID injection in project_command.go (Semgrep #627/#628, CWE-89)#43467

Open
pelikhan with Copilot wants to merge 10 commits into
mainfrom
copilot/uk-ai-resilience-fix-graphql-id-injection
Open

Fix GraphQL ID injection in project_command.go (Semgrep #627/#628, CWE-89)#43467
pelikhan with Copilot wants to merge 10 commits into
mainfrom
copilot/uk-ai-resilience-fix-graphql-id-injection

Conversation

Copilot AI commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Semgrep alerts #627/#628 flagged createProject and linkProjectToRepo for concatenating API-returned node IDs directly into gh api graphql -f key=value arguments without sanitization.

Changes

  • pkg/workflow/github_cli.go — adds RunGHInputContext: a new helper mirroring RunGHContext but accepting an io.Reader for stdin, enabling safe use of gh 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 via json.Marshal and pipe it through RunGHInputContext(..., "--input", "-"), eliminating raw string concatenation for ownerId, title, projectId, and repositoryId.

Before (vulnerable pattern):

workflow.RunGH("Creating project...", "api", "graphql",
    "-f", "query="+mutation,
    "-f", "ownerId="+ownerId,   // raw concatenation
    "-f", "title="+title,
)

After:

requestJSON, _ := json.Marshal(map[string]any{
    "query":     mutation,
    "variables": map[string]any{"ownerId": ownerId, "title": title},
})
workflow.RunGHInputContext(ctx, "Creating project...", bytes.NewReader(requestJSON), "api", "graphql", "--input", "-")

This matches the approach already used by updateSingleSelectFieldOptions in the same file.

Copilot AI and others added 2 commits July 5, 2026 05:32
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
…eProject and linkProjectToRepo

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix GraphQL ID injection in project_command.go Fix GraphQL ID injection in project_command.go (Semgrep #627/#628, CWE-89) Jul 5, 2026
Copilot AI requested a review from pelikhan July 5, 2026 05:38
@pelikhan pelikhan marked this pull request as ready for review July 5, 2026 05:42
Copilot AI review requested due to automatic review settings July 5, 2026 05:42
@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

⚠️ PR Code Quality Reviewer failed during code quality review.

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

No test files were added or modified in this PR. Test Quality Sentinel skipped.

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

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).

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 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.RunGHInputContext to execute gh commands with context + spinner while piping stdin from an io.Reader.
  • Refactor createProject and linkProjectToRepo to json.Marshal GraphQL {query, variables} and pass it via --input - (no raw concatenation of IDs/titles).
  • Update multiple compiled workflow .lock.yml files (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"
@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Review Summary 🧠

Applied /diagnosing-bugs, /tdd, and /codebase-design.

Verdict: REQUEST_CHANGES — one remaining injection surface and missing regression tests.

📋 Issues by Priority

🔴 Blocking

  1. pkg/cli/project_command.go line 368–373linkProjectToRepo still passes repoOwner and repoName as raw -f arguments in the repoIdQuery call. This is the same user-controlled input → CLI argument pattern the PR was fixing. Migrate to RunGHInputContext + --input -.

🟡 Important

  1. pkg/cli/project_command.go line 317 / pkg/workflow/github_cli.go line 176 — No regression tests for createProject, linkProjectToRepo, or RunGHInputContext. Without tests, a future refactor could silently reintroduce the vulnerable -f pattern.

🟢 Minor

  1. pkg/workflow/github_cli.go line 185spinner.Stop() is not deferred; use defer spinner.Stop() for panic-safety.
  2. pkg/cli/project_command.go line 404updateSingleSelectFieldOptions uses the older ExecGH + .Stdin path; consider migrating to RunGHInputContext for a single canonical stdin-piping pattern.
  3. pkg/workflow/github_cli_wasm.go line 47 — Wasm stub lacks a doc comment noting that input is intentionally ignored.

@copilot please address the review comments above.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 112.1 AIC · ⌖ 6.31 AIC · ⊞ 6.6K ·
Comment /matt to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 repoIdQuery inside linkProjectToRepo still passes owner and name as raw -f arguments — the same injection surface the PR was intended to close. This is a blocking issue.
  • Missing regression tests: Neither createProject/linkProjectToRepo in project_command_test.go nor RunGHInputContext in github_cli_test.go have tests that would catch a regression to the vulnerable pattern.
  • Spinner safety: RunGHInputContext calls spinner.Stop() manually rather than via defer, leaving a gap if cmd.Output() panics.
  • Pattern fragmentation: updateSingleSelectFieldOptions (pre-existing) still uses the lower-level ExecGH(...).Stdin path; this PR is a good opportunity to migrate it to RunGHInputContext for a single canonical approach.

Positive Highlights

  • ✅ Clean fix for the two Semgrep-flagged mutations (createProject, linkProjectToRepo) — json.Marshal + --input - eliminates the raw concatenation correctly.
  • RunGHInputContext follows the established runGHWithSpinnerContext pattern, 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{
    &quot;query&quot;: repoIdQuery,
    &quot;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).

&lt;details&gt;
&lt;summary&gt;💡 What to test&lt;/summary&gt;

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.

&lt;details&gt;
&lt;summary&gt;💡 Suggested tests&lt;/summary&gt;

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 fileconsider migrating `updateSingleSelectFieldOptions` to `RunGHInputContext` to consolidate the pattern.

&lt;details&gt;
&lt;summary&gt;Context&lt;/summary&gt;

- `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(&quot;gh CLI not available in Wasm&quot;)` 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.

&lt;details&gt;
&lt;summary&gt;💡 Suggested doc comment addition&lt;/summary&gt;

```go
// RunGHInputContext is a no-op stub for Wasm builds. The input reader is not used.
func RunGHInputContex…

</details>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

  • createProject and linkProjectToRepo mutations now use a typed JSON body via json.Marshal + bytes.NewReader, eliminating raw string concatenation of API-returned node IDs (ownerId, title, projectId, repositoryId).
  • RunGHInputContext is a clean, minimal abstraction that mirrors RunGHContext and 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): use escapeGraphQLString for owner literals inline in query strings — not changed by this PR.
  • createView REST 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…

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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)

  1. Missing defer spinner.Stop() (pkg/workflow/github_cli.go:185) — the new RunGHInputContext calls spinner.Stop() directly after cmd.Output(). A panic inside Output() will leave the terminal with an orphaned spinner. Fixing with defer is trivial.

  2. Spinner logic duplicated (pkg/workflow/github_cli.go:176) — RunGHInputContext is a fourth copy of the same TTY/spinner block. This should instead extend runGHWithSpinnerContext with an optional io.Reader stdin 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

Comment thread pkg/workflow/github_cli.go Outdated
spinner.Start()
output, err := cmd.Output()
err = enrichGHError(err)
spinner.Stop()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in f9e0f79runGHWithSpinnerContext (the shared helper) now uses defer spinner.Stop(), so RunGHInputContext inherits the safe pattern by delegation. Also updated RunGHWithHost and RunGHContextWithHost to use defer in 1984fb3.

// 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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in f9e0f79runGHWithSpinnerContext 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.

@pelikhan

pelikhan commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

@copilot invoke pr-finisher skill

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please run the pr-finisher skill and address the unresolved review feedback, including the remaining linkProjectToRepo input-handling and missing regression-test follow-ups, then rerun checks if you make changes.

Generated by 👨‍🍳 PR Sous Chef · 12.3 AIC · ⌖ 5.1 AIC · ⊞ 3.5K ·
Comment /souschef to run again

Copilot AI and others added 2 commits July 5, 2026 07:09
…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>
pelikhan and others added 3 commits July 5, 2026 00:15
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
…phql-id-injection' into copilot/uk-ai-resilience-fix-graphql-id-injection

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

Copilot AI commented Jul 5, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please run the pr-finisher skill and address the unresolved review feedback, including the remaining linkProjectToRepo input-handling and missing regression-test follow-ups, then rerun ch...

Done in 9232854:

  • linkProjectToRepo repoIdQuery was already converted to RunGHInputContext + --input - in f9e0f79.
  • Added TestRunGHInputContext to github_cli_test.go — verifies stdin wiring and consistent delegation with RunGHContext.
  • Added TestGraphQLRequestBodyStructure to project_command_test.go — covers all three call sites (createProject, linkProjectToRepo mutation, repoIdQuery) and validates that json.Marshal + variables structure survives special-character round-trips (regression guard against -f concat).

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

🤖 PR Triage — Run #7

Field Value
Category bug (security)
Risk 🔴 high
Score 80 / 100
Action fast_track
Batch pr-batch:security-fixes

Score breakdown: impact 40 + urgency 28 + quality 12

Fixes GraphQL ID injection (Semgrep #627/#628, CWE-89) in project_command.go. Moves both mutation call sites to json.Marshal + RunGHInputContext piping via --input -, eliminating raw string concatenation of API-returned node IDs. 3 files, +69/-26. Security fix — expedite human review alongside #43468.

Generated by 🔧 PR Triage Agent · 56.3 AIC · ⌖ 8.84 AIC · ⊞ 5.5K ·

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[uk-ai-resilience] [security] Fix GraphQL ID injection in project_command.go (Semgrep #627/#628, CWE-89)

4 participants