Skip to content

feat: add <img> to safe-outputs HTML tag allowlist#29401

Merged
pelikhan merged 4 commits intomainfrom
copilot/add-img-to-safe-outputs-allowlist
Apr 30, 2026
Merged

feat: add <img> to safe-outputs HTML tag allowlist#29401
pelikhan merged 4 commits intomainfrom
copilot/add-img-to-safe-outputs-allowlist

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 30, 2026

<img> tags in safe-output bodies (e.g. create-issue, update-issue) were being converted to parentheses form — breaking alignment/sizing patterns common in GitHub Markdown. Standard ![alt](url) syntax can't replicate align or width attributes.

Changes

  • sanitize_content_core.cjs — adds "img" to the allowedTags array in convertXmlTags(); on* event handlers and style are already stripped by stripDangerousAttributes(), and src is already validated by sanitizeUrlDomains()

  • sanitize_content.test.cjs — updates tests to reflect <img> being preserved:

    • <img/> preserved instead of converted to (img/)
    • Mixed-content test expects <img src=x> to pass through
    • XSS test updated: verifies onerror/onload are stripped from the tag rather than asserting <img is absent
    • New tests: layout attributes (align, width, alt) preserved; onerror stripped

Before / after:

<!-- input -->
<img align="right" width="120" src="https://example.com/mascot.png" alt="Mascot" />

<!-- before -->
(img align="right" width="120" src="https://example.com/mascot.png" alt="Mascot" /)

<!-- after -->
<img align="right" width="120" src="https://example.com/mascot.png" alt="Mascot" />

XSS safety is unchanged — <img src=x onerror="alert(1)"> becomes <img src=x>.

Copilot AI linked an issue Apr 30, 2026 that may be closed by this pull request
Add `img` to the `allowedTags` array in `convertXmlTags()` so that
`<img>` tags are preserved in safe-output content instead of being
converted to parentheses. Dangerous attributes (on* event handlers
and style) are already stripped by `stripDangerousAttributes()`, and
the `src` URL is validated by `sanitizeUrlDomains()`.

Update tests to reflect the new behaviour:
- `<img/>` is preserved rather than converted
- `<img src=x>` in mixed content is preserved
- XSS test updated: `onerror`/`onload` are stripped, but the `<img>`
  tag itself is kept
- New positive tests for layout attributes (align, width, alt) and
  XSS attribute stripping

Agent-Logs-Url: https://github.com/github/gh-aw/sessions/5076fa54-7dd9-4f3a-bed2-9364d21e3c62

Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
Copilot AI changed the title [WIP] Add <img> to safe-outputs HTML tag allowlist feat: add <img> to safe-outputs HTML tag allowlist Apr 30, 2026
Copilot AI requested a review from lpcox April 30, 2026 23:18
@pelikhan pelikhan marked this pull request as ready for review April 30, 2026 23:31
Copilot AI review requested due to automatic review settings April 30, 2026 23:31
@github-actions
Copy link
Copy Markdown
Contributor

✅ smoke-ci: safeoutputs CLI comment + comment-memory run (25194518386)

Generated by Smoke CI for issue #29401 ·

@github-actions
Copy link
Copy Markdown
Contributor

Comment Memory

CI lights the path
Green checks bloom at dawn
Quiet bots still sing

Note

This comment is managed by comment memory.

It stores persistent context for this thread in the code block at the top of this comment.
Edit only the text inside the backtick fences; workflow metadata and the footer are regenerated automatically.

Learn more about comment memory

Generated by Smoke CI for issue #29401 ·

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Enables preservation of HTML <img> tags in “safe-output” sanitized Markdown so image alignment/sizing attributes (e.g. align, width) are not lost during XML-tag neutralization.

Changes:

  • Added img to the convertXmlTags() safe HTML allowlist so <img> is preserved rather than converted to parentheses form.
  • Updated and added sanitizer tests to expect <img> to pass through while ensuring dangerous on* attributes are stripped.
Show a summary per file
File Description
actions/setup/js/sanitize_content_core.cjs Adds img to the safe HTML tag allowlist and documents expected safety controls.
actions/setup/js/sanitize_content.test.cjs Updates tag-conversion and XSS tests for the new <img> allowlist behavior and adds new img-specific assertions.

Copilot's findings

Tip

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

Comments suppressed due to low confidence (2)

