Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions app/.env.development
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ SENTRY_IGNORE_API_RESOLUTION_ERROR=1
MAPTILER_API_KEY=123
ADFS_PROFILE_URL=https://www.myaccount-r.eiam.admin.ch/
NEXTAUTH_URL=https://localhost:3000
# NEXT_PUBLIC_SENTRY_DSN=https://…
# NEXT_PUBLIC_SENTRY_CSP="https://*.sentry.io"
# NEXT_PUBLIC_SENTRY_ENV="dev"
# SENTRY_DSN=https://…
# SENTRY_CSP="https://*.sentry.io"
# SENTRY_ENV="dev"
4 changes: 2 additions & 2 deletions app/domain/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ export const ADFS_PROFILE_URL =
export const MAPTILER_API_KEY =
clientEnv?.MAPTILER_API_KEY ?? process.env.MAPTILER_API_KEY ?? "";

export const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
export const SENTRY_ENV = process.env.NEXT_PUBLIC_SENTRY_ENV;
export const SENTRY_DSN = clientEnv?.SENTRY_DSN ?? process.env.SENTRY_DSN;
export const SENTRY_ENV = clientEnv?.SENTRY_ENV ?? process.env.SENTRY_ENV;

/**
* Server-side-only **RUNTIME** variables (not exposed through window)
Expand Down
72 changes: 72 additions & 0 deletions app/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { NextRequest, NextResponse } from "next/server";

const EMBEDDABLE_PATH_PATTERNS = [
/^\/embed\//,
/^\/preview$/,
/^\/api\/embed-aem-ext\//,
];

function buildCSP(frameAncestors: string): string {
const isDev = process.env.NODE_ENV === "development";
const isVercel = !!process.env.VERCEL;
const sentryCSP = process.env.SENTRY_CSP ? ` ${process.env.SENTRY_CSP}` : "";
const unsafeEval = isDev ? " 'unsafe-eval'" : "";
const vercelDefault = isVercel
? " https://vercel.live/ https://vercel.com"
: "";
const vercelScript = isVercel
? " https://vercel.live/ https://vercel.com"
: "";
const vercelScriptElem = isVercel
? " https://vercel.live https://vercel.com https://*.vercel.app"
: "";
const vercelWorker = isVercel ? " https://*.vercel.app" : "";

return [
`default-src 'self' 'unsafe-inline'${unsafeEval}${sentryCSP}${vercelDefault}`,
`script-src 'self' 'unsafe-inline'${unsafeEval}${sentryCSP}${vercelScript} https://api.mapbox.com https://api.maptiler.com`,
`script-src-elem 'self' 'unsafe-inline' https://*.admin.ch https://visualize.admin.ch https://*.visualize.admin.ch${vercelScriptElem} https://api.mapbox.com`,
`style-src 'self' 'unsafe-inline' https://fonts.googleapis.com`,
`font-src 'self'`,
`img-src 'self' * data: blob:`,
`connect-src 'self' *`,
`worker-src 'self' blob: https://*.admin.ch${vercelWorker}`,
`form-action 'self'`,
`frame-ancestors ${frameAncestors}`,
`object-src 'none'`,
`base-uri 'self'`,
`upgrade-insecure-requests`,
].join("; ");
}

export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const isEmbeddable = EMBEDDABLE_PATH_PATTERNS.some((p) => p.test(pathname));
const frameAncestors = isEmbeddable ? "*" : "'self'";

const reportOnly = process.env.CSP_REPORT_ONLY === "true";
const cspKey = reportOnly
? "Content-Security-Policy-Report-Only"
: "Content-Security-Policy";

const response = NextResponse.next();
response.headers.set(cspKey, buildCSP(frameAncestors));

if (process.env.PREVENT_SEARCH_BOTS === "true") {
response.headers.set("X-Robots-Tag", "noindex, nofollow");
}

return response;
}

export const config = {
matcher: [
{
source: "/((?!_next/static|_next/image|favicon\\.ico).*)",
missing: [
{ type: "header", key: "next-router-prefetch" },
{ type: "header", key: "purpose", value: "prefetch" },
],
},
],
};
96 changes: 6 additions & 90 deletions app/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ console.log("GitHub Repo", process.env.NEXT_PUBLIC_GITHUB_REPO);
console.log("Extra Certs", process.env.NODE_EXTRA_CA_CERTS);
console.log("Prevent search bots", process.env.PREVENT_SEARCH_BOTS);

if (process.env.NEXT_PUBLIC_SENTRY_DSN) {
console.log("Sentry DSN:", process.env.NEXT_PUBLIC_SENTRY_DSN);
if (process.env.SENTRY_DSN) {
console.log("Sentry DSN:", process.env.SENTRY_DSN);
}

module.exports = withSentryConfig(
Expand All @@ -54,58 +54,9 @@ module.exports = withSentryConfig(
},

headers: async () => {
// See https://content-security-policy.com/ & https://developers.google.com/tag-platform/security/guides/csp
const isDev = process.env.NODE_ENV === "development";
const isVercel = !!process.env.VERCEL;
const sentryCSP = process.env.NEXT_PUBLIC_SENTRY_CSP
? ` ${process.env.NEXT_PUBLIC_SENTRY_CSP}`
: "";
const unsafeEval = isDev ? " 'unsafe-eval'" : "";
// Vercel Toolbar / Live Comments hosts — only needed on Vercel deployments
const vercelDefault = isVercel
? " https://vercel.live/ https://vercel.com"
: "";
const vercelScript = isVercel
? " https://vercel.live/ https://vercel.com"
: "";
const vercelScriptElem = isVercel
? " https://vercel.live https://vercel.com https://*.vercel.app"
: "";
const vercelWorker = isVercel ? " https://*.vercel.app" : "";

const buildCSP = (frameAncestors) =>
[
`default-src 'self' 'unsafe-inline'${unsafeEval}${sentryCSP}${vercelDefault}`,
`script-src 'self' 'unsafe-inline'${unsafeEval}${sentryCSP}${vercelScript} https://api.mapbox.com https://api.maptiler.com`,
`script-src-elem 'self' 'unsafe-inline' https://*.admin.ch https://visualize.admin.ch https://*.visualize.admin.ch${vercelScriptElem} https://api.mapbox.com`,
`style-src 'self' 'unsafe-inline' https://fonts.googleapis.com`,
`font-src 'self'`,

// * to allow loading legend images from custom WMS / WMTS endpoints and data: to allow downloading images
`img-src 'self' * data: blob:`,

// * to allow WMS / WMTS endpoints
`connect-src 'self' *`,

`worker-src 'self' blob: https://*.admin.ch${vercelWorker}`,
`form-action 'self'`,
`frame-ancestors ${frameAncestors}`,
`object-src 'none'`,
`base-uri 'self'`,
`upgrade-insecure-requests`,
].join("; ");

// When CSP_REPORT_ONLY=true, emit the report-only header so violations
// are surfaced to the browser console without being enforced. Useful
// for rolling out tighter policies. The header is otherwise always
// present — there is intentionally no kill-switch to fully disable CSP.
const reportOnly =
process.env.CSP_REPORT_ONLY &&
process.env.CSP_REPORT_ONLY === "true";
const cspKey = reportOnly
? "Content-Security-Policy-Report-Only"
: "Content-Security-Policy";

// Static security headers that don't depend on runtime env vars.
// Dynamic headers (CSP, X-Robots-Tag) are set in middleware.ts so they
// can read runtime env vars injected into the container at startup.
const baseHeaders = [
{ key: "X-Content-Type-Options", value: "nosniff" },
{
Expand All @@ -129,43 +80,8 @@ module.exports = withSentryConfig(
// `frame-ancestors` already handles clickjacking protection.
{ key: "Cross-Origin-Opener-Policy", value: "same-origin" },
];
if (process.env.PREVENT_SEARCH_BOTS === "true") {
baseHeaders.push({
key: "X-Robots-Tag",
value: "noindex, nofollow",
});
}

const headers = [];

// Catch-all — block iframing to prevent clickjacking on the editor / browser / login UI.
// Must come first: when multiple Next.js header rules match the same path,
// later rules override earlier ones for the same header key.
headers.push({
source: "/:path*",
headers: [
...baseHeaders,
{ key: cspKey, value: buildCSP("'self'") },
],
});

// Routes that are intended to be embedded in third-party iframes.
// These override the catch-all CSP to allow `frame-ancestors *`.
// `/api/embed-aem-ext/*` serves the AEM external-embed HTML wrapper,
// which partner sites may iframe directly.
const embeddableSources = [
"/embed/:path*",
"/preview",
"/api/embed-aem-ext/:path*",
];
for (const source of embeddableSources) {
headers.push({
source,
headers: [...baseHeaders, { key: cspKey, value: buildCSP("*") }],
});
}

return headers;
return [{ source: "/:path*", headers: baseHeaders }];
},

pageExtensions: ["js", "ts", "tsx", "mdx"],
Expand Down
2 changes: 2 additions & 0 deletions app/pages/api/client-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export default async function clientEnvApi(
GRAPHQL_ENDPOINT: process.env.GRAPHQL_ENDPOINT,
ADFS_PROFILE_URL: process.env.ADFS_PROFILE_URL,
MAPTILER_API_KEY: process.env.MAPTILER_API_KEY,
SENTRY_DSN: process.env.SENTRY_DSN,
SENTRY_ENV: process.env.SENTRY_ENV,
})}`;

if (result) {
Expand Down
Loading