diff --git a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java index 74d6baed0..cabced160 100644 --- a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java +++ b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java @@ -164,6 +164,7 @@ public class ApplicationProperties { private String customGlobalAPIKey; private Jwt jwt = new Jwt(); private Validation validation = new Validation(); + private String xFrameOptions = "DENY"; public Boolean isAltLogin() { return saml2.getEnabled() || oauth2.getEnabled(); diff --git a/app/core/src/main/resources/settings.yml.template b/app/core/src/main/resources/settings.yml.template index c0664a395..8a952fa97 100644 --- a/app/core/src/main/resources/settings.yml.template +++ b/app/core/src/main/resources/settings.yml.template @@ -81,6 +81,7 @@ security: revocation: mode: none # Revocation checking mode: 'none' (disabled), 'ocsp' (OCSP only), 'crl' (CRL only), 'ocsp+crl' (OCSP with CRL fallback) hardFail: false # Fail validation if revocation status cannot be determined (true=strict, false=soft-fail) + xFrameOptions: DENY # X-Frame-Options header value. Options: 'DENY' (default, prevents all framing), 'SAMEORIGIN' (allows framing from same domain), 'DISABLED' (no X-Frame-Options header sent). Note: automatically set to DISABLED when login is disabled premium: key: 00000000-0000-0000-0000-000000000000 diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java index b84f651d1..8b61d9c3e 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java @@ -201,6 +201,31 @@ public class SecurityConfiguration { http.csrf(CsrfConfigurer::disable); + // Configure X-Frame-Options based on settings.yml configuration + // When login is disabled, automatically disable X-Frame-Options to allow embedding + if (!loginEnabledValue) { + http.headers(headers -> headers.frameOptions(frameOptions -> frameOptions.disable())); + } else { + String xFrameOption = securityProperties.getXFrameOptions(); + if (xFrameOption != null) { + http.headers(headers -> { + if ("DISABLED".equalsIgnoreCase(xFrameOption)) { + headers.frameOptions(frameOptions -> frameOptions.disable()); + } else if ("SAMEORIGIN".equalsIgnoreCase(xFrameOption)) { + headers.frameOptions(frameOptions -> frameOptions.sameOrigin()); + } else { + // Default to DENY + headers.frameOptions(frameOptions -> frameOptions.deny()); + } + }); + } else { + // If not configured, use default DENY + http.headers(headers -> + headers.frameOptions(frameOptions -> frameOptions.deny()) + ); + } + } + if (loginEnabledValue) { http.addFilterBefore( diff --git a/frontend/public/locales/en-GB/translation.toml b/frontend/public/locales/en-GB/translation.toml index 9e818ca3f..561caee18 100644 --- a/frontend/public/locales/en-GB/translation.toml +++ b/frontend/public/locales/en-GB/translation.toml @@ -4527,6 +4527,13 @@ description = "Maximum number of failed login attempts before account lockout" label = "Login Reset Time (minutes)" description = "Time before failed login attempts are reset" +[admin.settings.security.xFrameOptions] +label = "X-Frame-Options" +description = "Controls whether the application can be embedded in iframes" +deny = "Deny (Prevents all framing)" +sameorigin = "Same Origin (Allow framing from same domain)" +disabled = "Disabled (No X-Frame-Options header)" + [admin.settings.security.csrfDisabled] label = "Disable CSRF Protection" description = "Disable Cross-Site Request Forgery protection (not recommended)" diff --git a/frontend/src/proprietary/components/shared/config/configSections/AdminSecuritySection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminSecuritySection.tsx index 19721b760..1cf09ab1e 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/AdminSecuritySection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminSecuritySection.tsx @@ -16,6 +16,7 @@ interface SecuritySettingsData { loginMethod?: string; loginAttemptCount?: number; loginResetTimeMinutes?: number; + xFrameOptions?: string; jwt?: { persistence?: boolean; enableKeyRotation?: boolean; @@ -125,6 +126,7 @@ export default function AdminSecuritySection() { 'security.loginMethod': securitySettings.loginMethod, 'security.loginAttemptCount': securitySettings.loginAttemptCount, 'security.loginResetTimeMinutes': securitySettings.loginResetTimeMinutes, + 'security.xFrameOptions': securitySettings.xFrameOptions, // JWT settings 'security.jwt.persistence': securitySettings.jwt?.persistence, 'security.jwt.enableKeyRotation': securitySettings.jwt?.enableKeyRotation, @@ -280,6 +282,27 @@ export default function AdminSecuritySection() { disabled={!loginEnabled} /> + +