# Dynamic CSP in Next.js applications

Content Security Policy (CSP) is a crucial security feature that helps protect web applications from script injections and other potential security threats. Typically, CSP is implemented statically, in the head of the website, or HTTP response. In this blog post, we'll discuss the problem we encountered with this approach and the solution we devised to manage CSP dynamically within our Next.js application.

- URL: https://sudolabs.com/insights/dynamic-csp-in-next-js-applications
- Published: 2023-11-03
- Updated: 2024-02-27
- Author: Jovan Blažek
- Categories: Tech
- Language: English

## In need of adapting CSP for real-time flexibility

In most cases, web applications are content with a static implementation of CSP, which provides protection against script injections. However, we faced a unique challenge in one of our projects. We needed a flexible solution that would allow us to change the CSP rules on the fly, without the need for constant redeployment. This became particularly cumbersome due to the integration of extended analytics tools and experimentation requirements.

To address this challenge, we came up with the idea of managing CSP rules directly within our application.

## Our solution

Our solution involves storing CSP rules in a database and dynamically constructing the CSP header based on these rules.

We agreed on the following flow:

1. Define the CSP rules for each source using the frontend dashboard.
2. Store the data in a PostgreSQL database.
3. On page request, build the CSP header from the rules provided by API.

This approach, however, presented several challenges, such as communication with the backend from the Next.js' middleware and the need to cache the results to avoid performance issues.

### Storing CSP rules in the database

To store the CSP rules, we created a new table called `trusted_sources` in our PostgreSQL database. This table includes one column for each CSP directive, such as `childSrc`, `connectSrc`, `fontSrc`, and more. Each column is of type `boolean`, allowing us to enable or disable specific source types for each domain.

### Fetching the data in the middleware

![](https://a.storyblok.com/f/117609/718x1119/c4c9bd4b58/columns.png)

To get the CSP rules from the database, we created a GraphQL query in our API, which we called from the Next.js middleware. Since initializing Apollo Client in the middleware was not possible, we had to call GraphQL directly using the `fetch` method. We defined the following function, `fetchTrustedSources`, to retrieve the trusted sources from the backend.

```typescript
export const fetchTrustedSources = async (): Promise<
	FetchedTrustedSource[]
> => {
	const trustedSourcesResult = await fetch(API_URL, {
		method: "POST",
		headers: {
			"Content-Type": "application/json",
		},
		body: JSON.stringify({
			operationName: "trustedSources",
			variables: {
				input: {
					pagination: {
						limit: 1000,
					},
				},
			},
			query: `
        query trustedSources($input: TrustedSourcesInput) {
          trustedSources(input: $input) {
            data {
              domain
              childSrc
              connectSrc
              fontSrc
              frameSrc
              imgSrc
              manifestSrc
              mediaSrc
              objectSrc
              prefetchSrc
              scriptSrc
              scriptSrcElem
              scriptSrcAttr
              styleSrc
              styleSrcElem
              styleSrcAttr
              workerSrc
            }
          }
        }
      `,
		}),
	})
	return get(
		await trustedSourcesResult.json(),
		["data", "trustedSources", "data"],
		[]
	)
}
```

### Parsing and caching the data

Building the CSP header from the rules provided by API was fairly straightforward. Once we had it working, we added a cache to avoid contacting the API on every request. This obviously improved the overall performance of the application. For improved flexibility, we used the environment variable `REVALIDATE_INTERVAL` to control the cache duration.

In the following piece of code, you can see the implementation of the CSP middleware. In the beginning, we are checking if the cache has expired. If it has, we fetch the data from the API and update the cache. Then we call the `parseTrustedSources` function, which is responsible for building the CSP header. If the cache has not expired, we simply use the cached data.

```typescript
import * as Sentry from "@sentry/nextjs"
import { NextRequest, NextResponse } from "next/server"
import { parseTrustedSources } from "utils/cspParser"
import { fetchTrustedSources } from "./fetchTrustedSources"
import { TrustedSource } from "./types"

let trustedSourcesCache: {
	updatedAt: number | null
	data: TrustedSource[]
} = {
	updatedAt: null,
	data: [],
}

const REVALIDATE_INTERVAL = parseInt(process.env.CSP_REVALIDATE_INTERVAL) || 0

const getCSPHeader = async (nonce: string) => {
	const timeNow = Date.now()
	const hasCacheExpired =
		!trustedSourcesCache.updatedAt ||
		timeNow - trustedSourcesCache.updatedAt > REVALIDATE_INTERVAL

	if (hasCacheExpired) {
		try {
			const trustedSources = await fetchTrustedSources()
			trustedSourcesCache = {
				updatedAt: timeNow,
				data: trustedSources,
			}
		} catch (error) {
			console.error(
				"Failed to fetch trusted sources in middleware.",
				error
			)
			Sentry.captureException(error)
		}
	}
	return parseTrustedSources(trustedSourcesCache.data, nonce)
}

export const contentSecurityPolicyHeaderMiddleware = async (
	request: NextRequest,
	response: NextResponse,
	generateNonce: boolean
) => {
	let nonce = request.cookies.get("nonce")
	if (!nonce || generateNonce) {
		nonce = crypto.randomUUID().replaceAll("-", "")
		const cspHeader = await getCSPHeader(nonce)
		response.cookies.set("nonce", nonce)
		response.headers.set("Content-Security-Policy", cspHeader)
	}
	return response
}
```

### Calling the middleware

The final piece of the puzzle involved calling the middleware and modifying the response. The most crucial part of the code here is the filtering process when the response is modified. We don't want to generate a new nonce for CSP on every request; we only need it for the initial page load. Next.js often makes requests for prefetching pages or loading data for hydration, and we want to skip modifying the CSP for those requests.

```typescript
export const middleware = (request: NextRequest) => {
	const response = NextResponse.next()
	const generateNonce =
		request.headers.get("purpose") !== "prefetch" &&
		request.headers.get("x-nextjs-data") !== "1"

	return generateNonce
		? contentSecurityPolicyHeaderMiddleware(
				request,
				response,
				generateNonce
		  )
		: response
}
```

## What to keep in mind when implementing this solution

Managing Content Security Policy dynamically within your web application can be a powerful tool for improving security without the need for frequent redeployments. However, there are some security considerations to keep in mind when implementing this solution.

This feature can serve as a backdoor to your application so keeping the access limited is crucial. Make sure to use strong authentication and authorization mechanisms to protect your application from unauthorized access, like two-factor authentication and role-based access control. You should ensure that the database is properly secured and that the API for managing trusted domains is not exposed to the public.

You should also consider the security implications of allowing CSP rules to be modified by non-technical users. In our case, we decided to limit access to the dashboard for editing CSP to technical users only.

In summary, dynamic CSP management empowers web applications to adapt to evolving security needs, improving security while minimizing operational disruptions.
