mirror of
https://github.com/vxcontrol/pentagi.git
synced 2026-09-26 20:25:36 +00:00
fix(markdown-editor): guard the link toolbar against non-navigable protocols (M3)
handleSetLink passed window.prompt output straight to setLink with no protocol check, while the image toolbar already guards via isSafeImageSrc — so a user could toolbar-insert `javascript:alert(1)` as a link href into the persisted document. Not a live XSS today (the read-only viewer's react-markdown urlTransform and tiptap Link's isAllowedUri both sanitize on render), but defense-in-depth: keep dangerous protocols out of the document at authoring time so it never relies on every future render path sanitizing them. Add isSafeUrl (mirror of isSafeImageSrc): allow http/https/mailto/tel + relative/anchor (which resolve to the page protocol); reject javascript:/data:/vbscript:/file:/malformed. Apply it in handleSetLink. Unit-tested alongside isSafeImageSrc; 889 vitest green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
5eb4ea22ad
commit
587d8a0ecf
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { isSafeImageSrc } from './markdown-editor-toolbar';
|
||||
import { isSafeImageSrc, isSafeUrl } from './markdown-editor-toolbar';
|
||||
|
||||
// The Image extension stores whatever src it is handed; isSafeImageSrc is the allowlist that blocks dangerous
|
||||
// PROTOCOLS (javascript:, data:text/html, vbscript:, file:) before a src is saved. A bare relative string
|
||||
@@ -30,3 +30,27 @@ describe('isSafeImageSrc — image-src protocol allowlist', () => {
|
||||
expect(isSafeImageSrc(url)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSafeUrl — link-href protocol allowlist', () => {
|
||||
it.each([
|
||||
'https://example.com/path?a=1|2',
|
||||
'http://example.com',
|
||||
'mailto:a@b.com',
|
||||
'tel:+123',
|
||||
'/relative/path',
|
||||
'#anchor',
|
||||
'./sibling',
|
||||
])('allows %s', (url) => {
|
||||
expect(isSafeUrl(url)).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
'javascript:alert(1)',
|
||||
'data:text/html,<script>alert(1)</script>',
|
||||
'vbscript:msgbox(1)',
|
||||
'file:///etc/passwd',
|
||||
'http://', // malformed → rejected via the catch
|
||||
])('rejects %s', (url) => {
|
||||
expect(isSafeUrl(url)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,6 +48,20 @@ export const isSafeImageSrc = (url: string): boolean => {
|
||||
}
|
||||
};
|
||||
|
||||
// Link-toolbar counterpart of isSafeImageSrc: only navigable protocols (relative/anchor URLs resolve to the
|
||||
// page's http/https). Keeps `javascript:` / `data:` out of the persisted document at authoring time so it
|
||||
// never depends on every future render path sanitizing them. Load/paste bypass this (tiptap Link's
|
||||
// isAllowedUri + the read-only viewer sanitize protocols on render).
|
||||
const SAFE_LINK_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:']);
|
||||
|
||||
export const isSafeUrl = (url: string): boolean => {
|
||||
try {
|
||||
return SAFE_LINK_PROTOCOLS.has(new URL(url, window.location.href).protocol);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// memo: every keystroke re-renders the RHF-controlled parent; without it all ~20 Toggle subtrees re-render
|
||||
// per keystroke for referentially-stable props.
|
||||
export const MarkdownEditorToolbar = memo(function MarkdownEditorToolbar({
|
||||
@@ -68,6 +82,10 @@ export const MarkdownEditorToolbar = memo(function MarkdownEditorToolbar({
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isSafeUrl(url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
|
||||
}, [editor]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user