Skip to content

fix: html script, style, and comment escaping#3159

Merged
DylanPiercey merged 1 commit intomainfrom
fix-script-style-escape
Apr 17, 2026
Merged

fix: html script, style, and comment escaping#3159
DylanPiercey merged 1 commit intomainfrom
fix-script-style-escape

Conversation

@DylanPiercey
Copy link
Copy Markdown
Contributor

Fix escaping issue for dynamic text interpolation inside <script>, <style>, <html-script> and <html-style> tags.

The issue was that the escaping logic for those tags used a CASE SENSITIVE search for the closing tag which could be bypassed like so:

<script>${"</SCRIPT><img src=x onerror=alert('uh oh')>"}</script>

Note that script and style there should never render unsanitized user defined values, regardless of wether or not the closing tag is escaped, since these are conceptually just "eval".

Also fixes escaping for <html-comment> tag.
Previously this tag relied on normal xml escaping which looks for <.
This PR updates to have a special escape for <html-comment> tags that replaces > instead.

// Previously incorrectly escaped.
<html-comment>${">Uh oh"}</html-comment>

@DylanPiercey DylanPiercey self-assigned this Apr 17, 2026
@github-project-automation github-project-automation bot moved this to Todo in Roadmap Apr 17, 2026
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 17, 2026

🦋 Changeset detected

Latest commit: 57a555e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
marko Patch
@marko/runtime-tags Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 17, 2026

Codecov Report

❌ Patch coverage is 98.27586% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 89.58%. Comparing base (33f925a) to head (57a555e).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...s/runtime-tags/src/translator/core/html-comment.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3159   +/-   ##
=======================================
  Coverage   89.57%   89.58%           
=======================================
  Files         370      371    +1     
  Lines       47086    47134   +48     
  Branches     4270     4273    +3     
=======================================
+ Hits        42177    42224   +47     
- Misses       4857     4858    +1     
  Partials       52       52           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 17, 2026

Walkthrough

This PR adds patch updates for marko and @marko/runtime-tags packages to fix escaping issues in dynamic text interpolation. The changes address a bypass of escaping logic by implementing case-insensitive matching for closing tags in <script> and <style> contexts. A new escape helper function is introduced to handle > character escaping in <html-comment> content to prevent premature comment termination. Multiple test fixtures are added to validate escaping behavior across different tag contexts. Runtime helper exports are updated to support comment escaping functionality.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: html script, style, and comment escaping' directly describes the primary changes across the PR, covering all three main escaping fixes addressed.
Description check ✅ Passed The description clearly explains the escaping issues fixed for script/style tags (case-sensitivity bypass) and html-comment tags (> character handling), with concrete examples of vulnerable patterns.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-script-style-escape

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
packages/runtime-tags/src/__tests__/fixtures/html-style-injection/template.marko (1)

1-2: Consider adding a mixed-case </StYlE> variant to harden the regression.

Uppercase is covered well; a mixed-case case adds confidence against future regressions in case-insensitive matching.

Based on learnings, tests in this area should be fixture-driven; adding one more fixture case fits that pattern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/runtime-tags/src/__tests__/fixtures/html-style-injection/template.marko`
around lines 1 - 2, Add a new fixture case that mirrors the existing test but
uses a mixed-case closing tag variant (e.g., "</StYlE>") to ensure
case-insensitive matching is robust: update the injection value (the
let/injection variable) to include "</StYlE>" and add a corresponding css rule
in the <html-style> block (e.g., .evil { content: '${injection}'; }) so the test
covers mixed-case closing-tag injection alongside the existing uppercase
variant.
packages/runtime-tags/src/__tests__/fixtures/html-script-injection/template.marko (1)

1-2: Optional: add mixed-case </ScRiPt> fixture coverage too.

This improves long-term confidence that the matcher remains truly case-insensitive.

Based on learnings, fixture-based tests are the expected style for this package area.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/runtime-tags/src/__tests__/fixtures/html-script-injection/template.marko`
around lines 1 - 2, Add a new fixture (or extend the existing template.marko)
that exercises a mixed-case script terminator so the matcher is proven
case-insensitive: create a variant where the injected string is "</ScRiPt>"
(same symbol name injection used in the diff) and render it inside the
<html-script> block (the same html-script usage in template.marko) to ensure the
test harness/fixture asserts correct behavior with mixed-case closing tags.
packages/runtime-class/test/render/fixtures/escape-comment/test.js (1)

