Skip to content

Sanitize notification HTML to prevent XSS#3243

Merged
mlodic merged 2 commits intointelowlproject:developfrom
IshaanXCoder:fix/sanitize-notification
Jan 29, 2026
Merged

Sanitize notification HTML to prevent XSS#3243
mlodic merged 2 commits intointelowlproject:developfrom
IshaanXCoder:fix/sanitize-notification

Conversation

@IshaanXCoder
Copy link
Contributor

Sanitize notification HTML to prevent XSS

Closes #3123

Description

This PR fixes a potential XSS vulnerability in the NotificationsList component by replacing dangerouslySetInnerHTML with react-markdown

Changes

  • Replaced dangerouslySetInnerHTML with <ReactMarkdown> in frontend/src/components/jobs/notification/NotificationsList.jsx
  • Configured rel="noopener noreferrer" for external links to improve security.
  • Note: Modifying src/components/common/markdownToHtml.jsx was avoided because it is being used across multiple components, thus I added the snippet to NotificationsList.jsx itself. Let me know if I should be folllowign an alternative approach.
  • Tested in frontend/tests/components/jobs/notification/NotificationsList.test.jsx

Type of change

  • Bug fix (non-breaking change which fixes an issue).

Checklist

  • I have read and understood the rules about how to Contribute to this project
  • The pull request is for the branch develop
  • Please avoid adding new libraries as requirements whenever it is possible. Use new libraries only if strictly needed to solve the issue you are working for. In case of doubt, ask a maintainer permission to use a specific library.
  • [N/A] Linters (Black, Flake, Isort) gave 0 errors. If you have correctly installed pre-commit, it does these checks and adjustments on your behalf.
  • I have added tests for the feature/bug I solved (see tests folder). All the tests (new and old ones) gave 0 errors.
  • If the GUI has been modified:
    • I have a provided a screenshot of the result in the PR.
    • I have created new frontend tests for the new component or updated existing ones.

Copilot AI review requested due to automatic review settings January 28, 2026 05:48
Copy link
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

Sanitizes notification rendering to mitigate XSS by replacing raw HTML injection with Markdown rendering.

Changes:

  • Replaced dangerouslySetInnerHTML usage in notifications with react-markdown.
  • Added custom markdown renderers for emphasis and links (including target="_blank" and rel="noopener noreferrer").
  • Added frontend tests focused on XSS payloads and basic markdown behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
frontend/src/components/jobs/notification/NotificationsList.jsx Switches notification body rendering to ReactMarkdown with custom link handling.
frontend/tests/components/jobs/notification/NotificationsList.test.jsx Adds tests to ensure injected HTML/scripts don’t produce DOM elements and validates markdown rendering behaviors.

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

