Add blocked pattern matching for add-labels and remove-labels#16690
Add blocked pattern matching for add-labels and remove-labels#16690
Conversation
- Update Go types: AddLabelsConfig, RemoveLabelsConfig, ListJobConfig - Update JSON schema: add blocked field validation - Update handler config builders to include blocked patterns - Add Go tests for blocked pattern parsing and compilation - JavaScript validation and handler updates coming next Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
- Add comprehensive tests for validateLabels with blocked patterns - Test security precedence (blocked before allowed) - Test exact match, prefix patterns, and bot patterns - All tests passing Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds denylist-style pattern matching (blocked) for add-labels and remove-labels safe outputs to prevent unsafe label application/removal (e.g., workflow trigger labels or bot/admin labels), strengthening defense-in-depth against prompt injection.
Changes:
- Extend workflow config types and JSON schema to accept
blocked: []stringfor add/remove label safe outputs. - Pass
blockedthrough handler config into the JavaScript label handlers. - Update
validateLabels()to filter blocked patterns (viamatchesSimpleGlob) before applying allowlists, with accompanying tests.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/workflow/safe_output_builder.go | Adds Blocked to shared list-job config and emits a _BLOCKED env var. |
| pkg/workflow/add_labels.go | Adds Blocked to AddLabelsConfig and threads it into list-job config/logging. |
| pkg/workflow/remove_labels.go | Adds Blocked to RemoveLabelsConfig and logs blocked count. |
| pkg/workflow/compiler_safe_outputs_config.go | Serializes blocked into JS handler config for add/remove labels. |
| pkg/parser/schemas/main_workflow_schema.json | Adds blocked array to add/remove label safe-output schema. |
| actions/setup/js/safe_output_validator.cjs | Implements blocked-pattern filtering inside validateLabels(). |
| actions/setup/js/add_labels.cjs | Reads config.blocked and passes it into validateLabels(). |
| actions/setup/js/remove_labels.cjs | Reads config.blocked and passes it into validateLabels(). |
| actions/setup/js/safe_output_validator.test.cjs | Adds unit tests for blocked pattern behavior and precedence. |
| pkg/workflow/compile_outputs_label_test.go | Adds compile/parse tests ensuring blocked is parsed and emitted into handler config. |
Comments suppressed due to low confidence (1)
pkg/parser/schemas/main_workflow_schema.json:5242
- The schema sets
blocked.minItemsto 1, which rejectsblocked: []. The PR description and runtime validator both treat an empty blocked list as “no restrictions”, so this is inconsistent. Either allow empty arrays here (minItems: 0) or update the documented behavior to require omittingblockedentirely when you want no blocking.
"blocked": {
"type": "array",
"description": "Optional list of blocked label patterns (supports glob patterns like '~*', '*[bot]'). Labels matching these patterns will be rejected. Applied before allowed list filtering for security.",
"items": {
"type": "string"
},
"minItems": 1,
"maxItems": 50
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "blocked": { | ||
| "type": "array", | ||
| "description": "Optional list of blocked label patterns (supports glob patterns like '~*', '*[bot]'). Labels matching these patterns will be rejected. Applied before allowed list filtering for security.", | ||
| "items": { | ||
| "type": "string" | ||
| }, | ||
| "minItems": 1, | ||
| "maxItems": 50 |
There was a problem hiding this comment.
Same issue as add-labels: blocked.minItems is 1, so blocked: [] is schema-invalid even though the PR description/runtime treat empty as “no restrictions”. Consider minItems: 0 (or update the documented behavior to require omitting the field).
This issue also appears on line 5235 of the same file.
| strict: false | ||
| safe-outputs: | ||
| add-labels: | ||
| blocked: ["~*", "*\\**"] |
There was a problem hiding this comment.
The test config uses the blocked pattern "*\\**", which is not explained elsewhere (schema/docs/examples mention patterns like "~*" and "*[bot]"). This makes the test harder to understand and may imply an escape syntax that matchesSimpleGlob doesn’t actually support for literal *. Consider switching this test data to a documented pattern (e.g., "*[bot]") or explicitly documenting/implementing the intended escaping behavior.
| blocked: ["~*", "*\\**"] | |
| blocked: ["~*", "*[bot]"] |
| validLabels = labels.filter(label => { | ||
| const labelStr = String(label).trim(); | ||
| const isBlocked = blockedPatterns.some(pattern => matchesSimpleGlob(labelStr, pattern)); | ||
| if (isBlocked) { | ||
| blockedLabels.push(labelStr); | ||
| } | ||
| return !isBlocked; | ||
| }); |
There was a problem hiding this comment.
Blocked-pattern matching is currently performed on labelStr = String(label).trim() before sanitizeLabelContent/Unicode hardening. Since sanitizeLabelContent removes zero-width chars and normalizes text, a label like ~\u200bstale could evade the ~* blocked pattern check and then be normalized to ~stale later, defeating the security boundary. Consider normalizing/sanitizing the label string (at least hardenUnicodeText, or reuse sanitizeLabelContent) before evaluating blocked patterns (and ideally use the same normalized value for subsequent allowlist checks).
| return !isBlocked; | ||
| }); | ||
| if (blockedLabels.length > 0) { | ||
| core.info(`Filtered out ${blockedLabels.length} blocked labels: ${blockedLabels.join(", ")}`); |
There was a problem hiding this comment.
validateLabels logs blocked labels by joining raw, untrusted label strings. Since labels come from agent output, this can inject newlines/control chars into logs and make traces hard to read. Consider logging a sanitized/escaped representation instead (e.g., JSON.stringify(blockedLabels) and/or sanitizing each label before logging).
| core.info(`Filtered out ${blockedLabels.length} blocked labels: ${blockedLabels.join(", ")}`); | |
| core.info(`Filtered out ${blockedLabels.length} blocked labels: ${JSON.stringify(blockedLabels)}`); |
Problem
Large repositories with 600+ labels cannot maintain exhaustive allowlists. Certain label patterns should never be applied by agentic workflows (e.g.,
~staletriggers workflows,*[bot]are administrative), but prompt-level instructions are vulnerable to injection attacks.Changes
Schema & Types
blocked: []stringfield toAddLabelsConfigandRemoveLabelsConfigmain_workflow_schema.jsonRuntime Validation
validateLabels()to filter blocked patterns usingmatchesSimpleGlobadd_labels.cjsandremove_labels.cjsto pass blocked configExample
Blocked labels matching
~*or*[bot]are rejected regardless of allowed list, providing infrastructure-level enforcement against prompt injection.Pattern Support
~*blocks labels starting with~*[bot]blocks labels ending with[bot]staleblocks only that labelBackward compatible: undefined/empty blocked list means no restrictions.
Warning
Firewall rules blocked me from connecting to one or more addresses (expand for details)
I tried to connect to the following addresses, but was blocked by firewall rules:
https://api.github.com/graphql/usr/bin/gh /usr/bin/gh api graphql -f query=query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled } } -f owner=github -f name=gh-aw 3383282/b223/vet^From [0-9a-f]\{40\} ache/go/1.25.0/x/tmp/gh-aw/aw.patch /tmp/go-build2309303883/b412/testutil.test -tes�� te 'scripts/**/*.js' --ignore-path .prettierignore -test.v=true e_modules/.bin/node -test.timeout=10git -test.run=^Test -test.short=true--show-toplevel git(http block)/usr/bin/gh /usr/bin/gh api graphql -f query=query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled } } -f owner=github -f name=gh-aw GOWORK 64/bin/go ache/node/24.13.0/x64/bin/node 2408�� /tmp/gh-aw-test-runs/20260219-005552-34564/test-1137714267 rev-parse /usr/bin/git b136a0101c461533git GO111MODULE 64/bin/go git(http block)https://api.github.com/repos/actions/checkout/git/ref/tags/v3/usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v3 --jq .object.sha js/**/*.json' --ignore-path ../../../.prettierignore x_amd64/vet .cfg(http block)/usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v3 --jq .object.sha -json GO111MODULE 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet env 3313979602 GO111MODULE ache/go/1.25.0/x64/bin/go on; \ echo "���git GOMOD GOMODCACHE go(http block)https://api.github.com/repos/actions/checkout/git/ref/tags/v4/usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v4 --jq .object.sha ./../pkg/workflow/js/**/*.json' --ignore-path typecheck .cfg l .cfg abelRemoveBlocke--show-toplevel /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet -ato�� -bool -buildtags 0/x64/bin/node -errorsas -ifaceassert -nilfunc 0/x64/bin/node(http block)/usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v4 --jq .object.sha ./../pkg/workflow/js/**/*.json' --ignore-path ../../../.prettierignore x_amd64/vet /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet --local .cfg 64/pkg/tool/linu--show-toplevel ortcfg -ato�� g/timeutil/format.go g/timeutil/format_test.go ache/go/1.25.0/x64/pkg/tool/linux_amd64/compile -errorsas -ifaceassert -nilfunc ache/go/1.25.0/x64/pkg/tool/linux_amd64/compile(http block)/usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v4 --jq .object.sha /tmp/gh-aw-test-runs/20260219-005135-15639/test-1322788439 status /usr/bin/git(http block)https://api.github.com/repos/actions/checkout/git/ref/tags/v5/usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha --noprofile(http block)/usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha /repos/test-owner/test-repo/actions/secrets --jq de_modules/.bin/node tierignore 3383282/b039/vetrev-parse ode_modules/.bin--show-toplevel git tion�� /tmp/gh-aw-test-runs/20260219-005135-15639/test-4179609409/.github/workflows config son ignore(http block)/usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha --show-toplevel on rkflow/js/**/*.json /../../.prettiergit erignore t git rev-�� --show-toplevel /opt/hostedtoolcache/go/1.25.0/x--write /usr/bin/git ked-pattern-matcgit -buildtags ode_modules/.bin--show-toplevel /usr/bin/git(http block)https://api.github.com/repos/actions/github-script/git/ref/tags/v8/usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha tierignore(http block)/usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha th .prettierignore 3383282/b100/vet.cfg 64/pkg/tool/linux_amd64/compile(http block)/usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha t_validator.test.cjs(http block)https://api.github.com/repos/actions/setup-go/git/ref/tags/v4/usr/bin/gh gh api /repos/actions/setup-go/git/ref/tags/v4 --jq .object.sha ./../pkg/workflow/js/**/*.json' --ignore-path typecheck /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet l .cfg 64/pkg/tool/linu--show-toplevel /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet -ato�� -bool -buildtags de_modules/.bin/sh -errorsas -ifaceassert -nilfunc 9303883/b410/styles.test(http block)/usr/bin/gh gh api /repos/actions/setup-go/git/ref/tags/v4 --jq .object.sha -json GO111MODULE ache/go/1.25.0/x64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE ache/go/1.25.0/x64/pkg/tool/linux_amd64/vet env -json GO111MODULE /opt/hostedtoolcache/go/1.25.0/x64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/actions/setup-node/git/ref/tags/v4/usr/bin/gh gh api /repos/actions/setup-node/git/ref/tags/v4 --jq .object.sha ./../pkg/workflow/js/**/*.json' --ignore-path k/gh-aw/gh-aw/cmd/gh-aw/capitali../../../.prettierignore 0/x64/bin/node h ../../../.pretgit .cfg 64/pkg/tool/linu--show-toplevel /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet t-ha�� ithub/workflows/ai-moderator.md -buildtags ache/node/24.13.0/x64/bin/node -errorsas -ifaceassert -nilfunc node(http block)/usr/bin/gh gh api /repos/actions/setup-node/git/ref/tags/v4 --jq .object.sha -json GO111MODULE /opt/hostedtoolcache/go/1.25.0/x64/bin/go GOINSECURE GOMOD GOMODCACHE go env -json GO111MODULE ache/node/24.13.0/x64/bin/node GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/github/gh-aw/actions/runs/1/artifacts/usr/bin/gh gh run download 1 --dir test-logs/run-1 -tests ache/go/1.25.0/x64/bin/node(http block)/usr/bin/gh gh run download 1 --dir test-logs/run-1 GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go env -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/github/gh-aw/actions/runs/12345/artifacts/usr/bin/gh gh run download 12345 --dir test-logs/run-12345 -tests x_amd64/vet(http block)/usr/bin/gh gh run download 12345 --dir test-logs/run-12345 GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go env ithub/workflows GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/github/gh-aw/actions/runs/12346/artifacts/usr/bin/gh gh run download 12346 --dir test-logs/run-12346 -tests x_amd64/compile(http block)/usr/bin/gh gh run download 12346 --dir test-logs/run-12346 GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go env -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/github/gh-aw/actions/runs/2/artifacts/usr/bin/gh gh run download 2 --dir test-logs/run-2 pkg/parser/schemas/main_workflow_schema.json ache/node/24.13.0/x64/bin/node(http block)/usr/bin/gh gh run download 2 --dir test-logs/run-2 GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go env 943541375 GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/github/gh-aw/actions/runs/3/artifacts/usr/bin/gh gh run download 3 --dir test-logs/run-3 -tests x_amd64/link logs/command.sh(http block)/usr/bin/gh gh run download 3 --dir test-logs/run-3 GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go env -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/github/gh-aw/actions/runs/4/artifacts/usr/bin/gh gh run download 4 --dir test-logs/run-4 -tests ndor/bin/sh(http block)/usr/bin/gh gh run download 4 --dir test-logs/run-4 GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go env -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/github/gh-aw/actions/runs/5/artifacts/usr/bin/gh gh run download 5 --dir test-logs/run-5(http block)/usr/bin/gh gh run download 5 --dir test-logs/run-5 GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go env 943541375 GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/github/gh-aw/actions/workflows/usr/bin/gh gh workflow list --json name,state,path json' --ignore-path ../../../.prettierignore 3383282/b047/vet.cfg .cfg pkg/workflow/actbash pkg/workflow/act--norc pkg/workflow/act--noprofile ache/go/1.25.0/x64/pkg/tool/linu../../../**/*.json(http block)/usr/bin/gh gh run list --json databaseId,number,url,status,conclusion,workflowName,createdAt,startedAt,updatedAt,event,headBranch,headSha,displayTitle --workflow nonexistent-workflow-12345 --limit 100(http block)/usr/bin/gh gh run list --json databaseId,number,url,status,conclusion,workflowName,createdAt,startedAt,updatedAt,event,headBranch,headSha,displayTitle --workflow nonexistent-workflow-12345 --limit 6 -ifaceassert -nilfunc x_amd64/vet tion�� te '**/*.cjs' '*.github/workflows/test.md -tests es/.bin/sh(http block)https://api.github.com/repos/github/gh-aw/git/ref/tags/v1.0.0/usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v1.0.0 --jq .object.sha ignore-path ../../../.prettierignore(http block)/usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v1.0.0 --jq .object.sha --format %(refname) 64/bin/go -d a87e368f275e9fe7commit 64/bin/go go env -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/nonexistent/action/git/ref/tags/v999.999.999/usr/bin/gh gh api /repos/nonexistent/action/git/ref/tags/v999.999.999 --jq .object.sha ignore-path ../../../.prettierignore(http block)/usr/bin/gh gh api /repos/nonexistent/action/git/ref/tags/v999.999.999 --jq .object.sha /tmp/go-build1751814234/b381/_pkGOINSECURE -trimpath 64/bin/go -p main -lang=go1.25 go env -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/nonexistent/repo/actions/runs/12345/usr/bin/gh gh run view 12345 --repo nonexistent/repo --json status,conclusion(http block)/usr/bin/gh gh run view 12345 --repo nonexistent/repo --json status,conclusion GOINSECURE GOMOD GOMODCACHE go env -json GO111MODULE ache/go/1.25.0/x64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/owner/repo/actions/workflowsOriginal prompt
blockedpattern matching foradd-labelssafe output #16625💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.
✨ PR Review Safe Output Test - Run 22164030442