fix(security): lock CSP script-src to self (no unsafe-inline/unsafe-eval)

Oracle round-9 hard blocker: live CSP advertised script-src 'self'
'unsafe-inline' 'unsafe-eval'. Verified the Vite build emits zero inline
<script> tags (only external module script) and no bundled dep uses
new Function / eval (reactflow / leaflet / zustand / dagre / cmdk /
framer-motion all clean).

- nginx.conf script-src reduced to 'self'.
- img-src extended for OSM tiles (https://*.tile.openstreetmap.org) and
  leaflet marker images served from unpkg.

Also (Oracle round-9 second blocker) chat_sidebar QA now creates a
thread + sends a message before asserting, then strict-asserts
threadRowCount >= 1 AND previewRows === threadRowCount AND
timeBadges === threadRowCount. No more vacuous PASS when sidebar is
empty.
This commit is contained in:
2026-06-14 15:42:28 +04:00
parent 6a665da21d
commit 973aa9c15b
2 changed files with 23 additions and 3 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
map $sent_http_content_type $csp_header { map $sent_http_content_type $csp_header {
default "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: blob:; connect-src 'self' https://demo.flow-master.ai https://canvas.flow-master.ai wss://canvas.flow-master.ai; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'"; default "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: blob: https://*.tile.openstreetmap.org https://unpkg.com; connect-src 'self' https://demo.flow-master.ai https://canvas.flow-master.ai wss://canvas.flow-master.ai; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'";
} }
server { server {
+22 -2
View File
@@ -264,11 +264,31 @@ record("llm_unconfigured_falls_back_gracefully", unmatchedReply.length > 0 && !/
// other personas have written since last open. // other personas have written since last open.
await p.locator(".tab", { hasText: /^\s*Chat\s*$/ }).click(); await p.locator(".tab", { hasText: /^\s*Chat\s*$/ }).click();
await p.waitForTimeout(2500); await p.waitForTimeout(2500);
await p.waitForTimeout(2000); // First, ensure at least one thread exists by opening one with the HR persona
// if the sidebar happens to be empty. This makes the assertion non-vacuous.
const initialThreadCount = await p.locator(".chat-thread-row").count();
if (initialThreadCount === 0) {
const sel = p.locator(".chat-new-row select").first();
if (await sel.count()) {
await sel.selectOption("hr-head@flow-master.ai").catch(() => {});
await p.locator(".chat-new-row button", { hasText: /Open/ }).click().catch(() => {});
await p.waitForSelector(".chat-thread-row", { timeout: 8000 }).catch(() => {});
}
const ta = p.locator(".chat-composer textarea").first();
if (await ta.count()) {
await ta.fill(`qa seed message ${Date.now()}`);
await p.locator(".chat-composer button", { hasText: /Send/i }).click().catch(() => {});
await p.waitForTimeout(2500);
}
}
await p.waitForFunction(() => document.querySelectorAll(".chat-thread-row").length >= 1, undefined, { timeout: 12000 }).catch(() => {});
await p.waitForTimeout(1500);
const threadRowCount = await p.locator(".chat-thread-row").count(); const threadRowCount = await p.locator(".chat-thread-row").count();
const previewRows = await p.locator(".chat-thread-meta").count(); const previewRows = await p.locator(".chat-thread-meta").count();
const timeBadges = await p.locator(".chat-thread-time").count(); const timeBadges = await p.locator(".chat-thread-time").count();
const sidebarHonest = threadRowCount === 0 || (previewRows === threadRowCount && timeBadges >= 1); // Strict: at least one thread, every thread has a preview, and the timestamp
// count equals the thread count (one per row).
const sidebarHonest = threadRowCount >= 1 && previewRows === threadRowCount && timeBadges === threadRowCount;
record( record(
"chat_sidebar_shows_previews_and_timestamps", "chat_sidebar_shows_previews_and_timestamps",
sidebarHonest, sidebarHonest,