Comment on lines +16 to +23
const markdownComponents = {
// eslint-disable-next-line id-length
em: ({ node: _node, ...props }) => <i className="text-code" {...props} />,
// eslint-disable-next-line id-length
a: ({ node: _node, href, children, ...props }) => {
// eslint-disable-next-line no-script-url
if (href && href.startsWith("javascript:")) {
return null;
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

The javascript: URL check is easy to bypass because it’s case-sensitive and doesn’t normalize/trim the URL (e.g., JaVaScRiPt:..., leading whitespace/newlines). It also only covers javascript: and not other dangerous schemes (e.g., data:, vbscript:). Prefer a scheme allowlist via react-markdown’s URL transform hook or by parsing the URL and validating protocol after normalization, and render the link text safely when disallowed.

Suggested change
const markdownComponents = {
// eslint-disable-next-line id-length
em: ({ node: _node, ...props }) => <i className="text-code" {...props} />,
// eslint-disable-next-line id-length
a: ({ node: _node, href, children, ...props }) => {
// eslint-disable-next-line no-script-url
if (href && href.startsWith("javascript:")) {
return null;
const SAFE_URL_PROTOCOLS = ["http:", "https:", "mailto:", "tel:"];
const isSafeHref = (href) => {
if (!href) {
return false;
}
const trimmedHref = href.trim();
// Allow relative links and in-page anchors.
if (trimmedHref.startsWith("/") || trimmedHref.startsWith("#")) {
return true;
}
try {
const url = new URL(trimmedHref, window.location.origin);
return SAFE_URL_PROTOCOLS.includes(url.protocol);
} catch (_e) {
// If parsing fails, treat as unsafe.
return false;
}
};
const markdownComponents = {
// eslint-disable-next-line id-length
em: ({ node: _node, ...props }) => <i className="text-code" {...props} />,
// eslint-disable-next-line id-length
a: ({ node: _node, href, children, ...props }) => {
if (!isSafeHref(href)) {
// Render the text but do not make it a clickable link.
return (
<span className="link-primary" {...props}>
{children}
</span>
);

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +37
const markdownComponents = {
// eslint-disable-next-line id-length
em: ({ node: _node, ...props }) => <i className="text-code" {...props} />,
// eslint-disable-next-line id-length
a: ({ node: _node, href, children, ...props }) => {
// eslint-disable-next-line no-script-url
if (href && href.startsWith("javascript:")) {
return null;
}
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="link-primary"
{...props}
>
{children}
</a>
);
},
};
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

This file introduces a custom markdownComponents implementation, but there is already a shared helper at frontend/src/components/common/markdownToHtml.jsx that wraps react-markdown and is used in multiple places (e.g., components/common/form/pluginsMultiSelectDropdownInput.jsx, components/jobs/result/pluginReportTables.jsx). Consider moving the security/link-hardening changes into the shared helper (or extracting shared markdown components) and reusing it here to avoid duplicated/inconsistent markdown behavior across the UI.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Already explained in the description.

Comment on lines 101 to 126
test("prevents javascript: protocol in links", () => {
const notifications = [
{
id: 4,
title: "JS Protocol Test",
body: '[Click me](javascript:alert("XSS"))',
read: false,
created_at: "2023-01-01T00:00:00Z",
},
];

render(
<NotificationsList
notifications={notifications}
refetchFn={mockRefetch}
/>,
);

const link = screen.queryByRole("link");
if (link) {
const href = link.getAttribute("href");
if (href) {
expect(href).not.toContain("javascript:");
}
}
});
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

The javascript: protocol test can produce false positives because it passes if no link is rendered at all (e.g., parsing changes), which wouldn’t actually validate sanitization. Make the assertion deterministic by first asserting the link text is present and then asserting either (a) no anchor is rendered for it, or (b) the rendered anchor’s href is transformed to a safe value.

Copilot uses AI. Check for mistakes.
Comment on lines 219 to 223
const link = screen.getByRole("link", { name: "Google" });
expect(link).toHaveAttribute("href", "https://google.com");
expect(link).toHaveAttribute("target", "_blank");
expect(link).toHaveClass("link-primary");
});
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

The link rendering test asserts target="_blank" but doesn’t verify the security requirement from the PR description (rel="noopener noreferrer"). Add an assertion for the rel attribute so regressions don’t reintroduce tabnabbing risk.

Copilot uses AI. Check for mistakes.
@IshaanXCoder
Copy link
Contributor Author

Hey @fgibertoni @mlodic PTAL at this :)

@mlodic
Copy link
Member

mlodic commented Jan 28, 2026

can you also show us a brief video that this actually works in the GUI?

@IshaanXCoder
Copy link
Contributor Author

can you also show us a brief video that this actually works in the GUI?

Hey @mlodic apologies for the delay, hope this video explains it clearly. (Before then after)

Screen.Recording.2026-01-29.at.5.58.56.PM.mov

@mlodic mlodic merged commit c2bd833 into intelowlproject:develop Jan 29, 2026
9 checks passed
@mlodic
Copy link
Member

mlodic commented Jan 29, 2026

thanks, very helpful!

@mlodic mlodic mentioned this pull request Jan 29, 2026
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.

3 participants