mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: production CSP blocking PostHog/Sentry/Scalar and silent failure hardening
The production CSP had connect-src/script-src/font-src set to 'self' only, silently blocking all analytics and error reporting in production while working fine in dev (where CSP is not applied). CSP fixes: - Add PostHog ingest + assets origins to connect-src and script-src - Add Sentry ingest origin to connect-src - Add Scalar fonts origin to font-src for API docs pages - Extract CSP construction into testable buildCsp() function Silent failure hardening: - Settings/features stores now set loadError flag and allow retry on subsequent fetch() calls instead of permanently caching failed state - Analytics init no longer sets initialized=true before the try block, allowing retry on failure - Settings dialog Tools section disables save button when settings failed to load, preventing accidental config wipe - Branding logo storage moved from process.cwd() to FILES_STORAGE_PATH so logos persist across Docker container recreation Test coverage: - 16 CSP directive tests covering all external service domains - Store retry-on-error behavior tests for settings and features stores - Analytics init retry-after-failure test
This commit is contained in:
@@ -10,6 +10,7 @@ import { db, schema } from "./db/index.js";
|
||||
import { runMigrations } from "./db/migrate.js";
|
||||
import { captureException, initAnalytics, shutdownAnalytics } from "./lib/analytics.js";
|
||||
import { startCleanupCron } from "./lib/cleanup.js";
|
||||
import { buildCsp } from "./lib/csp.js";
|
||||
import { ensureAiDirs, recoverInterruptedInstalls } from "./lib/feature-status.js";
|
||||
import { shutdownWorkerPool } from "./lib/worker-pool.js";
|
||||
import { requirePermission } from "./permissions.js";
|
||||
@@ -127,10 +128,7 @@ app.addHook("onSend", async (_request, reply) => {
|
||||
reply.header("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
reply.header("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
||||
const csp = _request.url.startsWith("/api/docs")
|
||||
? "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data:; connect-src 'self'; font-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self'"
|
||||
: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data: https://tile.openstreetmap.org; connect-src 'self'; font-src 'self' data:; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'";
|
||||
reply.header("Content-Security-Policy", csp);
|
||||
reply.header("Content-Security-Policy", buildCsp(_request.url.startsWith("/api/docs")));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
const POSTHOG_ORIGINS = ["https://us.i.posthog.com", "https://us-assets.i.posthog.com"];
|
||||
const SENTRY_ORIGINS = ["https://*.ingest.us.sentry.io"];
|
||||
const SCALAR_FONT_ORIGIN = "https://fonts.scalar.com";
|
||||
|
||||
export function buildCsp(isDocs: boolean): string {
|
||||
const connectSrc = ["'self'", ...POSTHOG_ORIGINS, ...SENTRY_ORIGINS].join(" ");
|
||||
const fontSrc = isDocs ? `'self' data: ${SCALAR_FONT_ORIGIN}` : "'self' data:";
|
||||
const scriptSrc = isDocs
|
||||
? "'self' 'unsafe-inline' https://us-assets.i.posthog.com"
|
||||
: "'self' https://us-assets.i.posthog.com";
|
||||
|
||||
if (isDocs) {
|
||||
return `default-src 'self'; script-src ${scriptSrc}; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data:; connect-src ${connectSrc}; font-src ${fontSrc}; object-src 'none'; base-uri 'self'; form-action 'self'`;
|
||||
}
|
||||
|
||||
return `default-src 'self'; script-src ${scriptSrc}; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data: https://tile.openstreetmap.org; connect-src ${connectSrc}; font-src ${fontSrc}; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'`;
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import { db, schema } from "../db/index.js";
|
||||
import { ensureSharpCompat } from "../lib/heic-converter.js";
|
||||
import { requirePermission } from "../permissions.js";
|
||||
|
||||
const BRANDING_DIR = join(process.cwd(), "data", "branding");
|
||||
const BRANDING_DIR = join(env.FILES_STORAGE_PATH, "branding");
|
||||
const LOGO_PATH = join(BRANDING_DIR, "logo.png");
|
||||
const maxLogoSize = env.MAX_LOGO_SIZE_KB * 1024;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user