fix: override csp for allowed iframe

This commit is contained in:
Max Peintner
2025-04-30 09:14:11 +02:00
parent 22c1a73245
commit 7a25dce936
6 changed files with 76 additions and 19 deletions

View File

@@ -1,4 +1,5 @@
import createNextIntlPlugin from "next-intl/plugin"; import createNextIntlPlugin from "next-intl/plugin";
import { DEFAULT_CSP } from "./src/lib/csp";
const withNextIntl = createNextIntlPlugin(); const withNextIntl = createNextIntlPlugin();
@@ -29,9 +30,9 @@ const secureHeaders = [
// script-src va.vercel-scripts.com for analytics/vercel scripts // script-src va.vercel-scripts.com for analytics/vercel scripts
{ {
key: "Content-Security-Policy", key: "Content-Security-Policy",
value: value: `${DEFAULT_CSP} frame-ancestors 'none'`,
"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://va.vercel-scripts.com; connect-src 'self'; child-src; style-src 'self' 'unsafe-inline'; font-src 'self'; object-src 'none'; img-src 'self' https://vercel.com;",
}, },
{ key: "X-Frame-Options", value: "deny" },
]; ];
const imageRemotePatterns = [ const imageRemotePatterns = [

View File

@@ -33,8 +33,8 @@ export default async function Page(props: {
const { serviceUrl } = getServiceUrlFromHeaders(_headers); const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const sessionWithData = sessionId const sessionWithData = sessionId
? await loadSessionById(serviceUrl, sessionId, organization) ? await loadSessionById(sessionId, organization)
: await loadSessionByLoginname(serviceUrl, loginName, organization); : await loadSessionByLoginname(loginName, organization);
async function getAuthMethodsAndUser( async function getAuthMethodsAndUser(
serviceUrl: string, serviceUrl: string,
@@ -67,7 +67,6 @@ export default async function Page(props: {
} }
async function loadSessionByLoginname( async function loadSessionByLoginname(
host: string,
loginName?: string, loginName?: string,
organization?: string, organization?: string,
) { ) {
@@ -82,11 +81,7 @@ export default async function Page(props: {
}); });
} }
async function loadSessionById( async function loadSessionById(sessionId: string, organization?: string) {
host: string,
sessionId: string,
organization?: string,
) {
const recent = await getSessionCookieById({ sessionId, organization }); const recent = await getSessionCookieById({ sessionId, organization });
return getSession({ return getSession({
serviceUrl, serviceUrl,

View File

@@ -1,4 +1,5 @@
import { getAllSessions } from "@/lib/cookies"; import { getAllSessions } from "@/lib/cookies";
import { DEFAULT_CSP } from "@/lib/csp";
import { idpTypeToSlug } from "@/lib/idp"; import { idpTypeToSlug } from "@/lib/idp";
import { loginWithOIDCandSession } from "@/lib/oidc"; import { loginWithOIDCandSession } from "@/lib/oidc";
import { loginWithSAMLandSession } from "@/lib/saml"; import { loginWithSAMLandSession } from "@/lib/saml";
@@ -12,6 +13,7 @@ import {
getAuthRequest, getAuthRequest,
getOrgsByDomain, getOrgsByDomain,
getSAMLRequest, getSAMLRequest,
getSecuritySettings,
listSessions, listSessions,
startIdentityProviderFlow, startIdentityProviderFlow,
} from "@/lib/zitadel"; } from "@/lib/zitadel";
@@ -293,17 +295,32 @@ export async function GET(request: NextRequest) {
* This means that the user should not be prompted to enter their password again. * This means that the user should not be prompted to enter their password again.
* Instead, the server attempts to silently authenticate the user using an existing session or other authentication mechanisms that do not require user interaction * Instead, the server attempts to silently authenticate the user using an existing session or other authentication mechanisms that do not require user interaction
**/ **/
const securitySettings = await getSecuritySettings({
serviceUrl,
});
const selectedSession = await findValidSession({ const selectedSession = await findValidSession({
serviceUrl, serviceUrl,
sessions, sessions,
authRequest, authRequest,
}); });
if (!selectedSession || !selectedSession.id) { const noSessionResponse = NextResponse.json(
return NextResponse.json( { error: "No active session found" },
{ error: "No active session found" }, { status: 400 },
{ status: 400 }, );
if (securitySettings?.embeddedIframe?.enabled) {
securitySettings.embeddedIframe.allowedOrigins;
noSessionResponse.headers.set(
"Content-Security-Policy",
`${DEFAULT_CSP} frame-ancestors ${securitySettings.embeddedIframe.allowedOrigins.join(" ")};`,
); );
noSessionResponse.headers.delete("X-Frame-Options");
}
if (!selectedSession || !selectedSession.id) {
return noSessionResponse;
} }
const cookie = sessionCookies.find( const cookie = sessionCookies.find(
@@ -311,10 +328,7 @@ export async function GET(request: NextRequest) {
); );
if (!cookie || !cookie.id || !cookie.token) { if (!cookie || !cookie.id || !cookie.token) {
return NextResponse.json( return noSessionResponse;
{ error: "No active session found" },
{ status: 400 },
);
} }
const session = { const session = {
@@ -332,7 +346,19 @@ export async function GET(request: NextRequest) {
}, },
}), }),
}); });
return NextResponse.redirect(callbackUrl);
const callbackResponse = NextResponse.redirect(callbackUrl);
if (securitySettings?.embeddedIframe?.enabled) {
securitySettings.embeddedIframe.allowedOrigins;
callbackResponse.headers.set(
"Content-Security-Policy",
`${DEFAULT_CSP} frame-ancestors ${securitySettings.embeddedIframe.allowedOrigins.join(" ")};`,
);
callbackResponse.headers.delete("X-Frame-Options");
}
return callbackResponse;
} else { } else {
// check for loginHint, userId hint and valid sessions // check for loginHint, userId hint and valid sessions
let selectedSession = await findValidSession({ let selectedSession = await findValidSession({

View File

@@ -0,0 +1,2 @@
export const DEFAULT_CSP =
"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://va.vercel-scripts.com; connect-src 'self'; child-src; style-src 'self' 'unsafe-inline'; font-src 'self'; object-src 'none'; img-src 'self' https://vercel.com;";

View File

@@ -92,6 +92,21 @@ export async function getLoginSettings({
return useCache ? cacheWrapper(callback) : callback; return useCache ? cacheWrapper(callback) : callback;
} }
export async function getSecuritySettings({
serviceUrl,
}: {
serviceUrl: string;
}) {
const settingsService: Client<typeof SettingsService> =
await createServiceForHost(SettingsService, serviceUrl);
const callback = settingsService
.getSecuritySettings({})
.then((resp) => (resp.settings ? resp.settings : undefined));
return useCache ? cacheWrapper(callback) : callback;
}
export async function getLockoutSettings({ export async function getLockoutSettings({
serviceUrl, serviceUrl,
orgId, orgId,

View File

@@ -1,6 +1,8 @@
import { headers } from "next/headers"; import { headers } from "next/headers";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { DEFAULT_CSP } from "./lib/csp";
import { getServiceUrlFromHeaders } from "./lib/service"; import { getServiceUrlFromHeaders } from "./lib/service";
import { getSecuritySettings } from "./lib/zitadel";
export const config = { export const config = {
matcher: [ matcher: [
@@ -22,6 +24,8 @@ export async function middleware(request: NextRequest) {
const { serviceUrl } = getServiceUrlFromHeaders(_headers); const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const securitySettings = await getSecuritySettings({ serviceUrl });
const instanceHost = `${serviceUrl}` const instanceHost = `${serviceUrl}`
.replace("https://", "") .replace("https://", "")
.replace("http://", ""); .replace("http://", "");
@@ -39,6 +43,20 @@ export async function middleware(request: NextRequest) {
responseHeaders.set("Access-Control-Allow-Origin", "*"); responseHeaders.set("Access-Control-Allow-Origin", "*");
responseHeaders.set("Access-Control-Allow-Headers", "*"); responseHeaders.set("Access-Control-Allow-Headers", "*");
responseHeaders.set(
"Content-Security-Policy",
`${DEFAULT_CSP} frame-ancestors 'none'`,
);
if (securitySettings?.embeddedIframe?.enabled) {
securitySettings.embeddedIframe.allowedOrigins;
responseHeaders.set(
"Content-Security-Policy",
`${DEFAULT_CSP} frame-ancestors ${securitySettings.embeddedIframe.allowedOrigins.join(" ")};`,
);
responseHeaders.delete("X-Frame-Options");
}
request.nextUrl.href = `${serviceUrl}${request.nextUrl.pathname}${request.nextUrl.search}`; request.nextUrl.href = `${serviceUrl}${request.nextUrl.pathname}${request.nextUrl.search}`;
return NextResponse.rewrite(request.nextUrl, { return NextResponse.rewrite(request.nextUrl, {
request: { request: {