diff --git a/cypress/config/settings.cypress.json b/cypress/config/settings.cypress.json
index 45e38a29e..8e529c3d3 100644
--- a/cypress/config/settings.cypress.json
+++ b/cypress/config/settings.cypress.json
@@ -7,6 +7,7 @@
"applicationTitle": "Overseerr",
"applicationUrl": "",
"csrfProtection": false,
+ "cspFrameAncestorDomains": "",
"cacheImages": false,
"defaultPermissions": 32,
"defaultQuotas": {
diff --git a/overseerr-api.yml b/overseerr-api.yml
index 96a4520a7..dafb7602f 100644
--- a/overseerr-api.yml
+++ b/overseerr-api.yml
@@ -168,6 +168,9 @@ components:
csrfProtection:
type: boolean
example: false
+ cspFrameAncestorDomains:
+ type: string
+ example: 'example.com'
hideAvailable:
type: boolean
example: false
diff --git a/package.json b/package.json
index 9ce9330f8..fd24e7b1f 100644
--- a/package.json
+++ b/package.json
@@ -61,6 +61,7 @@
"express-session": "1.17.3",
"formik": "^2.4.6",
"gravatar-url": "3.1.0",
+ "helmet": "^7.1.0",
"lodash": "4.17.21",
"mime": "3",
"next": "^14.2.4",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7391a775a..4b7dfcfe5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -95,6 +95,9 @@ importers:
gravatar-url:
specifier: 3.1.0
version: 3.1.0
+ helmet:
+ specifier: ^7.1.0
+ version: 7.1.0
lodash:
specifier: 4.17.21
version: 4.17.21
@@ -5292,6 +5295,10 @@ packages:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
+ helmet@7.1.0:
+ resolution: {integrity: sha512-g+HZqgfbpXdCkme/Cd/mZkV0aV3BZZZSugecH03kl38m/Kmdx8jKjBikpDj2cr+Iynv4KpYEviojNdTJActJAg==}
+ engines: {node: '>=16.0.0'}
+
hermes-estree@0.19.1:
resolution: {integrity: sha512-daLGV3Q2MKk8w4evNMKwS8zBE/rcpA800nu1Q5kM08IKijoSnPe9Uo1iIxzPKRkn95IxxsgBMPeYHt3VG4ej2g==}
@@ -15595,6 +15602,8 @@ snapshots:
he@1.2.0: {}
+ helmet@7.1.0: {}
+
hermes-estree@0.19.1: {}
hermes-estree@0.20.1: {}
diff --git a/server/index.ts b/server/index.ts
index 4ccc6fed1..2865eeb2f 100644
--- a/server/index.ts
+++ b/server/index.ts
@@ -32,6 +32,7 @@ import express from 'express';
import * as OpenApiValidator from 'express-openapi-validator';
import type { Store } from 'express-session';
import session from 'express-session';
+import helmet from 'helmet';
import next from 'next';
import dns from 'node:dns';
import net from 'node:net';
@@ -159,6 +160,23 @@ app
});
}
+ // Setup Content-Security-Policy
+ server.use(
+ helmet.contentSecurityPolicy({
+ useDefaults: false,
+ directives: {
+ 'default-src':
+ helmet.contentSecurityPolicy.dangerouslyDisableDefaultSrc,
+ 'frame-ancestors': [
+ "'self'",
+ ...(settings.main.cspFrameAncestorDomains
+ ? [settings.main.cspFrameAncestorDomains]
+ : []),
+ ],
+ },
+ })
+ );
+
// Set up sessions
const sessionRespository = getRepository(Session);
server.use(
@@ -170,7 +188,11 @@ app
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 30,
httpOnly: true,
- sameSite: settings.main.csrfProtection ? 'strict' : 'lax',
+ sameSite: settings.main.csrfProtection
+ ? 'strict'
+ : settings.main.cspFrameAncestorDomains
+ ? 'none'
+ : 'lax',
secure: 'auto',
},
store: new TypeormStore({
diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts
index 074a4fcdc..6f7e7e1f5 100644
--- a/server/lib/settings/index.ts
+++ b/server/lib/settings/index.ts
@@ -104,6 +104,7 @@ export interface MainSettings {
applicationTitle: string;
applicationUrl: string;
csrfProtection: boolean;
+ cspFrameAncestorDomains: string;
cacheImages: boolean;
defaultPermissions: number;
defaultQuotas: {
@@ -310,6 +311,7 @@ class Settings {
applicationTitle: 'Jellyseerr',
applicationUrl: '',
csrfProtection: false,
+ cspFrameAncestorDomains: '',
cacheImages: false,
defaultPermissions: Permission.REQUEST,
defaultQuotas: {
diff --git a/server/utils/restartFlag.ts b/server/utils/restartFlag.ts
index 387ec5ce4..e2f52ebeb 100644
--- a/server/utils/restartFlag.ts
+++ b/server/utils/restartFlag.ts
@@ -13,7 +13,8 @@ class RestartFlag {
return (
this.settings.csrfProtection !== settings.csrfProtection ||
- this.settings.trustProxy !== settings.trustProxy
+ this.settings.trustProxy !== settings.trustProxy ||
+ this.settings.cspFrameAncestorDomains !== settings.cspFrameAncestorDomains
);
}
}
diff --git a/src/components/Settings/SettingsMain/index.tsx b/src/components/Settings/SettingsMain/index.tsx
index f7aac0d96..b010dcf01 100644
--- a/src/components/Settings/SettingsMain/index.tsx
+++ b/src/components/Settings/SettingsMain/index.tsx
@@ -44,6 +44,9 @@ const messages = defineMessages('components.Settings.SettingsMain', {
csrfProtectionTip: 'Set external API access to read-only (requires HTTPS)',
csrfProtectionHoverTip:
'Do NOT enable this setting unless you understand what you are doing!',
+ cspFrameAncestorDomains: 'Frame-Ancestor Domains',
+ cspFrameAncestorDomainsTip:
+ 'Domains to allow embedding Jellyseer as iframe, object or embed. Incompatible with CSRF-Protection',
cacheImages: 'Enable Image Caching',
cacheImagesTip:
'Cache externally sourced images (requires a significant amount of disk space)',
@@ -130,6 +133,7 @@ const SettingsMain = () => {
applicationTitle: data?.applicationTitle,
applicationUrl: data?.applicationUrl,
csrfProtection: data?.csrfProtection,
+ cspFrameAncestorDomains: data?.cspFrameAncestorDomains,
hideAvailable: data?.hideAvailable,
locale: data?.locale ?? 'en',
region: data?.region,
@@ -151,6 +155,7 @@ const SettingsMain = () => {
applicationTitle: values.applicationTitle,
applicationUrl: values.applicationUrl,
csrfProtection: values.csrfProtection,
+ cspFrameAncestorDomains: values.cspFrameAncestorDomains,
hideAvailable: values.hideAvailable,
locale: values.locale,
region: values.region,
@@ -318,6 +323,31 @@ const SettingsMain = () => {
+
+
+
+