Skip to content

Normalize labels before security filtering - #74752

Merged
pelikhan merged 4 commits into
mainfrom
copilot/uk-ai-resilience-fix-blocked-label-check
Nov 4, 2026
Merged

pelikhan merged 4 commits into
mainfrom
copilot/uk-ai-resilience-fix-blocked-label-check

Conversation

Copilot AI commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Blocked-label patterns were checked before Unicode sanitization, allowing labels with zero-width or fullwidth characters to normalize into a blocked label after the check.

  • Validation order — Sanitize labels before removal, blocked-pattern, and allowed-pattern checks.
  • Regression coverage — Test zero-width and fullwidth variants of a blocked label.
validateLabels(["bug", "block\u200Bed", "blocked"], undefined, 10, ["blocked"])
// value: ["bug"]

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix bypass of blocked-label check via unnormalized labels Normalize labels before security filtering Nov 4, 2026
Copilot AI requested a review from gh-aw-bot November 4, 2026 05:30
@pelikhan
pelikhan marked this pull request as ready for review November 4, 2026 05:50
Copilot AI auto review requested due to automatic review settings November 4, 2026 05:50
@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

✅ Design Decision Gate 🏗️ completed the design decision gate check. See the comment below for the result and any generated ADR draft.

Design Decision Gate 🏗️

Outcome: reported_no_action

The agent completed and reported no action. A no-op output alone does not verify that all required work was completed or that publication was possible.

Agent messages are omitted from this comment. The category uses structured run status, not interpretation of agent prose.

Generated from Design Decision Gate 🏗️ · routed: gpt56 max · ~1 AIC · ⌖ ~2 AIC · ⊞ 7.7K · ◷

🏗️ ADR gate enforced by Design Decision Gate 🏗️

@github-actions

github-actions Bot commented Nov 4, 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 Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

✅ Test Quality Sentinel completed test quality analysis.

Test Quality Sentinel

Outcome: reported_no_action

The agent completed and reported no action. A no-op output alone does not verify that all required work was completed or that publication was possible.

Agent messages are omitted from this comment. The category uses structured run status, not interpretation of agent prose.

Generated from Test Quality Sentinel · routed: gpt56 max · ~1 AIC · ⌖ ~1 AIC · ⊞ 6.3K · ◷

🧪 Test quality analysis by Test Quality Sentinel

@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

✅ PR Code Quality Reviewer completed the code quality review.

🔎 Code quality review by PR Code Quality Reviewer

@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

✅ Ponytail Reviewer completed successfully!

Generated by Ponytail Reviewer for #74752

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Comment Memory

Peek at saved memory (pr-code-quality-reviewer)
reviewed_at: 2026-11-04T05:53:44Z
review_event: REQUEST_CHANGES
top_themes:
  - blocked-pattern bypass after `@mention` sanitization
files_reviewed:
  - actions/setup/js/safe_output_validator.cjs
  - actions/setup/js/safe_output_validator.test.cjs
comment_count: 1

Note

This comment is managed by comment memory.

Expand the saved memory block to view or edit the persistent context for this thread.
Edit only the text inside the backtick fences; workflow metadata and the footer are regenerated automatically.

Learn more about comment memory

🔎 Code quality review by PR Code Quality Reviewer · copilot · gpt50 · ~3 AIC · ⌖ ~1 AIC · ⊞ 1.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.

The validation-order change introduces a high-severity bypass for blocked @mention labels: sanitization rewrites the value before matching, so existing blocked patterns can stop matching. Please preserve raw-form security filtering while returning sanitized labels.

Highlights
  • Blocked-pattern matching must remain effective for labels transformed by mention neutralization.
  • Add regression coverage for blocked @user-style patterns.

🔎 Code quality review by PR Code Quality Reviewer · copilot · gpt50 · ~3 AIC · ⌖ ~1 AIC · ⊞ 1.6K
Comment /review to run again

// Sanitize labels before applying security-sensitive filters
let validLabels = labels
.filter(label => label != null && label !== false && label !== 0)
.map(label => sanitizeLabelContent(String(label).trim()))

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.

This reordering lets blocked @mention labels bypass the blocked-pattern check because sanitizeLabelContent wraps @name as @name, so a configured blocked pattern like @name no longer matches and the label is emitted.

💡 Preserve the raw value for security filtering