actions/setup/js/sanitize_content_core.cjs:625

  • The new note says src is covered by sanitizeUrlDomains(), but URL sanitization here is actually split across sanitizeUrlProtocols() (blocks data:, javascript:, http: etc) and sanitizeUrlDomains() (only allowlists absolute https:// and protocol-relative // URLs). Consider updating this comment to reflect both steps and clarify that relative/non-URL src values aren’t domain-validated by sanitizeUrlDomains().
  // b, blockquote, br, code, details, em, h1–h6, hr, i, img, li, ol, p, pre, strong, sub, summary, sup, table, tbody, td, th, thead, tr, ul
  // Plus GFM inline tags: abbr, del, ins, kbd, mark, s, span
  // Note: img on* event handlers and style are stripped by stripDangerousAttributes(); src is covered by sanitizeUrlDomains()

actions/setup/js/sanitize_content.test.cjs:542

  • The new img XSS test covers onerror when it’s written in the normal whitespace-separated form. Given the allowlisting change, please also add a regression test for malformed-but-common browser parsing edge cases like <img/onerror=alert(1) src=x> (slash immediately after tag name), which currently won’t match the \s+on\w+ stripping pattern and could leave the handler intact.
    it("should strip dangerous event-handler attributes from img tags", () => {
      const result = sanitizeContent('<img src=x onerror="alert(1)">');
      expect(result).toContain("<img");
      expect(result).not.toContain("onerror");
    });
  • Files reviewed: 2/2 changed files
  • Comments generated: 2

Comment on lines 621 to +625
function convertXmlTags(s) {
// Allow safe HTML tags supported by GitHub Flavored Markdown:
// b, blockquote, br, code, details, em, h1–h6, hr, i, li, ol, p, pre, strong, sub, summary, sup, table, tbody, td, th, thead, tr, ul
// b, blockquote, br, code, details, em, h1–h6, hr, i, img, li, ol, p, pre, strong, sub, summary, sup, table, tbody, td, th, thead, tr, ul
// Plus GFM inline tags: abbr, del, ins, kbd, mark, s, span
// Note: img on* event handlers and style are stripped by stripDangerousAttributes(); src is covered by sanitizeUrlDomains()
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allowlisting img makes it possible to preserve malformed-but-browser-tolerated tags like <img/onerror=alert(1) src=x>. Because stripDangerousAttributes() only strips on*/style when preceded by whitespace, /onerror (immediately after the tag name) would bypass stripping and could leave an executable event handler in the output. Please harden the attribute stripping to handle a / directly after the tag name (or otherwise tokenize attributes), and add a regression test for this form.

This issue also appears on line 623 of the same file.

Copilot uses AI. Check for mistakes.
Comment on lines +524 to +527
it("should preserve self-closing img tags", () => {
const result = sanitizeContent("Hello <img/> world");
expect(result).toBe("Hello (img/) world");
expect(result).toBe("Hello <img/> world");
});
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change repurposes the prior test that validated conversion of disallowed self-closing tags (e.g. <img/>(img/)). Now that img is allowed, we’re missing coverage that a non-allowlisted self-closing tag is still converted (e.g. <div/>(div/)). Please add a replacement assertion for a disallowed self-closing tag so the behavior stays protected.

This issue also appears on line 538 of the same file.

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 75/100

⚠️ Acceptable — with suggestions

Metric Value
New/modified tests analyzed 4
✅ Design tests (behavioral contracts) 4 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 2 (50%)
Duplicate test clusters 0
Test inflation detected ⚠️ Yes (28 test lines added vs 3 production lines; ratio ≈ 9.3:1)
🚨 Coding-guideline violations None

Test Classification Details

View All Test Results (4 tests)
Test File Classification Issues Detected
should preserve self-closing img tags sanitize_content.test.cjs:~523 ✅ Design Happy-path only; no error case
should preserve img tags with layout attributes sanitize_content.test.cjs:~528 ✅ Design Happy-path only; src attribute not verified
should strip dangerous event-handler attributes from img tags sanitize_content.test.cjs:~536 ✅ Design ✅ Covers security edge case
should handle malicious XSS attempts (modified) sanitize_content.test.cjs:~1578 ✅ Design ✅ Covers XSS edge cases; updated for img allowance

Flagged Tests — Suggestions

⚠️ should preserve img tags with layout attributes

Classification: Design test (behavioral contract) — but partial
Issue: The test verifies align, width, and alt attributes are preserved but does not verify the src attribute, which is arguably the most important attribute on an <img> tag.
What design invariant does this test enforce? Layout-relevant img attributes survive sanitization.
What would break if deleted? A regression stripping layout attributes would go undetected — but a regression stripping the src URL would also go undetected.
Suggested improvement: Add `expect(result).toContain('src="(example.com/redacted) to verify the image source is preserved.

⚠️ should strip dangerous event-handler attributes from img tags + XSS section of should handle malicious XSS attempts

Classification: Both valid design tests — slight duplication
Issue: Both test onerror stripping on <img src=x>'. The standalone test and the XSS test both assert toContain("<img") and not.toContain("onerror"). Two tests is fine (not a full duplicate cluster requiring 3+), but consider merging or differentiating the test cases more clearly.
Suggested improvement: Keep the dedicated test for onerror and ensure the XSS suite focuses on the broader XSS taxonomy (e.g., onload, script injection) without fully repeating the onerror case.

⚠️ Test Inflation

Issue: 28 lines added to the test file vs. 3 lines added to the production file (sanitize_content_core.cjs) — a ratio of approximately 9.3:1, exceeding the 2:1 guideline threshold.
Context: For a security-sensitive change (adding a new allowed tag), thorough test coverage is appropriate and the tests themselves are genuinely high-value. The inflation is understandable here, but worth noting for awareness.


Language Support

Tests analyzed:

  • 🟨 JavaScript (*.test.cjs): 4 tests (vitest)
  • 🐹 Go (*_test.go): 0 tests

Verdict

⚠️ Check passed. 0% of new tests are implementation tests (threshold: 30%). Score is 75/100 (acceptable). Minor suggestions above — none blocking.

The tests provide good behavioral coverage for the <img> allowlist addition, particularly the XSS/event-handler stripping cases. The test inflation flag is informational: for a security feature, the extra test thoroughness is justified.


📖 Understanding Test Classifications

Design Tests (High Value) verify what the system does:

  • Assert on observable outputs, return values, or state changes
  • Cover error paths and boundary conditions
  • Would catch a behavioral regression if deleted
  • Remain valid even after internal refactoring

Implementation Tests (Low Value) verify how the system does it:

  • Assert on internal function calls (mocking internals)
  • Only test the happy path with typical inputs
  • Break during legitimate refactoring even when behavior is correct
  • Give false assurance: they pass even when the system is wrong

Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.

References:

🧪 Test quality analysis by Test Quality Sentinel · ● 345.8K ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test Quality Sentinel: 75/100. Test quality is acceptable — 0% of new tests are implementation tests (threshold: 30%). Minor suggestions posted in the comment above (missing src attribute assertion, slight onerror duplication, test inflation noted but contextually justified for a security feature).

@github-actions github-actions Bot mentioned this pull request Apr 30, 2026
@pelikhan
Copy link
Copy Markdown
Collaborator

… test failures

- Change \s+ to [\s/]+ in stripDangerousAttributes() regex so that
  malformed tags like <img/onerror=alert(1) src=x> cannot bypass
  dangerous-attribute stripping (the / immediately after the tag name
  is now treated as a separator, same as whitespace)
- Update compute_text.test.cjs and sanitize_output.test.cjs to expect
  <img> to be preserved now that img is in the allowlist (fixes 4 CI
  failures)
- Add test: disallowed self-closing tag <div/> still converts to (div/)
- Add test: slash-prefixed dangerous attribute is stripped from img

Agent-Logs-Url: https://github.com/github/gh-aw/sessions/69fecc69-c347-40a7-b18d-82ba5ff2091f

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 30, 2026

@copilot review all comments and https://github.com/github/gh-aw/actions/runs/25194524570/job/73872015354?pr=29401

Addressed all review comments and the CI failures in commit 16c9015:

  1. CI failures (compute_text.test.cjs, sanitize_output.test.cjs) — 4 tests were still expecting (img ...) from when img wasn't on the allowlist; updated them to expect the preserved <img ...> form.

  2. Security hardening — changed \s+ to [\s/]+ in stripDangerousAttributes() so malformed tags like <img/onerror=alert(1) src=x> no longer bypass stripping (the / immediately after the tag name is now treated as an attribute separator). Added a regression test for this form.

  3. Disallowed self-closing tag coverage — added a test asserting <div/>(div/) so that protection is still verified.

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:

  • invalid.example.invalid
    • Triggering command: /usr/lib/git-core/git-remote-https /usr/lib/git-core/git-remote-https origin https://invalid.example.invalid/nonexistent-repo.git e/git init�� ndor/bin/git git ode_modules/.bin/git =receive test@example.com--git-dir=/tmp/bare-incremental-Qo0eAd /git (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI requested a review from pelikhan April 30, 2026 23:47
@pelikhan
Copy link
Copy Markdown
Collaborator

@lpcox fyi

@pelikhan pelikhan merged commit a1b4aef into main Apr 30, 2026
@pelikhan pelikhan deleted the copilot/add-img-to-safe-outputs-allowlist branch April 30, 2026 23:56
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.

Add <img> to safe-outputs HTML tag allowlist

4 participants