Skip to content

SEC-004: Fix content sanitization gaps in exchange_otlp_workload_identity.cjs and report_failed_jobs.cjs - #50806

Merged
pelikhan merged 2 commits into
mainfrom
copilot/sec-004-sanitize-content
Aug 6, 2026
Merged

SEC-004: Fix content sanitization gaps in exchange_otlp_workload_identity.cjs and report_failed_jobs.cjs#50806
pelikhan merged 2 commits into
mainfrom
copilot/sec-004-sanitize-content

Conversation

Copilot AI commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Two handlers tripped the SEC-004 Safe Outputs conformance check: one legitimately uses body for HTTP transport (exempt), the other interpolates API-sourced job names/URLs directly into a GitHub issue body without sanitization.

Changes

  • exchange_otlp_workload_identity.cjs — Add @safe-outputs-exempt SEC-004 annotation; body here is an OAuth/WIF HTTP request payload, not a GitHub content field (mirrors the pattern in artifact_client.cjs).

  • report_failed_jobs.cjs — Import sanitizeContent and apply it in formatFailedJobsList():

    • job.name passed through sanitizeContent() before backtick interpolation
    • job.html_url validated against https:// scheme (drops javascript: and other unsafe schemes) then also sanitized
    const safeName = sanitizeContent(job.name);
    if (job.html_url && job.html_url.startsWith("https://")) {
      const safeUrl = sanitizeContent(job.html_url);
      return `- [\`${safeName}\`](${safeUrl})`;
    }
    return `- \`${safeName}\``;
  • report_failed_jobs.test.cjs (new) — Unit tests for formatFailedJobsList covering plain names, URLs, <script> injection in name, HTML comment @mention injection, javascript: URI rejection, multi-job formatting, and empty input.

…led_jobs content

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix content sanitization gaps in exchange_otlp_workload_identity and report_failed_jobs SEC-004: Fix content sanitization gaps in exchange_otlp_workload_identity.cjs and report_failed_jobs.cjs Aug 6, 2026
Copilot AI requested a review from pelikhan August 6, 2026 07:02
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Triage Summary

Category: bug (security) | Risk: high | Priority score: 78/100

Score breakdown: impact 40 + urgency 25 + quality 13

Recommended action: fast_track

Fixes a SEC-004 sanitization gap allowing unsanitized job names/URLs into issue bodies (potential injection vector). Touches security-sensitive path (exchange_otlp_workload_identity.cjs, report_failed_jobs.cjs) with added tests. No CI/review data yet — flag for expedited human security review given the OTLP/telemetry connection.

Note: related to OpenTelemetry (otlp) per customer triage rules — labeling otel and requesting @mnkeifer review.

Generated by 🔧 PR Triage Agent · auto · 33.2 AIC · ⌖ 2.45 AIC · ⊞ 7.9K ·

@github-actions github-actions Bot added the otel label Aug 6, 2026
@pelikhan
pelikhan marked this pull request as ready for review August 6, 2026 12:08
Copilot AI balanced review requested due to automatic review settings August 6, 2026 12:08

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

Fixes SEC-004 safe-output conformance gaps for failed-job reporting and OAuth transport.

Changes:

  • Documents the OAuth request-body exemption.
  • Sanitizes failed-job names and validates/sanitizes URLs.
  • Adds focused formatting and injection tests.
Show a summary per file
File Description
actions/setup/js/exchange_otlp_workload_identity.cjs Adds the SEC-004 transport exemption.
actions/setup/js/report_failed_jobs.cjs Sanitizes dynamic failed-job content.
actions/setup/js/report_failed_jobs.test.cjs Tests sanitization and URL handling.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 3/3 changed files
  • Comments generated: 0
  • Review effort level: Balanced

@github-actions

github-actions Bot commented Aug 6, 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.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based 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.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

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.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

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

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.

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

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

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.

🧪 Test Quality Sentinel Report

⚠️ Test Quality Score: 77/100 — Acceptable

Analyzed 7 test(s): 7 design, 0 implementation, 0 violation(s).

📊 Metrics (7 tests)
Metric Value
Analyzed 7 (Go: 0, JS: 7)
✅ Design 7 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 4 (57%)
Duplicate clusters 0
Inflation Yes (70 test lines vs 6 prod lines added — 11.7:1)
🚨 Violations 0
Test File Classification Issues
formats a plain job name with a URL report_failed_jobs.test.cjs design_test, high_value None
formats a plain job name without a URL report_failed_jobs.test.cjs design_test, high_value None
sanitizes markdown/HTML special characters report_failed_jobs.test.cjs design_test, high_value None
sanitizes HTML comment injection report_failed_jobs.test.cjs design_test, high_value None
rejects javascript: scheme URL report_failed_jobs.test.cjs design_test, high_value None
joins multiple jobs with newlines report_failed_jobs.test.cjs design_test, high_value None
returns empty string for empty array report_failed_jobs.test.cjs design_test, high_value None

