fix(security): strip video embeds from rich text linkification

This commit is contained in:
2026-11-04 10:51:56 +00:00
parent 500a30e95b
commit d9f0c0797d
2 changed files with 82 additions and 1 deletions
+5 -1
View File
@@ -687,6 +687,8 @@ export const updateLinks = (input) => {
const EMPTY_QUILL_PARAGRAPHS_REGEX =
/<p[^>]*>\s*(?:<br\s*\/?>|&nbsp;|\s)*\s*<\/p>/gi;
const VIDEO_EMBED_REGEX = /<(iframe|video)\b[^>]*>[\s\S]*?<\/\1\s*>/gi;
const URL_REGEX = /\b(https?:\/\/[^\s<]+)/gi;
const stripTrailingUrlPunctuation = (url) => {
@@ -732,7 +734,9 @@ export const updateLinks = (input) => {
const processHtmlString = (htmlString) => {
if (typeof htmlString !== "string" || !htmlString) return htmlString;
let html = htmlString.replace(EMPTY_QUILL_PARAGRAPHS_REGEX, "");
let html = htmlString
.replace(VIDEO_EMBED_REGEX, "")
.replace(EMPTY_QUILL_PARAGRAPHS_REGEX, "");
const anchorPlaceholders = [];
html = html.replace(/<a\b[^>]*>[\s\S]*?<\/a>/gi, (match) => {
@@ -219,6 +219,83 @@ test("clients/fileRouteBuilder builds query with optional encoding and suffix he
);
});
const loadUpdateLinks = () => {
const filePath = path.join(rootDir, "components", "utils", "index.js");
const source = fs.readFileSync(filePath, "utf8");
const start = source.indexOf("export const updateLinks =");
const end = source.indexOf("\nexport const whichRepType", start);
assert.ok(start >= 0, "updateLinks export not found");
assert.ok(end > start, "updateLinks boundary not found");
const implementation = source
.slice(start, end)
.replace("export const updateLinks =", "const updateLinks =");
const context = { module: { exports: {} } };
vm.runInNewContext(
`${implementation}\nmodule.exports = updateLinks;`,
context,
{ filename: filePath }
);
return context.module.exports;
};
test("updateLinks removes Quill video iframe", () => {
const updateLinks = loadUpdateLinks();
const input =
'<p>Before</p><iframe class="ql-video" src="https://example.com/video"></iframe><p>After</p>';
assert.strictEqual(updateLinks(input), "<p>Before</p><p>After</p>");
});
test("updateLinks removes HTML video elements", () => {
const updateLinks = loadUpdateLinks();
const input =
'<p>Before</p><video controls src="https://example.com/video"><source src="https://example.com/file.mp4"></video><p>After</p>';
assert.strictEqual(updateLinks(input), "<p>Before</p><p>After</p>");
});
test("updateLinks converts plain URLs", () => {
const updateLinks = loadUpdateLinks();
assert.strictEqual(
updateLinks("<p>Visit https://example.com.</p>"),
'<p>Visit <a href="https://example.com" target="_blank" rel="noopener noreferrer">https://example.com</a>.</p>'
);
});
test("updateLinks preserves existing anchors", () => {
const updateLinks = loadUpdateLinks();
const input = '<p><a href="https://example.com">Existing link</a></p>';
assert.strictEqual(updateLinks(input), input);
});
test("updateLinks preserves standard rich-text formatting", () => {
const updateLinks = loadUpdateLinks();
const input = "<p><strong>Bold</strong> and <em>italic</em></p>";
assert.strictEqual(updateLinks(input), input);
});
test("updateLinks removes empty Quill paragraphs", () => {
const updateLinks = loadUpdateLinks();
assert.strictEqual(
updateLinks("<p>Content</p><p><br></p><p>&nbsp;</p>"),
"<p>Content</p>"
);
});
const run = async () => {
let passed = 0;