Skip to content

Use parameterized GraphQL variables instead of Sprintf interpolation - #51827

Merged
pelikhan merged 4 commits into
mainfrom
copilot/uk-ai-resilience-risk-review
Aug 10, 2026
Merged

Use parameterized GraphQL variables instead of Sprintf interpolation#51827
pelikhan merged 4 commits into
mainfrom
copilot/uk-ai-resilience-risk-review

Conversation

Copilot AI commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

CodeQL (workflow-go-graphql-injection-sprintf, alerts #651/#652) flagged GraphQL queries built by string interpolation. The last such call site was the closing-issues query in pkg/cli/outcome_eval.go, which relied on a hand-rolled escapeGraphQLString helper defined in pkg/cli/project_command.go.

Changes

  • ghAPIGraphQL accepts variables — new variables map[string]any parameter, forwarded to gh api graphql. Strings use -f (raw field) so gh's @file / {placeholder} expansion can't be triggered by interpolated values; other types use -F for correct GraphQL typing. Args are emitted in sorted key order for determinism.
  • loadPullRequestIntentData query is now static, declaring $owner, $name, $number.
  • Removed escapeGraphQLString and its test — manual escaping is no longer needed anywhere. All other GraphQL calls in project_command.go were already variable-based.
  • Tests — stubs updated for the new signature; added assertions that the query text carries no interpolated values and that owner/name/number arrive as variables.

Before:

query := fmt.Sprintf(`query {
    repository(owner: "%s", name: "%s") {
        pullRequest(number: %d) { ... }
    }
}`, escapeGraphQLString(owner), escapeGraphQLString(name), prNumber)

result, err := objectiveMappingGHAPIGraphQL(ctx, query, repo)

After:

query := `query($owner: String!, $name: String!, $number: Int!) {
    repository(owner: $owner, name: $name) {
        pullRequest(number: $number) { ... }
    }
}`
variables := map[string]any{"owner": owner, "name": name, "number": prNumber}

result, err := objectiveMappingGHAPIGraphQL(ctx, query, variables, repo)

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix GraphQL injection via Sprintf in project_command.go Use parameterized GraphQL variables instead of Sprintf interpolation Aug 10, 2026
Copilot AI requested a review from pelikhan August 10, 2026 16:12
@pelikhan
pelikhan marked this pull request as ready for review August 10, 2026 16:20
Copilot AI balanced review requested due to automatic review settings August 10, 2026 16:20

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

Replaces interpolated GraphQL queries with parameterized variables to address CodeQL injection findings.

Changes:

  • Adds deterministic, typed GraphQL variable forwarding.
  • Converts pull-request intent lookup to a static query.
  • Removes obsolete escaping code and updates tests.
Show a summary per file
File Description
pkg/cli/project_command.go Removes manual GraphQL escaping.
pkg/cli/project_command_test.go Removes obsolete escaping tests.
pkg/cli/outcome_eval.go Adds variable-based GraphQL execution.
pkg/cli/outcome_eval_test.go Updates stubs and parameterization assertions.

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread pkg/cli/outcome_eval.go
Comment on lines +317 to +321
for _, name := range slices.Sorted(maps.Keys(variables)) {
switch value := variables[name].(type) {
case string:
// -f sends the value literally, avoiding gh's @file / {placeholder} expansion.
args = append(args, "-f", name+"="+value)
assert.Equal(t, "mapped", report.AttributionStatus)
assert.Equal(t, "closing_issue", report.AttributionSource)

assert.NotContains(t, capturedQuery, "owner/repo", "query should not interpolate values into the GraphQL document")

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.

Updated the assertions to check each quoted literal individually ("owner", "repo", and the raw 77) rather than just the concatenated owner/repo string.

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR #51827 does not have the 'implementation' label and has only 33 new lines of code in business logic directories (threshold: 100).

🏗️ ADR gate enforced by Design Decision Gate 🏗️

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Ponytail Reviewer completed successfully!

Generated by Ponytail Reviewer for #51827

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

🧪 Test quality analysis by Test Quality Sentinel

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

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

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • api.individual.githubcopilot.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "api.individual.githubcopilot.com"

See Network Configuration for more information.

🔎 Code quality review by PR Code Quality Reviewer

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

Good security improvement. Replacing fmt.Sprintf interpolation with parameterized GraphQL variables eliminates injection risk and the escapeGraphQLString workaround. The two existing Copilot inline comments (#3751352069, #3751352151) already capture the actionable gaps (CLI encoding not tested by unit tests; weak NotContains assertion). No additional blocking issues found.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 16.9 AIC · ⌖ 7.06 AIC · ⊞ 5.4K

@github-actions github-actions Bot mentioned this pull request Aug 10, 2026

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

Generated by ✂️ Ponytail Reviewer for #51827 · auto · 23.4 AIC · ⌖ 4.27 AIC · ⊞ 6.8K
Comment /ponytail to run again

Comment thread pkg/cli/outcome_eval.go
func ghAPIGraphQL(ctx context.Context, query string, variables map[string]any, repo string) (map[string]any, error) {
ownerRepo, host := repoutil.NormalizeRepoForAPI(repo)
args := []string{"api", "graphql", "-f", "query=" + query}
for _, name := range slices.Sorted(maps.Keys(variables)) {

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.

L317: yagni: sorts variable map keys for deterministic arg order, but the only caller passes 3 fixed keys and no test checks arg order. Iterate variables directly, drop the maps/slices imports.

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.

Kept the sorted iteration — it's now exercised directly by TestBuildGraphQLArgs's "deterministic key order" subtest, so it's no longer untested dead weight.

@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 and /tdd — the security fix is correct and well-scoped, but two pre-existing review comments and one new finding flag test coverage gaps worth addressing.

📋 Key Themes & Highlights

Key Themes

  • Untested encoding path: the -f/-F arg-building loop in ghAPIGraphQL (line 321) is bypassed by the test stub, so the security-critical encoding is never actually exercised
  • Weak injection assertion: NotContains(capturedQuery, "owner/repo") (line 310) would also pass on the old interpolated query, so it doesn’t prove injection safety
  • Unguarded %v formatting: non-string variable types use fmt.Sprintf("%v", value) without documenting or enforcing supported types (line 328)

Positive Highlights

  • ✅ Clean removal of hand-rolled escapeGraphQLString — eliminating the unsafe helper is exactly right
  • ✅ Sorted key order for deterministic CLI arg emission
  • ✅ Correct -f (raw) / -F (typed) split for gh api graphql
  • assert.Equal on capturedVariables gives a strong check that values arrive correctly at the call site

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 32.8 AIC · ⌖ 7.53 AIC · ⊞ 7.1K
Comment /matt to run again

Comment thread pkg/cli/outcome_eval.go
}
var output []byte
var err error
if host != "" {

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.

[/diagnosing-bugs] fmt.Sprintf("%v", value) for non-string variables relies on Go's default formatting, but gh api graphql -F expects specific value forms. For current int usage this is fine, but future callers passing float64, bool, or custom types could silently emit wrong values.

💡 Suggestion

Document supported types explicitly, or guard against unexpected ones:

default:
    return nil, fmt.Errorf("ghAPIGraphQL: unsupported variable type %T for key %q", variables[name], name)

This makes the contract explicit and surfaces bugs early.

@copilot please address this.

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.

Added the explicit type guard as suggested — unsupported variable types now return an error naming the key and %T instead of silently formatting them. Covered by a new test case in TestBuildGraphQLArgs.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100 — Excellent

Analyzed 1 modified test (Go: 1, JS: 0), 1 design test, 0 violations. (TestEscapeGraphQLString deleted — no longer needed.)

📊 Metrics (1 modified test)
Metric Value
Analyzed 1 (Go: 1, JS: 0)
✅ Design 1 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 1 (100%)
Duplicate clusters 0
Inflation No (11 test lines / 22 prod lines = 0.5:1)
🚨 Violations 0
Test File Classification Issues
TestEnrichOutcomeWithObjectiveValue_TracesPullRequestToRootIssue (modified) outcome_eval_test.go behavioral_contract · design_test · high_value None

Verdict

Passed. 0% implementation tests (threshold: 30%). The modified test adds three security-contract assertions directly verifying the injection-prevention design invariant: (1) no owner/repo interpolated into the query document, (2) query declares typed GraphQL variables, (3) values arrive as the variables map. The deleted TestEscapeGraphQLString is a clean removal matching the deleted helper. No build-tag violations, no mock-library violations.

🧪 Test quality analysis by Test Quality Sentinel · sonnet46 · 52.3 AIC · ⌖ 7.7 AIC · ⊞ 7.6K ·
Comment /review 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.

✅ Test Quality Sentinel: 100/100. 0% implementation tests (threshold: 30%).

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

Copilot AI and others added 2 commits August 10, 2026 19:16
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
@pelikhan
pelikhan merged commit 359e337 into main Aug 10, 2026
@pelikhan
pelikhan deleted the copilot/uk-ai-resilience-risk-review branch August 10, 2026 20:52
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.86.2

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] [risk-review] Tier B: GraphQL injection via Sprintf in pkg/cli/project_command.go (CodeQL #651, #652)

3 participants