Verdict

Passed. 0% implementation tests (threshold: 30%). Score penalized for test-to-production inflation ratio (11.7:1 > 2:1). All 7 tests verify user-visible security and behavioral contracts of formatFailedJobsList — the high inflation is justified by the security-focused sanitization coverage added in SEC-004.

References: §31100183012

🧪 Test quality analysis by Test Quality Sentinel · sonnet46 · 27 AIC · ⊞ 7.7K ·
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: 77/100. 0% implementation tests (threshold: 30%).

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

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.

Skills-Based Review 🧠

Applied /diagnosing-bugs and /tdd — leaving as COMMENT with a few small hardening suggestions.

📋 Key Themes & Highlights

Key Themes

  • Negative-only assertions: Two tests check that bad content is absent without verifying what the sanitized output actually is — a broken sanitizeContent that returns empty string would still pass.
  • HTML comment vector: The injection test targets the @mention but not the <!-- delimiter itself, which is the real attack surface.
  • Null safety: job.name is not guarded against null/undefined before being passed to sanitizeContent.

Positive Highlights

  • ✅ Correct scheme-check (startsWith('https://')) before accepting html_url — simple and effective.
  • ✅ Good test breadth: covers `(redacted) URI, HTML injection, multi-job, and empty-array cases.
  • ✅ Exempt annotation in exchange_otlp_workload_identity.cjs is well-commented and mirrors the established pattern.
> 🧠 *Reviewed using Matt Pocock's skills by [Matt Pocock Skills Reviewer](https://github.com/github/gh-aw/actions/runs/31100183061)* · sonnet46 · 27.6 AIC · ⊞ 7.1K > Comment /matt to run again


it("sanitizes markdown/HTML special characters in job name", () => {
const result = formatFailedJobsList([{ name: "<script>alert(1)</script>", html_url: null }]);
expect(result).not.toContain("<script>");

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] Test asserts absence of the raw tag but not what the sanitized output is — if sanitizeContent silently returns an empty string or garbles legitimate names, this test still passes.

💡 Suggested fix

Add a positive assertion alongside the negative ones:

it('sanitizes markdown/HTML special characters in job name', () => {
  const result = formatFailedJobsList([{ name: '<script>alert(1)</script>', html_url: null }]);
  expect(result).not.toContain('<script>');
  expect(result).not.toContain('</script>');
  // Verify something meaningful remains after sanitization
  expect(result).toContain('alert(1)');
});

Without the positive assertion, a regression returning - \`` would still pass the test.

@copilot please address this.

expect(result).not.toContain("</script>");
});

