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.
JB
Jovan Blažek
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:
Define the CSP rules for each source using the frontend dashboard.
Store the data in a PostgreSQL database.
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
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.
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.
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.
exportconstmiddleware=(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.