Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/components/chat/message-bubble.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,29 @@ describe("MessageBubble Link Sanitization", () => {
expect(links[1].getAttribute("href")).toBe("#");
expect(links[2].getAttribute("href")).toBe("#");
});

it("should sanitize obfuscated protocols and non-allowlisted schemes to #", () => {
const message = {
id: "3",
role: "assistant",
content:
"Obfuscated: [Tab XSS](<java\tscript:alert(1)>), [Blob](blob:text/html,test), and [File](file:///etc/passwd).",
parts: [
{
type: "text",
text: "Obfuscated: [Tab XSS](<java\tscript:alert(1)>), [Blob](blob:text/html,test), and [File](file:///etc/passwd).",
},
],
} as any;

const { container } = render(
<MessageBubble {...defaultProps} message={message} />,
);

const links = container.querySelectorAll("a");
expect(links.length).toBe(3);
expect(links[0].getAttribute("href")).toBe("#");
expect(links[1].getAttribute("href")).toBe("#");
expect(links[2].getAttribute("href")).toBe("#");
});
});
17 changes: 15 additions & 2 deletions src/components/chat/message-bubble.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,24 @@ const selectElementText = (target: HTMLElement) => {
selection?.addRange(range);
};

const isSafeUrl = (href?: string): boolean => {
if (!href) return false;
// Normalize URL by stripping control characters and whitespace before protocol validation
/* eslint-disable-next-line no-control-regex */
const controlChars = /[\u0000-\u0020\u007F-\u009F]/g;
const normalized = href.replace(controlChars, "");
return (
normalized.startsWith("#") ||
normalized.startsWith("/") ||
/^(https?|mailto|tel):/i.test(normalized)
);
};

const markdownComponents = {
a: ({ href, children }: { href?: string; children?: React.ReactNode }) => {
const isHash = href?.startsWith("#");
// Prevent prompt injection leading to XSS via javascript:, data:, or vbscript: protocols
const isSafe = href && !/^(javascript|data|vbscript):/i.test(href.trim());
// Prevent prompt injection leading to XSS by enforcing a strict protocol allowlist
const isSafe = isSafeUrl(href);
const isExternal = !isHash;
return (
<a
Expand Down
Loading