🩹 fix: relax connect-src to allow sourcemap fetches (silence CSP noise)

Manual e2e on PR #12934: every time DevTools is open while viewing a
DOCX or PPTX preview, the console fills with CSP violations like:

  Connecting to 'https://cdn.jsdelivr.net/npm/docx-preview@0.3.7/
  dist/docx-preview.min.js.map' violates the following Content
  Security Policy directive: "connect-src 'none'". The request has
  been blocked.

The actual rendering isn't affected (sourcemap fetches happen AFTER
the script has already loaded and executed via `script-src`), but
the noise is enough to make people suspect a real problem and
distracts from useful console output.

Fix: relax `connect-src` from `'none'` to `'self' https://cdn.
jsdelivr.net` in both DOCX and PPTX CDN docs. This allows:
  - Same-origin fetches (sandpack-static-server) — covers any
    bundler-embedded sourcemaps + same-origin runtime fetches the
    renderer might make
  - jsdelivr fetches — covers sourcemaps from the CDN where we
    loaded the script

Exfiltration risk stays minimal: the iframe is cross-origin to
LibreChat so an attacker can't read application data anyway, and
neither 'self' (sandpack-static-server) nor jsdelivr is a useful
target for exfiltrating slide content to a host the attacker
controls.

Tests updated: assertions for `connect-src 'none'` swapped to
`connect-src 'self' https://cdn.jsdelivr.net` for both DOCX + PPTX
CDN docs. Added negative assertion for wildcard `*` in connect-src
so a future "let's allow everything" rewrite can't widen the
exfiltration surface.
This commit is contained in:
Danny Avila 2026-05-04 20:12:29 -04:00
parent 78642bc6bb
commit 41cc527211
2 changed files with 45 additions and 8 deletions

View file

@ -123,10 +123,15 @@ describe('Office HTML producers', () => {
);
expect(cspMatch).not.toBeNull();
const csp = cspMatch![1];
// The bomb defense: no outbound HTTP requests from the rendered
// iframe — a parser bug in docx-preview can't be turned into
// exfiltration of the embedded document content.
expect(csp).toMatch(/connect-src 'none'/);
/* `connect-src` is restricted to self + jsdelivr (where the
* renderer script came from). Allowing these is necessary for
* DevTools sourcemap fetches; broader exfiltration paths
* (arbitrary HTTPS, websockets, etc.) stay blocked. Manual
* e2e on PR #12934 — strict `'none'` filled DevTools console
* with `.min.js.map` violations every time the iframe was
* inspected. */
expect(csp).toMatch(/connect-src 'self' https:\/\/cdn\.jsdelivr\.net/);
expect(csp).not.toMatch(/connect-src[^;]*\*/); // no wildcard
// No `<base>` tampering, no form submission either.
expect(csp).toMatch(/base-uri 'none'/);
expect(csp).toMatch(/form-action 'none'/);
@ -438,7 +443,7 @@ describe('Office HTML producers', () => {
expect(html).toContain('crossorigin="anonymous"');
});
test('CSP locks the iframe down: connect-src none, no eval, no base/form tampering', async () => {
test('CSP locks the iframe down: connect-src restricted, no eval, no base/form tampering', async () => {
const pptx = await buildPptx([{ title: 'X' }]);
const html = await _internal.pptxToHtmlViaCdn(pptx);
const cspMatch = html.match(
@ -446,7 +451,14 @@ describe('Office HTML producers', () => {
);
expect(cspMatch).not.toBeNull();
const csp = cspMatch![1];
expect(csp).toMatch(/connect-src 'none'/);
/* `connect-src` allows self + jsdelivr (the script's origin)
* so DevTools sourcemap fetches succeed and any same-origin
* fetches the renderer makes at runtime work. Wildcard
* exfiltration paths stay blocked. Manual e2e on PR #12934 —
* strict `'none'` filled DevTools console with `.min.js.map`
* violations every time the iframe was inspected. */
expect(csp).toMatch(/connect-src 'self' https:\/\/cdn\.jsdelivr\.net/);
expect(csp).not.toMatch(/connect-src[^;]*\*/); // no wildcard
expect(csp).toMatch(/base-uri 'none'/);
expect(csp).toMatch(/form-action 'none'/);
expect(csp).toMatch(/script-src https:\/\/cdn\.jsdelivr\.net 'unsafe-inline'/);

View file

@ -378,13 +378,25 @@ const OFFICE_HTML_OUTPUT_CAP = 512 * 1024;
* styles into `<head>` at render time).
*/
function buildDocxCdnDocument(base64: string, mammothFallbackHtml: string): string {
/* `connect-src` allows fetches to:
* - `'self'`: the sandpack-static-server origin the iframe runs in
* (covers any same-origin sourcemap fetches the bundler embedded)
* - `https://cdn.jsdelivr.net`: where the renderer script came from
* (DevTools fetches `.min.js.map` from the same host as the script
* to map minified line numbers; with `'none'` the console fills
* with CSP violations every time DevTools is open)
*
* Exfiltration risk is minimal: the iframe is cross-origin to the
* LibreChat host so an attacker can't read application data from it,
* and the only meaningful target ('self' or jsdelivr) isn't useful
* for exfiltrating slide content to a host the attacker controls. */
const csp = [
"default-src 'none'",
"script-src https://cdn.jsdelivr.net 'unsafe-inline'",
"style-src 'unsafe-inline'",
"img-src 'self' data: blob:",
'font-src data:',
"connect-src 'none'",
"connect-src 'self' https://cdn.jsdelivr.net",
"base-uri 'none'",
"form-action 'none'",
].join('; ');
@ -1061,6 +1073,19 @@ function buildPptxCdnDocument(base64: string): string {
* visible symptom is a black iframe with the renderer half-
* started. Allowing blob:-only workers is the minimum surface to
* unblock rendering without permitting arbitrary worker URLs. */
/* `connect-src` allows fetches to:
* - `'self'`: the sandpack-static-server origin the iframe runs in
* (covers any same-origin fetches pptx-preview or its bundled
* echarts make at render time, plus same-origin sourcemap loads)
* - `https://cdn.jsdelivr.net`: where the renderer script came from
* (DevTools fetches `.min.js.map` from there to map minified
* line numbers; with `'none'` the console fills with CSP
* violations every time DevTools is open)
*
* Exfiltration risk is minimal: the iframe is cross-origin to the
* LibreChat host so an attacker can't read application data, and
* the only meaningful targets ('self' or jsdelivr) aren't useful
* for exfiltrating slide content to a host the attacker controls. */
const csp = [
"default-src 'none'",
"script-src https://cdn.jsdelivr.net 'unsafe-inline'",
@ -1068,7 +1093,7 @@ function buildPptxCdnDocument(base64: string): string {
'worker-src blob:',
"img-src 'self' data: blob:",
'font-src data:',
"connect-src 'none'",
"connect-src 'self' https://cdn.jsdelivr.net",
"base-uri 'none'",
"form-action 'none'",
].join('; ');