Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pkg/workflow/compiler_validators.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,12 @@ func (c *Compiler) validateToolConfiguration(workflowData *WorkflowData, markdow
log.Printf("Validating push-to-pull-request-branch configuration")
c.validatePushToPullRequestBranchWarnings(workflowData.SafeOutputs, workflowData.CheckoutConfigs)

// Reject bare "*" in allowed-labels (CTR-015)
log.Printf("Validating safe-outputs allowed-labels glob scope")
if err := c.validateSafeOutputsAllowedLabelsGlobScope(workflowData.SafeOutputs); err != nil {
return formatCompilerError(markdownPath, "error", err.Error(), err)
}

// Validate network allowed domains configuration
log.Printf("Validating network allowed domains")
if err := c.validateNetworkAllowedDomains(workflowData.NetworkPermissions); err != nil {
Expand Down
63 changes: 63 additions & 0 deletions pkg/workflow/safe_outputs_allowed_labels_validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package workflow

import (
"fmt"
"strings"
)

var safeOutputsAllowedLabelsValidationLog = newValidationLogger("safe_outputs_allowed_labels")

// validateSafeOutputsAllowedLabelsGlobScope returns an error when any safe-outputs
// allowed-labels field contains a bare "*" glob pattern (CTR-015).
//
// A bare "*" in allowed-labels is semantically equivalent to omitting the field
// entirely: all labels are permitted and the restriction is ineffective. This is
// almost always accidental. Authors should use specific names or narrower patterns
// such as "team-*" or "priority-*" instead.
func (c *Compiler) validateSafeOutputsAllowedLabelsGlobScope(config *SafeOutputsConfig) error {
if config == nil {
return nil
}

type labelledConfig struct {
name string
labels []string
}

var configs []labelledConfig

if config.CreateIssues != nil && len(config.CreateIssues.AllowedLabels) > 0 {
configs = append(configs, labelledConfig{"safe-outputs.create-issue.allowed-labels", config.CreateIssues.AllowedLabels})
}
if config.CreateDiscussions != nil && len(config.CreateDiscussions.AllowedLabels) > 0 {
configs = append(configs, labelledConfig{"safe-outputs.create-discussion.allowed-labels", config.CreateDiscussions.AllowedLabels})
}
if config.UpdateDiscussions != nil && len(config.UpdateDiscussions.AllowedLabels) > 0 {
configs = append(configs, labelledConfig{"safe-outputs.update-discussion.allowed-labels", config.UpdateDiscussions.AllowedLabels})
}
if config.CreatePullRequests != nil && len(config.CreatePullRequests.AllowedLabels) > 0 {
configs = append(configs, labelledConfig{"safe-outputs.create-pull-request.allowed-labels", config.CreatePullRequests.AllowedLabels})
}
if config.MergePullRequest != nil && len(config.MergePullRequest.AllowedLabels) > 0 {
configs = append(configs, labelledConfig{"safe-outputs.merge-pull-request.allowed-labels", config.MergePullRequest.AllowedLabels})
}

for _, lc := range configs {
for _, pattern := range lc.labels {
if strings.TrimSpace(pattern) == "*" {
msg := fmt.Sprintf(
"CTR-015: %s contains a bare \"*\" wildcard that matches any label, "+
"effectively disabling the label restriction.\n\n"+
"Using \"*\" in allowed-labels has the same effect as omitting the field entirely "+
"and may allow the agent to apply labels that trigger unintended automation.\n"+
"Replace with specific label names or narrower patterns (e.g., \"team-*\", \"priority-*\") "+
"to restrict which labels the agent is allowed to apply.",
lc.name,
)
safeOutputsAllowedLabelsValidationLog.Printf("Error: %s", msg)
return fmt.Errorf("%s", msg)
}
}
}
return nil
}
126 changes: 126 additions & 0 deletions pkg/workflow/safe_outputs_allowed_labels_validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
//go:build !integration

package workflow

import (
"os"
"path/filepath"
"testing"

"github.com/github/gh-aw/pkg/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestCTR015AllowedLabelsGlobScope tests that the compiler rejects (CTR-015) when
// a bare "*" wildcard appears in any safe-outputs allowed-labels field.
func TestCTR015AllowedLabelsGlobScope(t *testing.T) {
basePermissions := `
permissions:
contents: read
issues: read

on:
issues:
types: [opened]

engine: claude
strict: false
`

tests := []struct {
name string
safeOutputs string
expectError bool
}{
{
name: "create-issue with bare * in allowed-labels triggers error",
safeOutputs: `safe-outputs:
create-issue:
allowed-labels: ["*"]
`,
expectError: true,
},
{
name: "create-discussion with bare * triggers error",
safeOutputs: `safe-outputs:
create-discussion:
allowed-labels: ["*"]
`,
expectError: true,
},
{
name: "create-pull-request with bare * triggers error",
safeOutputs: `safe-outputs:
create-pull-request:
allowed-labels: ["*"]
`,
expectError: true,
},
{
name: "merge-pull-request with bare * triggers error",
safeOutputs: `safe-outputs:
merge-pull-request:
allowed-labels: ["*"]
`,
expectError: true,
},
{
name: "update-discussion with bare * triggers error",
safeOutputs: `safe-outputs:
update-discussion:
allowed-labels: ["*"]
`,
expectError: true,
},
{
name: "specific label names do not trigger error",
safeOutputs: `safe-outputs:
create-issue:
allowed-labels: ["bug", "enhancement"]
`,
expectError: false,
},
{
name: "prefix glob pattern does not trigger error",
safeOutputs: `safe-outputs:
create-issue:
allowed-labels: ["team-*", "priority-*"]
`,
expectError: false,
},
{
name: "mixed specific and bare * triggers error",
safeOutputs: `safe-outputs:
create-issue:
allowed-labels: ["bug", "*", "enhancement"]
`,
expectError: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpDir := testutil.TempDir(t, "ctr015-test")

content := "---\n" + basePermissions + tt.safeOutputs + "---\n\n# Test Workflow\n\nTest body.\n"
wfPath := filepath.Join(tmpDir, "test.md")
err := os.WriteFile(wfPath, []byte(content), 0o600)
require.NoError(t, err, "Should write test workflow file")

compiler := NewCompiler()
compileErr := compiler.CompileWorkflow(wfPath)

if tt.expectError {
assert.Error(t, compileErr,
"CTR-015: expected error for bare \"*\" in allowed-labels")
assert.Contains(t, compileErr.Error(), "CTR-015",
"CTR-015: error message should reference the rule ID")
} else {
assert.NoError(t, compileErr,
"CTR-015: did not expect error for valid allowed-labels")
}
})
}
}

11 changes: 10 additions & 1 deletion specs/compiler-threat-detection-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ sidebar:

# GitHub Actions Compiler Threat Detection Specification

**Version**: 1.0.4
**Version**: 1.0.5
**Status**: Candidate Recommendation
**Latest Version**: https://github.com/github/gh-aw/blob/main/specs/compiler-threat-detection-spec.md
**Editors**: GitHub Next (GitHub, Inc.)
Expand Down Expand Up @@ -122,6 +122,7 @@ A conforming implementation MUST include detection coverage for at least the fol
- **CTR-012 Safe-Outputs Wildcard Push Scope**: Detect misconfiguration patterns when `safe-outputs.push-to-pull-request-branch: target: "*"` is used; warn when no wildcard fetch pattern is present in checkout (suppressed for public repos) and when no access constraints (`title-prefix` or `labels`) are configured.
- **CTR-013 Argument Injection via Package/Image Names**: Detect package or container image names that start with `-` (hyphen) in npm/npx, pip/uv, and Docker frontmatter configurations; reject these names before they are passed to `exec.Command` calls where they would be interpreted as CLI flags, enabling argument injection.
- **CTR-014 Supply Chain Attack via Install Scripts**: Detect when `run-install-scripts: true` is configured in workflow frontmatter (globally or per-runtime); warn in non-strict mode and reject in strict mode to protect against malicious npm pre/post install hooks that can exfiltrate secrets or corrupt the runner environment.
- **CTR-015 Allowed Label Glob Scope**: Detect bare `*` wildcard patterns in `safe-outputs.*.allowed-labels` fields (`create-issue`, `create-discussion`, `update-discussion`, `create-pull-request`, `merge-pull-request`); reject compilation when such a pattern is present because it renders the label restriction ineffective and may allow the agent to apply labels that trigger unintended label-driven automation in the repository.

### 4.2 Compiler Response Requirements

Expand Down Expand Up @@ -209,6 +210,7 @@ Implementations MUST maintain a clear mapping from each active `CTR-*` rule to c
| CTR-012 Safe-Outputs Wildcard Push Scope | `pkg/workflow/push_to_pull_request_branch_validation.go` | `pkg/workflow/push_to_pull_request_branch_test.go`, `pkg/workflow/push_to_pull_request_branch_warning_test.go` |
| CTR-013 Argument Injection via Package/Image Names | `pkg/workflow/name_validation.go` (shared helper `rejectHyphenPrefixPackages`), `pkg/workflow/npm_validation.go`, `pkg/workflow/pip_validation.go`, `pkg/workflow/docker_validation.go` | `pkg/workflow/argument_injection_test.go` |
| CTR-014 Supply Chain Attack via Install Scripts | `pkg/workflow/run_install_scripts_validation.go` (`validateRunInstallScripts`, `resolveRunInstallScripts`) | `pkg/workflow/run_install_scripts_validation_test.go` |
| CTR-015 Allowed Label Glob Scope | `pkg/workflow/safe_outputs_allowed_labels_validation.go` (`validateSafeOutputsAllowedLabelsGlobScope`) | `pkg/workflow/safe_outputs_allowed_labels_validation_test.go` |

The mappings above are pattern-based references and MUST be validated against concrete file paths whenever this specification is updated.

Expand Down Expand Up @@ -247,6 +249,7 @@ The following test IDs map one-to-one to the CTR rules in Section 4.1. Each test
| **T-CTR-012** | CTR-012 Safe-Outputs Wildcard Push Scope | Workflow uses `safe-outputs.push-to-pull-request-branch: target: "*"` without a wildcard fetch pattern in checkout (for non-public repos) or without `title-prefix` or `labels` access constraints | Compilation warning identifying the unconstrained wildcard scope and the missing checkout fetch pattern or access constraint; suppressed for public repositories | `CTR-012` |
| **T-CTR-013** | CTR-013 Argument Injection via Package/Image Names | A workflow frontmatter declares an npm/npx package, a pip/uv package, or a Docker container image name that starts with `-` (e.g., `--privileged`, `-exploit`) | Compilation failure with error identifying the invalid name, the affected tool kind, and instructing the user to fix the package or image name | `CTR-013` |
| **T-CTR-014** | CTR-014 Supply Chain Attack via Install Scripts | A workflow frontmatter sets `run-install-scripts: true` (globally or under `runtimes.node`) | Compilation warning in non-strict mode identifying the supply chain risk and advising removal of `run-install-scripts: true`; compilation failure in strict mode | `CTR-014` |
| **T-CTR-015** | CTR-015 Allowed Label Glob Scope | A workflow frontmatter sets `safe-outputs.*.allowed-labels` to `["*"]` (bare wildcard) for any safe-output type that supports the field (`create-issue`, `create-discussion`, `update-discussion`, `create-pull-request`, `merge-pull-request`) | Compilation failure with error identifying the field name, explaining that `"*"` disables label restrictions and may permit unintended label-driven automation, and recommending specific names or narrower patterns | `CTR-015` |

### 7.2 Test Coverage Requirements

Expand All @@ -268,6 +271,12 @@ The following test IDs map one-to-one to the CTR rules in Section 4.1. Each test

## 9. Change Log

### 1.0.5 (2026-05-14)

- Added CTR-015 Allowed Label Glob Scope (compilation error when `safe-outputs.*.allowed-labels` contains a bare `"*"` wildcard that effectively disables label restrictions and may permit unintended label-driven automation; triggered by the new glob pattern support for `allowed-labels` introduced in gh-aw #32027)
- Added T-CTR-015 test ID entry in Section 7.1
- Extended Section 6.1 baseline rule mapping table with CTR-015 implementation references (`safe_outputs_allowed_labels_validation.go`)

### 1.0.4 (2026-05-13)

- Added CTR-014 Supply Chain Attack via Install Scripts (warn/reject when `run-install-scripts: true` is configured; protects against malicious npm pre/post install hooks)
Expand Down