1-3: Add a direct leading-> fixture input to mirror the reported bug.

"-->" is good coverage, but a second case like ">Uh oh" would directly guard the exact scenario called out in the PR description.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/runtime-class/test/render/fixtures/escape-comment/test.js` around
lines 1 - 3, Add a second fixture entry that includes a string starting with a
direct leading '>' to cover the reported edge case; update the exported test
data (exports.templateData) or add a new export (e.g.,
exports.templateDataLeadingGt) to include a value like ">Uh oh" alongside the
existing "-->" value so the test harness exercises the exact scenario described
in the PR.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.changeset/blue-dragons-see.md:
- Around line 6-14: Update the release note text to (1) add the <html-comment>
tag to the list of tags whose dynamic interpolation escaping was tightened so
the sentence includes "<script>, <style>, <html-script>, <html-style> and
<html-comment>", and (2) fix the wording on the sentence containing "wether or
not the closing tag is escaped" to read "whether or not" and change "user
defined" to "user-defined"; keep the existing example and meaning otherwise.

In @.changeset/frank-memes-knock.md:
- Around line 6-13: Update the changeset note to mention both the
`<html-comment>` fix and the script/style close-tag escaping patches: explicitly
state that `<html-comment>` now uses a special escape replacing `>` instead of
the normal XML `<`-based escaping, and add that script and style tag closing
sequences are also fixed to prevent incorrect escaping of their `</...>` close
tags; reference the `<html-comment>` tag and the script/style close-tag behavior
in the single-sentence summary so release notes reflect the full security fix
scope.

In
`@packages/runtime-class/src/runtime/html/helpers/escape-comment-placeholder.js`:
- Around line 15-16: The escapeCommentHelper currently stringifies every value
causing false/null/undefined to render as "false"/"null"/"undefined"; change
escapeCommentHelper to match other HTML escape helpers by returning "" for null,
undefined, and false while preserving 0: i.e., if value === 0 pass "0" to
escape, if value === null || value === undefined || value === false return "",
otherwise stringify and pass to escape; update the exported function
escapeCommentHelper accordingly.

---

Nitpick comments:
In `@packages/runtime-class/test/render/fixtures/escape-comment/test.js`:
- Around line 1-3: Add a second fixture entry that includes a string starting
with a direct leading '>' to cover the reported edge case; update the exported
test data (exports.templateData) or add a new export (e.g.,
exports.templateDataLeadingGt) to include a value like ">Uh oh" alongside the
existing "-->" value so the test harness exercises the exact scenario described
in the PR.

In
`@packages/runtime-tags/src/__tests__/fixtures/html-script-injection/template.marko`:
- Around line 1-2: Add a new fixture (or extend the existing template.marko)
that exercises a mixed-case script terminator so the matcher is proven
case-insensitive: create a variant where the injected string is "</ScRiPt>"
(same symbol name injection used in the diff) and render it inside the
<html-script> block (the same html-script usage in template.marko) to ensure the
test harness/fixture asserts correct behavior with mixed-case closing tags.

In
`@packages/runtime-tags/src/__tests__/fixtures/html-style-injection/template.marko`:
- Around line 1-2: Add a new fixture case that mirrors the existing test but
uses a mixed-case closing tag variant (e.g., "</StYlE>") to ensure
case-insensitive matching is robust: update the injection value (the
let/injection variable) to include "</StYlE>" and add a corresponding css rule
in the <html-style> block (e.g., .evil { content: '${injection}'; }) so the test
covers mixed-case closing-tag injection alongside the existing uppercase
variant.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6d9f5ab3-6f92-42b0-9ea7-3f2baf66397c

📥 Commits

Reviewing files that changed from the base of the PR and between 33f925a and 57a555e.

⛔ Files ignored due to path filters (31)
  • packages/runtime-tags/src/__tests__/fixtures/html-comment-counter/__snapshots__/html.expected/template.js is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/.name-cache.json is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/csr-sanitized.expected.md is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/csr.expected.md is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/dom.expected/template.hydrate.js is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/dom.expected/template.js is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/html.expected/template.js is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/resume-sanitized.expected.md is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/resume.expected.md is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/ssr-sanitized.expected.md is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/ssr.expected.md is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/.name-cache.json is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/csr-sanitized.expected.md is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/csr.expected.md is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/dom.expected/template.hydrate.js is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/dom.expected/template.js is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/html.expected/template.js is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/resume-sanitized.expected.md is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/resume.expected.md is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/ssr-sanitized.expected.md is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/ssr.expected.md is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/.name-cache.json is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/csr-sanitized.expected.md is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/csr.expected.md is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/dom.expected/template.hydrate.js is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/dom.expected/template.js is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/html.expected/template.js is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/resume-sanitized.expected.md is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/resume.expected.md is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/ssr-sanitized.expected.md is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/ssr.expected.md is excluded by !**/__snapshots__/** and included by **
📒 Files selected for processing (29)
  • .changeset/blue-dragons-see.md
  • .changeset/frank-memes-knock.md
  • packages/runtime-class/src/runtime/html/helpers/escape-comment-placeholder.js
  • packages/runtime-class/src/runtime/html/helpers/escape-script-placeholder.js
  • packages/runtime-class/src/runtime/html/helpers/escape-style-placeholder.js
  • packages/runtime-class/src/translator/taglib/core/translate-html-comment.js
  • packages/runtime-class/test/render/fixtures/escape-comment/expected.html
  • packages/runtime-class/test/render/fixtures/escape-comment/template.marko
  • packages/runtime-class/test/render/fixtures/escape-comment/test.js
  • packages/runtime-class/test/render/fixtures/escape-comment/vdom-expected.html
  • packages/runtime-class/test/render/fixtures/escape-script-case/expected.html
  • packages/runtime-class/test/render/fixtures/escape-script-case/template.marko
  • packages/runtime-class/test/render/fixtures/escape-script-case/test.js
  • packages/runtime-class/test/render/fixtures/escape-script-case/vdom-expected.html
  • packages/runtime-class/test/render/fixtures/escape-style-case/expected.html
  • packages/runtime-class/test/render/fixtures/escape-style-case/template.marko
  • packages/runtime-class/test/render/fixtures/escape-style-case/test.js
  • packages/runtime-class/test/render/fixtures/escape-style-case/vdom-expected.html
  • packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/template.marko
  • packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/test.ts
  • packages/runtime-tags/src/__tests__/fixtures/html-script-injection/template.marko
  • packages/runtime-tags/src/__tests__/fixtures/html-script-injection/test.ts
  • packages/runtime-tags/src/__tests__/fixtures/html-style-injection/template.marko
  • packages/runtime-tags/src/__tests__/fixtures/html-style-injection/test.ts
  • packages/runtime-tags/src/__tests__/html-content.test.ts
  • packages/runtime-tags/src/html.ts
  • packages/runtime-tags/src/html/content.ts
  • packages/runtime-tags/src/translator/core/html-comment.ts
  • packages/runtime-tags/src/translator/util/runtime.ts

Comment thread .changeset/blue-dragons-see.md
Comment thread .changeset/frank-memes-knock.md
@DylanPiercey DylanPiercey merged commit 19d4b37 into main Apr 17, 2026
11 checks passed
@DylanPiercey DylanPiercey deleted the fix-script-style-escape branch April 17, 2026 01:17
@github-project-automation github-project-automation bot moved this from Todo to Done in Roadmap Apr 17, 2026
@github-actions github-actions bot mentioned this pull request Apr 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant