Skip to content

feat: add opt-in sanitize option to strip script tags - #1574

Open
RAYMOND-LUO wants to merge 17 commits into
nextfrom
raymond/cx-3169-strip-script-tags
Open

feat: add opt-in sanitize option to strip script tags#1574
RAYMOND-LUO wants to merge 17 commits into
nextfrom
raymond/cx-3169-strip-script-tags

Conversation

@RAYMOND-LUO

@RAYMOND-LUO RAYMOND-LUO commented Jul 30, 2026

Copy link
Copy Markdown
Contributor
🎫 Resolve CX-3169

🎯 What does this PR do?

A raw <script>alert(1)</script> in page content ends up verbatim in server-rendered HTML and executes on page load. Neither engine sanitizes it: in MDX, a literal <script> parses as JSX rather than raw HTML, so the format: 'md'-only rehypeSanitize step never sees it; in MDX-ish, the pipeline runs rehypeRaw but has no sanitize step at all. Legacy rdmd always stripped script tags via its sanitize schema, so this closes that one gap.

Scope: this is deliberately narrow. It strips <script> and nothing else, so it does not bring the two engines to full parity with the md sanitize schema. javascript: URLs, on* handlers, <iframe>, <object> and friends still render on mdx and mdxish while md blocks them. Full sanitization for both engines is being handled separately in #1526; this lands the script fix without waiting on that larger change.

Opt-in: sanitize defaults to false, so this PR changes nothing for current callers. ReadMe turns it on per project via a god-mode flag (readmeio/readme#20204), forwarded by readmeio/mdx-renderer#341.

  • New processor/plugin/strip-tags.ts rehype plugin removes any node that would render one of the literal DOM tags in its STRIPPED_TAG_NAMES set (just script today, so future vectors are a one-line change): element nodes with a script tag name, and mdx-jsx nodes whose name is a lowercase-first case-variant of script (browsers parse tag names case-insensitively, so <sCrIpT> would execute). Capitalized <Script> component references are left alone.
  • Wired into both pipelines behind a new sanitize option: lib/compile.ts (all formats) and lib/mdxish.ts (after normalizeMdxJsxNodes).
  • Explicit HTMLBlocks are intentionally exempt, matching legacy: their scripts live in a string prop, never reach SSR HTML, and only run client-side behind runScripts.

🧪 QA tips

  • Add <script>alert(1)</script> to a page body and render via either engine with sanitize: true: no alert, no <script> in output HTML, surrounding content intact
  • Same content without the option: renders exactly as it does today
  • Script tags inside fenced code blocks still render as text

📸 Screenshot or Loom

Before

alertBefore.mov

After

alertAfter.mov

@RAYMOND-LUO
RAYMOND-LUO marked this pull request as ready for review August 5, 2026 16:55
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Added an optional sanitize setting to compiled and MDXish processing. Added caller remark and rehype plugin composition. Added rehypeStripTags, which removes configured literal HTML and lowercase MDX JSX tag subtrees while preserving capitalized components. Added tests for pipeline behavior, tag matching, defaults, plugin composition, nested content, code blocks, components, and HTMLBlock values.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

__tests__/plugins/strip-tags.test.tsx

Oops! Something went wrong! :(

ESLint: 8.57.1

Error: Error while loading rule '@vitest/unbound-method': You have used a rule which requires type information, but don't have parserOptions set to generate type information for this file. See https://tseslint.com/typed-linting for enabling linting with type information.
Parser: /node_modules/@typescript-eslint/parser/dist/index.js
Occurred while linting /tests/plugins/strip-tags.test.tsx
at throwError (/node_modules/@typescript-eslint/utils/dist/eslint-utils/getParserServices.js:40:11)
at getParserServices (/node_modules/@typescript-eslint/utils/dist/eslint-utils/getParserServices.js:29:9)
at create (/node_modules/@typescript-eslint/eslint-plugin/dist/rules/unbound-method.js:138:55)
at Object.create (/node_modules/@typescript-eslint/utils/dist/eslint-utils/RuleCreator.js:39:20)
at create (/node_modules/@vitest/eslint-plugin/dist/index.cjs:6355:35)
at Object.create (/node_modules/@typescript-eslint/utils/dist/eslint-utils/RuleCreator.js:39:20)
at createRuleListeners (/node_modules/eslint/lib/linter/linter.js:895:21)
at /node_modules/eslint/lib/linter/linter.js:1066:110
at Array.forEach ()
at runRules (/node_modules/eslint/lib/linter/linter.js:1003:34)


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

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

🧹 Nitpick comments (2)
processor/plugin/strip-scripts.ts (1)

13-18: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Hoist the lowercase-name regex.

isScriptNode creates /^[a-z]/ for every visited MDX JSX node. Declare it once at module scope.

Proposed change
+const lowercaseFirstLetter = /^[a-z]/;
+
 const isScriptNode = (node: Nodes): boolean => {
   if (node.type === 'element') return node.tagName.toLowerCase() === 'script';

   if (node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') {
     const name = node.name ?? '';
-    return /^[a-z]/.test(name) && name.toLowerCase() === 'script';
+    return lowercaseFirstLetter.test(name) && name.toLowerCase() === 'script';
   }

As per coding guidelines, “Hoist invariants such as regexes, constants, interfaces, and unified() processors to module scope.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@processor/plugin/strip-scripts.ts` around lines 13 - 18, Hoist the invariant
lowercase-name regex used by isScriptNode to module scope, then reuse that
single regex when checking MDX JSX node names. Keep the existing script-node
detection behavior unchanged.

Source: Coding guidelines

__tests__/plugins/strip-scripts.test.tsx (1)

64-80: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Cover the mdxJsxTextElement branch and parser boundaries.

This test covers mdxJsxFlowElement, but isScriptNode has a separate mdxJsxTextElement branch. Add lowercase, mixed-case, and uppercase text-node cases.

Also add escaped-tag, blank-line, and surrounding-whitespace cases through each configured RMDX and MDXish engine.

As per coding guidelines, “Unit-test dense logic directly rather than testing it only through a consumer” and “Cover both RMDX and MDXish engines and test edge cases including … escapes, nesting, blank lines, whitespace, and formatting differences.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@__tests__/plugins/strip-scripts.test.tsx` around lines 64 - 80, Extend the
strip-scripts tests to cover mdxJsxTextElement nodes with lowercase, mixed-case,
and uppercase script names, verifying only literal script nodes are removed
while Script references remain. Add parser-boundary cases for escaped tags,
blank lines, and surrounding whitespace across every configured RMDX and MDXish
engine, including nesting and formatting variations where applicable. Exercise
the dense isScriptNode logic directly in addition to consumer-level coverage.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@__tests__/plugins/strip-scripts.test.tsx`:
- Around line 64-80: Extend the strip-scripts tests to cover mdxJsxTextElement
nodes with lowercase, mixed-case, and uppercase script names, verifying only
literal script nodes are removed while Script references remain. Add
parser-boundary cases for escaped tags, blank lines, and surrounding whitespace
across every configured RMDX and MDXish engine, including nesting and formatting
variations where applicable. Exercise the dense isScriptNode logic directly in
addition to consumer-level coverage.

In `@processor/plugin/strip-scripts.ts`:
- Around line 13-18: Hoist the invariant lowercase-name regex used by
isScriptNode to module scope, then reuse that single regex when checking MDX JSX
node names. Keep the existing script-node detection behavior unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0e12884a-be74-4b02-9e68-092d4b349f60

📥 Commits

Reviewing files that changed from the base of the PR and between 7b229e2 and 40fac50.

📒 Files selected for processing (5)
  • __tests__/lib/mdxish/markdown-inside-single-line-html.test.ts
  • __tests__/plugins/strip-scripts.test.tsx
  • lib/compile.ts
  • lib/mdxish.ts
  • processor/plugin/strip-scripts.ts
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • readmeio/ai (manual)
  • readmeio/gitto (manual)
  • readmeio/markdown (manual)
  • readmeio/readme (manual)

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@__tests__/plugins/strip-scripts.test.tsx`:
- Around line 105-110: Update the test around execute and appendMarker to
include a literal script element in the Markdown/MDX input, then assert the
rendered html excludes both the script markup and its content while retaining
the existing caller-plugin and heading-ID assertions. This must verify
rehypeStripScripts remains active when rehypePlugins is nonempty.
🪄 Autofix

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 Plus

Run ID: 3eef66dd-9883-4f1e-b1e2-f45207178918

📥 Commits

Reviewing files that changed from the base of the PR and between 40fac50 and d959491.

📒 Files selected for processing (3)
  • __tests__/plugins/strip-scripts.test.tsx
  • lib/compile.ts
  • processor/plugin/strip-scripts.ts
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • readmeio/ai (manual)
  • readmeio/gitto (manual)
  • readmeio/markdown (manual)
  • readmeio/readme (manual)
🚧 Files skipped from review as they are similar to previous changes (2)
  • processor/plugin/strip-scripts.ts
  • lib/compile.ts

Comment thread __tests__/plugins/strip-scripts.test.tsx Outdated

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

I think we should not strip unconditionally and should have an option for it, otherwise a lot of customer scripts suddenly would stop working. Have some comments as well!

Comment thread processor/plugin/strip-scripts.ts Outdated

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.

Can we make this plugin more general e.g. strip-tags, and then have script as one of the tags stripped at the moment so we can extend it to more tags?

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.

good call! see a2fa2bb

import { renderingEngines } from '../components/utils';
import { execute, findElementByTagName } from '../helpers';

const engines = [

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.

Nit: We have a renderingEngines variable for tests in __tests__/components/utils., can we move that to __tests__/helperts.ts and reuse it?

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.

Ahh I see, although it does touch imports for a bunch of files and adds noise here. Also this test specifically needs the new flag turned on sanitize: true and the shared renderingEngines takes no options. Happy either way - what do u think?

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.

Ah yeah that’s fine to keep then

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.

Overall comments:

  • Nit: Can we wrap all of these tests in 1 big describe statement? Just for convention
  • Can we add tests for when the script are inside tables, comments, and custom components?

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.

done in 2455601, thx!

Renames the plugin to strip-tags and drives it from a STRIPPED_TAG_NAMES
set holding only `script` for now, so future vectors are a one-line change.
Both pipelines default sanitize to false, preserving current rendering
until a project opts in.

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

🧹 Nitpick comments (1)
__tests__/plugins/strip-tags.test.tsx (1)

33-74: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add pipeline-level edge-case coverage.

The direct transformer test covers <sCrIpT>, but no MDX or MDXish test passes that input through the full pipeline. Add table-driven cases for mixed-case tags, escaped tag text, emphasis, and whitespace or blank-line preservation. Cite CX-3169 in the regression suite.

As per coding guidelines, cover both engines and test emphasis variants, escapes, nesting, blank lines, whitespace, and formatting differences.

Also applies to: 204-223

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@__tests__/plugins/strip-tags.test.tsx` around lines 33 - 74, Expand the
parameterized “strip tags” coverage in the describe.each(engines) suite to
exercise the full MDX/MDXish render pipeline with mixed-case script tags,
escaped tag text, emphasis variants, nested content, blank lines, whitespace,
and formatting differences. Preserve the expected stripping or preservation
behavior for each case, cover both engines through the existing table, and cite
CX-3169 in the regression tests.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@__tests__/plugins/strip-tags.test.tsx`:
- Around line 33-74: Expand the parameterized “strip tags” coverage in the
describe.each(engines) suite to exercise the full MDX/MDXish render pipeline
with mixed-case script tags, escaped tag text, emphasis variants, nested
content, blank lines, whitespace, and formatting differences. Preserve the
expected stripping or preservation behavior for each case, cover both engines
through the existing table, and cite CX-3169 in the regression tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 02368f84-b12f-4a37-9eac-f86bae27d816

📥 Commits

Reviewing files that changed from the base of the PR and between 1efea9b and 2455601.

📒 Files selected for processing (4)
  • __tests__/plugins/strip-tags.test.tsx
  • lib/compile.ts
  • lib/mdxish.ts
  • processor/plugin/strip-tags.ts
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • readmeio/ai (manual)
  • readmeio/gitto (manual)
  • readmeio/markdown (manual)
  • readmeio/readme (manual)

@RAYMOND-LUO RAYMOND-LUO changed the title fix: strip script tags from page content feat: add opt-in sanitize option to strip script tags Aug 6, 2026
@RAYMOND-LUO

RAYMOND-LUO commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Heads up on a change of approach here. Rather than stripping for everyone, this is now gated behind a sanitize option that defaults to off, so nothing changes for existing projects. we don't know how many customers rely on raw html or script tags in their docs, so flipping it globally was too risky.

The opt-in lives as a god mode project flag, so we can turn it on per customer for the ones asking for sanitisation:

  • readmeio/mdx-renderer#341 forwards the flag through the render server. Worth noting it covers reusable content and custom components too, not just the page body, since those are author-editable and would otherwise be the same bypass one level down
  • readmeio/readme#20204 adds the sanitizeHtml flag and the god mode toggle

Both are drafts and depend on this one landing first for the rmdx bump. Full sanitisation (javascript: urls, iframes, etc) is still in #1526's for a follow-up. This only covers script tags, and the plugin is now driven by a STRIPPED_TAG_NAMES set so adding a vector later is an easy change.

cc @flinehan

@flinehan
flinehan requested a review from eaglethrost August 10, 2026 23:24
@RAYMOND-LUO
RAYMOND-LUO removed the request for review from eaglethrost August 10, 2026 23:55
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.

2 participants