it("sanitizes job name containing HTML comment injection", () => {

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] The HTML-comment injection test checks @exploituser is absent, but this conflates mention-neutralization with injection prevention — the real concern is that <!-- itself is stripped. A more precise assertion targets the payload structure.

💡 Suggested fix
it('sanitizes HTML comment injection in job name', () => {
  const result = formatFailedJobsList([{ name: '<!-- `@exploituser` injected payload -->', html_url: null }]);
  expect(result).not.toContain('<!--');
  expect(result).not.toContain('`@exploituser`');
});

Testing only the mention leaves <!-- passing through if mention-neutralization is disabled.

@copilot please address this.

@@ -48,10 +49,12 @@ function isActionsReadPermissionError(error) {
function formatFailedJobsList(jobs) {
return jobs
.map(job => {

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] sanitizeContent(job.name) is called without guarding against null/undefined — the GitHub Jobs API marks name as required, but defensive handling is cheap and prevents a hard crash if the contract is ever violated.

💡 Suggested fix
const safeName = sanitizeContent(job.name ?? '');

Alternatively, add a test for { name: null, html_url: null } to document the current behaviour.

@copilot please address this.

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

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.

Verdict: Request changes — the sanitization fix has a real markdown-breakout gap via unescaped backticks

💡 Themes
  • The exchange_otlp_workload_identity.cjs exemption annotation is consistent with existing @safe-outputs-exempt SEC-004 usage across the codebase (e.g. artifact_client.cjs, mcp_cli_bridge.cjs) — legitimate, no issue.
  • report_failed_jobs.cjs's new sanitizeContent usage correctly filters <script>, HTML-comment @mention injection, and non-https URL schemes, but sanitizeContent/sanitizeContentCore does not strip or escape a literal backtick character. Since the sanitized name is interpolated directly inside backtick/markdown-link syntax, a job.name containing a backtick can break out of the intended inline-code span and inject a forged markdown link (verified via direct reproduction) — this defeats the intent of the fix for the exact injection class SEC-004 targets.
  • The new test suite is a solid start (covers script tags, comment injection, (redacted) rejection) but has no case for a backtick in job.name`, so it would not catch the above.
> 🔎 *Code quality review by [PR Code Quality Reviewer](https://github.com/github/gh-aw/actions/runs/31100183244)* · auto · 56.9 AIC · ⊞ 7.8K > Comment /review to run again

const safeName = sanitizeContent(job.name);
if (job.html_url && job.html_url.startsWith("https://")) {
const safeUrl = sanitizeContent(job.html_url);
return `- [\`${safeName}\`](${safeUrl})`;

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.

Critical: sanitizeContent does not escape raw backtick characters, so a job name containing a backtick can break out of the inline-code span and inject arbitrary markdown (including a forged link) into the issue body — defeating the purpose of this fix.

💡 Details

sanitizeContent/sanitizeContentCore neutralizes @mentions, #refs, XML comments, and non-allowed URL schemes/domains, but never strips or escapes a literal character in the input. Since the code interpolatessafeName/safeUrldirectly inside the backtick+link template, ajob.name` such as:

job`](https://github.com/evil/repo)[pwn

escapes the intended backtick span and renders as a working markdown link with attacker-controlled anchor text, as long as the injected URL host is on the allowed-domains list (e.g. github.com) or the link segment is simply text with no URL at all. Confirmed by direct reproduction against the current sanitizeContent implementation:

formatFailedJobsList([{name: "job`](https://github.com/evil/repo)[pwn", html_url: null}])
// => "- `job`](https://github.com/evil/repo)[pwn`"

job.name in GitHub Actions job payloads can be influenced by matrix values or workflow-defined job/step names, so this is a plausible untrusted-input path feeding directly into a GitHub issue body — the exact class of injection this PR is meant to close.

Suggested fix: escape backticks in safeName/safeUrl before interpolation (e.g. replace a backtick with an escaped backtick or a safe placeholder), or use a helper that guarantees the output cannot contain an unescaped backtick, in addition to the existing sanitizeContent call.

it("returns empty string for empty jobs array", () => {
expect(formatFailedJobsList([])).toBe("");
});
});

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 new test suite doesn't include a case for a backtick in job.name, so it wouldn't have caught the markdown-breakout injection this PR is meant to prevent.

💡 Details

Tests cover <script> tags, HTML comment @mention injection, and (redacted) URL rejection, but none exercise a job name containing a literal backtick (e.g. `` job](https://github.com/evil/repo)[pwn ``). That's precisely the character sanitizeContent fails to neutralize (see the companion comment on `report_failed_jobs.cjs`), so this test suite gives false confidence that the injection surface is fully closed.

Suggested addition:

it("escapes/handles backticks in job name to prevent markdown breakout", () => {
  const result = formatFailedJobsList([{ name: "job`](https://github.com/evil/repo)[pwn", html_url: null }]);
  // Should not produce a working forged markdown link
  expect(result).not.toMatch(/\]\(https:\/\/github\.com\/evil\/repo\)/);
});

@pelikhan

pelikhan commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

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

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.

Review: SEC-004 Content Sanitization Fixes

The changes are correct and well-scoped. Both security issues are addressed appropriately.

exchange_otlp_workload_identity.cjs

The @safe-outputs-exempt SEC-004 annotation is the right fix — body here is an OAuth/WIF HTTP POST payload, not a GitHub content field. Mirrors the established pattern in artifact_client.cjs.

report_failed_jobs.cjs

  • sanitizeContent(job.name) neutralizes HTML tags, XML comments, @mentions, and other injection vectors.
  • startsWith("https://") guard correctly rejects (redacted) (redacted) and other unsafe schemes before embedding in Markdown.
  • Applying sanitizeContent to the URL is safe: buildAllowedDomains() includes GITHUB_SERVER_URL, so html_url from the GitHub API always passes the domain allowlist.

Test coverage

All key injection scenarios are covered: <script> tags, HTML comment @mention injection, `(redacted) URI, multi-job formatting, and empty input. Assertions are valid given the sanitizer behavior.> 🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 31.3 AIC · ⊞ 5.4K

@pelikhan
pelikhan merged commit 8eb2897 into main Aug 6, 2026
59 of 60 checks passed
@pelikhan
pelikhan deleted the copilot/sec-004-sanitize-content branch August 6, 2026 12:32
Copilot stopped work on behalf of pelikhan due to an error August 6, 2026 12:32
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.86.0

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.

[Safe Outputs Conformance] SEC-004: Content sanitization gaps in exchange_otlp_workload_identity.cjs and report_failed_jobs.cjs

3 participants