Run blocked-pattern checks against the trimmed raw label as well as the sanitized label (or otherwise reject when either form matches), while still returning the sanitized value. Add a regression test for a blocked @user pattern. This change is intended to harden the security boundary, but the sanitizer's mention-neutralization creates a new mismatch with existing blocked-pattern configurations.

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 in d4918e8: blocked-pattern checks now match both the trimmed raw label and the sanitized, truncated value, while returning only the sanitized value.

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

What this change does: It normalizes each label before checking removal, blocked, and allowed patterns. It also adds regression coverage for zero-width and fullwidth spellings of a blocked label.

Nice to have:

  1. The two regression cases duplicate one test (actions/setup/js/safe_output_validator.test.cjs:L293-L298)

    • What this is: Both tests call validateLabels with the same options and assert the same ["bug"] result; only the Unicode input changes.
    • Problem: The setup and assertions are repeated, leaving two copies to maintain for one behavior.
    • Fix: Use one parameterized it.each case with the two inputs and the shared call and assertions.
    • If we skip it: The tests work, but each additional normalization variant needs another copy of the same seven lines.

Verdict: Ship.
Lean: -7 lines possible.

Generated by ✂️ Ponytail Reviewer for #74752 · codex · routed: gpt56 max · ~4 AIC · ⌖ ~1 AIC · ⊞ 118
Comment /ponytail to run again

expect(result.value).toEqual(["bug"]);
});

it("should block fullwidth labels that sanitize to a blocked label", () => {

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.

Nice to have: these two cases repeat the same setup and assertions; parameterize one test over the zero-width and fullwidth inputs to remove 7 lines while keeping both regression cases and one shared expected result.

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 in d4918e8: the zero-width and fullwidth regression cases are now parameterized with shared setup and assertions.

@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. Requesting changes: the fix closes the zero-width and fullwidth bypasses, but it changes how blocked and allowed patterns match.

📋 Key Themes & Highlights

Key Themes

  • Regression in pattern matching: Patterns are compared only against the sanitized label. Literal entries with @, <, >, &, ' or " stop matching, on both the blocked and allowed lists. Confirmed by running the base and head validators side by side.
  • Test gap: The two new tests use plain-word patterns, so they cannot catch this.
  • Scope: The full vitest suite could not run in this environment (vitest is not installed and npm fails on TLS here). The new assertions pass in a standalone check, but the existing safe_output_validator.test.cjs suite still needs a run.

Positive Highlights

  • ✅ Validation order is now correct: sanitize, then removal, blocklist and allowlist checks.
  • ✅ The new zero-width and fullwidth tests describe the bypass clearly.
  • ✅ Deduplication and truncation are unchanged and still applied after filtering.

@copilot please address the review comments above.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · copilot · routed: haiku55 xhigh · ~4 AIC · ⌖ ~1 AIC · ⊞ 5.4K
Comment /matt to run again

const labelStr = String(label).trim();
const isBlocked = blockedPatterns.some(pattern => matchesSimpleGlob(labelStr, pattern));
validLabels = validLabels.filter(label => {
const isBlocked = blockedPatterns.some(pattern => matchesSimpleGlob(label, pattern));

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] Blocked-pattern matching now runs only on the sanitized label, but the configured patterns are not sanitized. Literal entries containing @, ', <, >, & or " no longer match. Example: validateLabels(["bug", "@copilot"], undefined, 10, ["@copilot"]) returns ["bug", "@copilot"] on this branch but ["bug"] before the change. The blocklist is weaker for any operator who configures such entries.

💡 Suggested fix

Match each pattern against both the trimmed raw label and the sanitized label, and keep the sanitized value as the output. Matching either form only tightens the blocklist. The zero-width and fullwidth cases still block because the sanitized form matches.

const matchesAny = (raw, value, patterns) => patterns.some(p => matchesSimpleGlob(raw, p) || matchesSimpleGlob(value, p));

I checked this in a scratch copy. It keeps the new zero-width and fullwidth blocks and restores the pre-change result for @copilot, <tag> and it's.

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

Fixed in d4918e8: blocked patterns are checked against both the raw label and its sanitized, truncated value. Added a raw @copilot regression.

const labelStr = String(label).trim();
return allowedLabels.some(pattern => matchesSimpleGlob(labelStr, pattern));
});
validLabels = validLabels.filter(label => allowedLabels.some(pattern => matchesSimpleGlob(label, pattern)));

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] The same regression reaches the allowed list. validateLabels(["@bot"], ["@bot"], 10) returned [" @bot "] before this change. It now fails with "No valid labels found after sanitization", because the sanitized `@bot` never matches the raw pattern. Apply the raw-or-sanitized match here too, and cover it with a test.

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

