diff --git a/packages/api/src/files/documents/html.spec.ts b/packages/api/src/files/documents/html.spec.ts index ea9a81be57..5989952f5f 100644 --- a/packages/api/src/files/documents/html.spec.ts +++ b/packages/api/src/files/documents/html.spec.ts @@ -6,6 +6,7 @@ import { csvToHtml, excelSheetToHtml, pptxToSlideListHtml, + sanitizeOfficeHtml, wordDocToHtml, } from './html'; @@ -217,4 +218,70 @@ describe('Office HTML producers', () => { expect(html!).toContain('lc-docx'); }); }); + + describe('sanitizeOfficeHtml security', () => { + test('strips after

'); + expect(out).not.toMatch(/ { + const out = await sanitizeOfficeHtml(''); + expect(out).not.toMatch(/onerror=/i); + expect(out).toContain('https://x.test/a.png'); + }); + + test('strips javascript: URLs from anchors', async () => { + const out = await sanitizeOfficeHtml('click'); + expect(out).not.toMatch(/javascript:/i); + // The text survives even when the href is dropped. + expect(out).toContain('click'); + }); + + test('rejects data: URLs in (only may use data:)', async () => { + /* The Sandpack iframe sandbox does NOT gate `target="_blank"` + * navigations. A surviving `` would + * open attacker-controlled HTML in a new tab on click. The + * sanitizer scopes `data:` to only — global schemes are + * http/https/mailto. Regression guard for the Codex review on + * PR #12934. */ + const out = await sanitizeOfficeHtml( + 'click', + ); + expect(out).not.toContain('data:'); + expect(out).not.toContain('script'); + expect(out).toContain('click'); + }); + + test('preserves data: URLs in (mammoth inlines DOCX images as base64)', async () => { + const tinyPng = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='; + const out = await sanitizeOfficeHtml(`dot`); + expect(out).toContain(tinyPng); + expect(out).toContain('alt="dot"'); + }); + + test('preserves http(s) and mailto links on anchors', async () => { + const out = await sanitizeOfficeHtml( + 'site mail', + ); + expect(out).toContain('https://example.com'); + expect(out).toContain('mailto:a@b.test'); + }); + + test('forces target=_blank rel=noopener on surviving anchors', async () => { + const out = await sanitizeOfficeHtml('link'); + expect(out).toContain('target="_blank"'); + expect(out).toContain('rel="noopener noreferrer"'); + }); + + test('strips '); + expect(out).not.toMatch(/ { +export async function sanitizeOfficeHtml(html: string): Promise { const sanitizeHtml = await getSanitizer(); return sanitizeHtml(html, { allowedTags: [ @@ -121,7 +121,14 @@ async function sanitizeOfficeHtml(html: string): Promise { h5: ['id'], h6: ['id'], }, - allowedSchemes: ['http', 'https', 'data', 'mailto'], + /* `data:` is intentionally NOT in the global scheme list — it's only + * allowed for `` (mammoth inlines DOCX images as base64 + * `data:image/...` URIs). Without this scoping, an `` smuggled through DOCX/PPTX would + * survive sanitization and open attacker-controlled HTML in a new + * tab when clicked — the Sandpack iframe sandbox doesn't gate + * `target="_blank"` navigations. */ + allowedSchemes: ['http', 'https', 'mailto'], allowedSchemesByTag: { img: ['http', 'https', 'data'], },