From 1f03796e0335b20a743d8aff3bd5e05362b493ee Mon Sep 17 00:00:00 2001 From: Shaurya K Sharma Date: Fri, 9 Oct 2026 00:06:33 +0100 Subject: [PATCH 1/2] fix(privacy): bundle editor code and remove external font loading (#543) --- apps/backend/app/core/security_headers.py | 10 ++++---- apps/backend/tests/test_security_headers.py | 9 ++++++++ apps/frontend/e2e/local-editor.spec.ts | 23 +++++++++++++++++++ apps/frontend/index.html | 3 --- apps/frontend/package-lock.json | 7 ++---- apps/frontend/package.json | 1 + .../explorer/components/CodePreview.tsx | 1 + .../components/RepositoryExplorer.test.tsx | 2 ++ .../explorer/components/localEditor.ts | 6 +++++ 9 files changed, 48 insertions(+), 14 deletions(-) create mode 100644 apps/frontend/e2e/local-editor.spec.ts create mode 100644 apps/frontend/src/features/explorer/components/localEditor.ts diff --git a/apps/backend/app/core/security_headers.py b/apps/backend/app/core/security_headers.py index 4bfb4a75..445e125d 100644 --- a/apps/backend/app/core/security_headers.py +++ b/apps/backend/app/core/security_headers.py @@ -63,9 +63,8 @@ def frontend_content_security_policy(index_html: str) -> str: The API's deny-all policy above blocks every script and stylesheet the page needs, so the shell gets its own. Inline scripts in index.html are allowed by hash, computed from the built file itself so editing that script can never - silently break the page. Third-party origins are exactly the ones the - frontend loads today: Google Fonts, and the Monaco editor from jsDelivr, - which runs its language workers from blob: URLs. Inline styles are allowed + silently break the page. Editor scripts and workers are bundled locally. Fonts use local system + fallbacks. No third-party font or executable CDN origin is permitted. Inline styles are allowed because the graph and editor libraries inject their own. """ @@ -75,13 +74,12 @@ def frontend_content_security_policy(index_html: str) -> str: "'sha256-" + base64.b64encode(hashlib.sha256(body.encode("utf-8")).digest()).decode() + "'" for body in _inline_scripts(index_html) ), - "https://cdn.jsdelivr.net", ] directives = [ "default-src 'self'", "script-src " + " ".join(script_sources), - "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net", - "font-src 'self' data: https://fonts.gstatic.com https://cdn.jsdelivr.net", + "style-src 'self' 'unsafe-inline'", + "font-src 'self' data:", "img-src 'self' data: blob:", "worker-src 'self' blob:", "connect-src 'self'", diff --git a/apps/backend/tests/test_security_headers.py b/apps/backend/tests/test_security_headers.py index ba125c2f..c2a1c4f3 100644 --- a/apps/backend/tests/test_security_headers.py +++ b/apps/backend/tests/test_security_headers.py @@ -75,3 +75,12 @@ def source(body: str) -> str: assert source("\n b();\n") in script_src # An external script is allowed by 'self', not by a hash of its empty body. assert source("") not in script_src + + +def test_frontend_policy_excludes_executable_and_font_cdns(): + from app.core.security_headers import frontend_content_security_policy + + policy = frontend_content_security_policy("") + assert "https://" not in policy + assert "worker-src 'self' blob:" in policy + assert "style-src 'self' 'unsafe-inline'" in policy diff --git a/apps/frontend/e2e/local-editor.spec.ts b/apps/frontend/e2e/local-editor.spec.ts new file mode 100644 index 00000000..d624778a --- /dev/null +++ b/apps/frontend/e2e/local-editor.spec.ts @@ -0,0 +1,23 @@ +import { readFileSync } from 'node:fs'; +import { expect, test } from '@playwright/test'; + +const fixtures = JSON.parse(readFileSync(process.env.PARTHA_VISUAL_FIXTURES ?? '/tmp/partha-e2e-fixtures.json', 'utf8')); + +test('source explorer renders Monaco without third-party executable or font requests', async ({ page }, testInfo) => { + const external: string[] = []; + page.on('request', request => { + const url = new URL(request.url()); + if (url.protocol.startsWith('http') && !['localhost', '127.0.0.1'].includes(url.hostname)) external.push(request.url()); + }); + await page.goto('/login'); + await page.getByLabel(/email/i).fill(fixtures.email); + await page.getByLabel('Password', {exact:true}).fill(fixtures.password); + await page.getByRole('button', {name:/sign in|log ?in/i}).click(); + await expect(page.getByRole('heading', {level:1})).toBeVisible(); + const repository = fixtures.repos.find((repo: {label:string}) => repo.label === 'small'); + await page.goto(`/repositories/${repository.id}?tab=Explorer&path=src/api/routes.ts`); + await expect(page.locator('.monaco-editor')).toBeVisible({timeout:20000}); + await expect(page.locator('.monaco-editor .view-lines')).toContainText('export'); + expect(external).toEqual([]); + await testInfo.attach('local-editor', {body:await page.screenshot(),contentType:'image/png'}); +}); diff --git a/apps/frontend/index.html b/apps/frontend/index.html index 94728612..5b833d1b 100644 --- a/apps/frontend/index.html +++ b/apps/frontend/index.html @@ -12,9 +12,6 @@ - - - PARTHA