Fixed in d4918e8: allowed patterns now match either the raw label or sanitized value, while the returned label remains sanitized. Added an @bot regression.

});

it("should block labels containing zero-width characters that sanitize to a blocked label", () => {
const result = validator.validateLabels(["bug", "block\u200Bed"], undefined, 10, ["blocked"]);

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.

[/tdd] These tests use plain-word patterns, so they cannot catch the regressions above. Add cases where the pattern itself contains a character the sanitizer changes, and assert the expected result for both blocked and allowed lists.

💡 Suggested test
it("should still block literal blocked patterns containing sanitized characters", () => {
  const result = validator.validateLabels(["bug", "@copilot"], undefined, 10, ["@copilot"]);

  expect(result.value).toEqual(["bug"]);
});

@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 in d4918e8: regression tests cover transformed @mention patterns in blocked and allowed lists, alongside Unicode normalization and truncation.

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.

🟡 Changes recommended

Label truncation occurs after policy checks, allowing a validated value to become a blocked label that still reaches the label handlers.

1 open issue
What changed in this PR

Updates gh-aw’s shared safe-output label validator to address the Unicode-based blocked-label bypass reported in #74548.

Changes:

  • Sanitizes labels before removal detection and blocked/allowed pattern checks.
  • Adds regression tests for zero-width and fullwidth variants of blocked labels.
File Description
actions/​setup/​js/​safe_output_validator.test.cjs Adds Unicode blocked-label regression cases.
actions/​setup/​js/​safe_output_validator.cjs Moves label sanitization ahead of security-sensitive filters.

Review effort: Max (auto)

// Sanitize labels before applying security-sensitive filters
let validLabels = labels
.filter(label => label != null && label !== false && label !== 0)
.map(label => sanitizeLabelContent(String(label).trim()))

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 d4918e8: labels are sanitized and truncated to 64 code units before blocked/allowed checks. Added an expansion-and-truncation regression where raw input does not match but the resulting value does.

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Note

The pull request was not created — a fallback review issue was created instead due to protected file changes: #74772

PR Finisher

Submitted for publication (not yet confirmed pushed): merge of main plus fixes in safe_output_validator.cjs: labels are sanitized and truncated to 64 before filtering; blocked/allowed patterns match raw, sanitized-pattern, or raw-label forms. Tests parameterized and extended (mention, backtick/<> patterns, allowed list, truncation).

Validation: ran make fmt and a manual node check of the new cases. vitest could not be installed (npm cert error), so make agent-report-progress was not run.

Review replies/resolutions deferred until the push is published. CI needs a maintainer re-trigger.

PR finisher dispatch requested for the following outstanding work: (see Sous-chef lines below)

Sous-chef head: ab75cfb
Sous-chef work: 1b7473767c21baf8033827fd8167e459c0503480b892e9bf47cfa385b0b9a257 3afb352016f3c4e46026eb466d879c257c142ee3e224fbcdc08e84ec81d1fdc3 6396d5295eee7f72bac5918e579bf9915c579967f3cc2e027e5b6dbad1557fce aecf91458e40d827073e22601dc73eb9fa74cebc9e6a8177368fce43c129bd46 e776465fcd1fa2653fec61a9b5c19a3a6ae041b8a70399ea7c11844c0b83bf9e ec0cbfdaf5533ee54e77be3616b1af624b1212427551e5ac033899efded395dd
Sous-chef state: ac193425dae86e169f0c7af7421e55cb62f7ed2abeaac67180d41a8cb4c3a119

Generated by 🏁 PR Finisher · copilot · auto · ~23 AIC · ⌖ ~1 AIC · ⊞ 10.7K · ◷

@pelikhan

pelikhan commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

@copilot run pr-finisher skill

Ran the PR-finisher workflow. Commit d4918e8 fixes the review feedback, and formatting, lint, and impacted tests pass. CI must be re-triggered by a maintainer; review replies are posted, but I cannot resolve threads with the available GitHub tools.

Copilot AI requested a review from pelikhan November 4, 2026 07:05
@pelikhan
pelikhan merged commit fdfc5d5 into main Nov 4, 2026
17 of 18 checks passed
@pelikhan
pelikhan deleted the copilot/uk-ai-resilience-fix-blocked-label-check branch November 4, 2026 07:09
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v1.2.0

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[uk-ai-resilience] Tier C: blocked-label check bypass via unnormalised labels (code scanning 1061)

4 participants