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:
+1
-1
@@ -1,5 +1,5 @@
|
||||
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 {
|
||||
|
||||
+22
-2
@@ -264,11 +264,31 @@ record("llm_unconfigured_falls_back_gracefully", unmatchedReply.length > 0 && !/
|
||||
// other personas have written since last open.
|
||||
await p.locator(".tab", { hasText: /^\s*Chat\s*$/ }).click();
|
||||
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 previewRows = await p.locator(".chat-thread-meta").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(
|
||||
"chat_sidebar_shows_previews_and_timestamps",
|
||||
sidebarHonest,
|
||||
|
||||
Reference in New Issue
Block a user