mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-10-22 01:53:33 +00:00
🔐 fix: Restrict data: URLs to <img> in office HTML sanitizer
Codex review on #12934 caught that `data:` lived in the global `allowedSchemes`, which meant a smuggled `<a href="data:text/html, <script>...</script>">` would survive sanitization. The Sandpack iframe sandbox does not gate `target="_blank"` navigations, so a click would open attacker-controlled HTML in a new tab. Scope `data:` to `<img src>` only via `allowedSchemesByTag` (mammoth inlines DOCX images as base64 `data:image/...` URIs — that path still works). Add a regression suite (`sanitizeOfficeHtml security`) with 8 cases covering: <script> stripping, event-handler removal, javascript:/data: rejection on anchors, data:image preservation in <img>, http/https/mailto allowance, target=_blank rel=noopener enforcement, and <iframe> stripping.
This commit is contained in:
parent
9ab4f040f3
commit
fb4ac84250
2 changed files with 76 additions and 2 deletions
|
|
@ -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 <script> tags entirely', async () => {
|
||||
const out = await sanitizeOfficeHtml('<p>before<script>alert(1)</script>after</p>');
|
||||
expect(out).not.toMatch(/<script\b/i);
|
||||
expect(out).not.toContain('alert(1)');
|
||||
expect(out).toContain('before');
|
||||
expect(out).toContain('after');
|
||||
});
|
||||
|
||||
test('drops event-handler attributes from surviving tags', async () => {
|
||||
const out = await sanitizeOfficeHtml('<img src="https://x.test/a.png" onerror="alert(1)">');
|
||||
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('<a href="javascript:alert(1)">click</a>');
|
||||
expect(out).not.toMatch(/javascript:/i);
|
||||
// The text survives even when the href is dropped.
|
||||
expect(out).toContain('click');
|
||||
});
|
||||
|
||||
test('rejects data: URLs in <a href> (only <img src> may use data:)', async () => {
|
||||
/* The Sandpack iframe sandbox does NOT gate `target="_blank"`
|
||||
* navigations. A surviving `<a href="data:text/html,...">` would
|
||||
* open attacker-controlled HTML in a new tab on click. The
|
||||
* sanitizer scopes `data:` to <img> only — global schemes are
|
||||
* http/https/mailto. Regression guard for the Codex review on
|
||||
* PR #12934. */
|
||||
const out = await sanitizeOfficeHtml(
|
||||
'<a href="data:text/html,<script>alert(1)</script>">click</a>',
|
||||
);
|
||||
expect(out).not.toContain('data:');
|
||||
expect(out).not.toContain('script');
|
||||
expect(out).toContain('click');
|
||||
});
|
||||
|
||||
test('preserves data: URLs in <img src> (mammoth inlines DOCX images as base64)', async () => {
|
||||
const tinyPng =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
|
||||
const out = await sanitizeOfficeHtml(`<img src="${tinyPng}" alt="dot">`);
|
||||
expect(out).toContain(tinyPng);
|
||||
expect(out).toContain('alt="dot"');
|
||||
});
|
||||
|
||||
test('preserves http(s) and mailto links on anchors', async () => {
|
||||
const out = await sanitizeOfficeHtml(
|
||||
'<a href="https://example.com">site</a> <a href="mailto:a@b.test">mail</a>',
|
||||
);
|
||||
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('<a href="https://example.com">link</a>');
|
||||
expect(out).toContain('target="_blank"');
|
||||
expect(out).toContain('rel="noopener noreferrer"');
|
||||
});
|
||||
|
||||
test('strips <iframe> entirely', async () => {
|
||||
const out = await sanitizeOfficeHtml('<p>x</p><iframe src="https://evil.test"></iframe>');
|
||||
expect(out).not.toMatch(/<iframe\b/i);
|
||||
expect(out).not.toContain('evil.test');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ function stripColorStyles(style: string | undefined): string | undefined {
|
|||
* The Sandpack iframe is itself a sandbox — this is defense in depth, not
|
||||
* the sole barrier.
|
||||
*/
|
||||
async function sanitizeOfficeHtml(html: string): Promise<string> {
|
||||
export async function sanitizeOfficeHtml(html: string): Promise<string> {
|
||||
const sanitizeHtml = await getSanitizer();
|
||||
return sanitizeHtml(html, {
|
||||
allowedTags: [
|
||||
|
|
@ -121,7 +121,14 @@ async function sanitizeOfficeHtml(html: string): Promise<string> {
|
|||
h5: ['id'],
|
||||
h6: ['id'],
|
||||
},
|
||||
allowedSchemes: ['http', 'https', 'data', 'mailto'],
|
||||
/* `data:` is intentionally NOT in the global scheme list — it's only
|
||||
* allowed for `<img src>` (mammoth inlines DOCX images as base64
|
||||
* `data:image/...` URIs). Without this scoping, an `<a href="data:
|
||||
* text/html,<script>…</script>">` 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'],
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue