Skip to content

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

Description

@github-actions

Summary

Semgrep alerts #628 and #627 (created 2026-06-16, open 20 days) flag unescaped fmt.Sprintf injections into GraphQL mutation strings in pkg/cli/project_command.go.

File: pkg/cli/project_command.go
Rule: workflow-graphql-id-unescaped (CWE-89 — GraphQL injection)
Severity: Warning (Semgrep)

Affected locations

Alert Function Line Injected value
#628 linkProjectToRepo() 368 projectId and repositoryId from prior API responses
#627 createProject() 295 ownerId from getOwnerNodeId() — inconsistently unescaped vs title (which uses escapeGraphQLString)

Tier & Risk Scoring

Dimension Score Notes
Exposure amplification 2 Affects CLI project management commands; requires authenticated caller
Patchability 1 Trivial — wrap with escapeGraphQLString() or use parameterized variables
Detectability 3 Would surface as GraphQL parse error or unexpected behavior
Operational fragility 2 Scoped to project CLI commands
Ownership confidence 3 No assignee for 20 days
Aggregate 11 Tier B — Open With Conditions

SLA: High — fix within 7 days.

Root Cause

In createProject() (line 295), ownerId is injected raw into the mutation string via fmt.Sprintf, while the title argument on the same call uses escapeGraphQLString(title). This is an inconsistency that creates a defense-in-depth gap: if the GitHub API ever returns a node ID with special characters (e.g., in a future API change), it could cause a GraphQL injection.

In linkProjectToRepo() (line 368), both projectId and repositoryId are injected without escaping.

While GitHub API node IDs are currently opaque base64-like strings with low direct attacker control, the inconsistent escaping violates the principle of defense-in-depth.

Recommended Fix

Option A — Apply escapeGraphQLString() to all node ID arguments:

// createProject() — line 295
mutation = fmt.Sprintf(`...`, escapeGraphQLString(ownerId), escapeGraphQLString(title))

// linkProjectToRepo() — line 368
mutation = fmt.Sprintf(`...`, escapeGraphQLString(projectId), escapeGraphQLString(repositoryId))

Option B (preferred) — Use parameterized GraphQL variables via -f flags:

args := []string{"api", "graphql", "-f", fmt.Sprintf("ownerId=%s", ownerId), "-f", fmt.Sprintf("title=%s", title), "--raw-field", "query=" + staticMutation}

This eliminates string interpolation entirely.

Governance Context

Identified by the UK AI Open Code Risk & Resilience Governance weekly scan (2026-07-06). See discussion report for full tier classification and remediation queue.

References: CodeQL alert #628 · Semgrep alert #627 · §28806888795

Generated by UK AI Operational Resilience · 119.2 AIC · ⌖ 6.39 AIC · ⊞ 5.2K ·

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions