Skip to content

fix(code-editor): sanitize raw HTML rendered by Markdown - #1775

Open
Priyank-gouda678 wants to merge 1 commit into
looker-open-source:mainfrom
Priyank-gouda678:code-editor-sanitize-markdown
Open

Priyank-gouda678 wants to merge 1 commit into
looker-open-source:mainfrom
Priyank-gouda678:code-editor-sanitize-markdown

Conversation

@Priyank-gouda678

Copy link
Copy Markdown

Markdown in @looker/code-editor runs rehype-raw with no sanitizer, so raw HTML in the markdown source becomes live DOM. <iframe src="javascript:...">, <iframe srcdoc="<script>...">, <form action="javascript:..."> and <script> all render unchanged (React only drops string on* handlers), and jsdom executes the javascript: iframe during the test run. The hackathon extension feeds participant-written project descriptions and judge notes into this component through ExtMarkdown (ProjectView, JudgingView), so one hacker can run script as any other hacker or judge who opens the project, inside the Looker extension with that user's API access. run-it also renders text/markdown API responses through it.

The fix adds rehype-sanitize (v4, the release that matches react-markdown 6 / unified 9) after rehype-raw, so the GitHub allowlist schema decides which elements, attributes and URL protocols survive. <mark> search highlighting, tables, code blocks and relative/https links keep working, and the existing code-editor, api-explorer, run-it, extension-utils and hackathon scene tests pass. A regression test in Markdown.spec.tsx fails before the change and passes after.

Developer Checklist ℹ️

  • Make sure to open an issue as a bug/issue before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea
  • Ensure the tests and linter pass
  • Appropriate docs were updated (if necessary)

@Priyank-gouda678
Priyank-gouda678 requested a review from a team as a code owner October 28, 2026 16:02
@google-cla

google-cla Bot commented Oct 28, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces HTML sanitization to the Markdown component by adding the rehype-sanitize plugin and includes a test to verify that unsafe tags like iframe, script, and form are stripped. The review feedback recommends expanding this test to also verify that unsafe protocols, such as javascript: on anchor tags, are successfully sanitized.

Comment on lines +136 to +145
test('it strips unsafe raw html', () => {
const markdown = `Text <iframe src="javascript:alert(1)"></iframe>
<iframe srcdoc="<script>alert(1)</script>"></iframe>
<script>alert(1)</script>
<form action="javascript:alert(1)"><button>go</button></form>`;
const { container } = renderWithTheme(<Markdown source={markdown} />);
// eslint-disable-next-line testing-library/no-container
expect(container.querySelector('iframe, script, form')).toBeNull();
expect(screen.getByText(/Text/)).toBeInTheDocument();
});

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.

medium

While the test successfully verifies that unsafe tags like iframe, script, and form are stripped, it does not verify that unsafe protocols (such as javascript:) on allowed tags like <a> are also sanitized. Since javascript: links are a very common XSS vector in Markdown, it is highly recommended to expand the test coverage to assert that these attributes are stripped.

  test('it strips unsafe raw html and protocols', () => {
    const markdown = `Text <iframe src="javascript:alert(1)"></iframe>
<iframe srcdoc="<script>alert(1)</script>"></iframe>
<script>alert(1)</script>
<form action="javascript:alert(1)"><button>go</button></form>
[dangerous link](javascript:alert(1))
<a href="javascript:alert(1)">dangerous raw link</a>`;
    const { container } = renderWithTheme(<Markdown source={markdown} />);
    // eslint-disable-next-line testing-library/no-container
    expect(container.querySelector('iframe, script, form')).toBeNull();
    expect(screen.getByText(/Text/)).toBeInTheDocument();

    // eslint-disable-next-line testing-library/no-container
    const links = container.querySelectorAll('a');
    links.forEach(link => {
      expect(link.getAttribute('href')).toBeNull();
    });
  });

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.

1 participant