Building a solid web application requires an extensive understanding of fundamentals in developments, infrastructure, and security. Ensuring the stability of our platforms and the security of our users' information is a critical matter that is getting more complex and sensitive over time. With every passing year, threats get more abundant and complex, so to mitigate this, software companies are incorporating more sophisticated and robust solutions. Developers have a responsibility to ensure that we take advantage of these solutions and adapt our software to comply with the policies and implement the necessary measures.
One of such measures is Content Security Policy or CSP. The purpose of this article is to help demystify the concept of content security, briefly define what CSP is, illustrate how to enable CSP in NodeJS, explore some possible errors the reader might encounter, and show how to address them.
If you have no experience working with NodeJS or you are just dipping your toes in it, we highly recommend you take some time to explore this introduction to NodeJS and make yourself familiar with it. We will discuss some aspects that might not be immediately clear unless you have some background in NodeJS.
Without further ado, let's jump right in.
What Is Content Security Policy?
Content Security Policy is a set of policies or instructions that the browser enforces on web pages. During the process of loading a page, the browser has to request and render a bunch of content and code. This process is standard and usually harmless, as pretty much all modern websites are in nature complex and composed of lots of lines of HTML, CSS, JavaScript, and other resources like images and files that the code references. This referenced code could be from the same origin (requested domain) or another origin; browsers usually don't distinguish that. So naturally, the browser will process and execute these resources. It is crucial that the browser does not run any malicious code or access data that does not belong to the current website.
A CSP provides a response header to ensure that the browser only executes resources vetted by the original website.
This indicates what resources are allowed to be processed by the browser. The security layer helps mitigate attackers from taking advantage of vulnerabilities like cross-site scripting (XSS) and injection attacks.
A basic Content Security Policy header would look something like this:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; font-src 'self'; img-src 'self'; frame-src 'self';
As you can see, each section in the header is pertinent to a specific kind of source. These may be images (img-src), scripts (script-src), styles (style-src), and so forth. In addition, the 'self' directive states that only resources from the same origin will be allowed to be executed. You can specify domains that you want to retrieve resources from by putting their URLs next to 'self.' Additionally, you can allow all domains by setting '*' (but don't do this unless you absolutely have to).
Enabling NodeJS Content Security Policy
Now that we are familiar with Content Security Policy and how it looks, let's see it in our code.
To implement basic CSP in NodeJS, you need to make sure that the header is being attached to all responses. To achieve this, open your js file, which contains the application server logic, and append the following code.
app.use(function (req, res, next) {
res.setHeader(
'Content-Security-Policy', "default-src 'self'; script-src 'self'; style-src 'self'; font-src 'self'; img-src 'self'; frame-src 'self'"
);
next();
});
It will look like this:
Lines of code that include the above code snippet.
Pretty simple right?
Once you address this, you can check that the response header contains the configuration mentioned above. The browser will enforce it, most likely resulting in a broken page and tons of alerts.
A list of alerts from the code.
No need to panic. Implementing proper Content Security Policies into our application requires a fair amount of changes and testing. For now, we want to address the errors while still having a functional site, and that's where the 'Content-Security-Policy-Report-Only' alternative will be helpful.
app.use(function (req, res, next) {
res.setHeader(
'Content-Security-Policy-Report-Only', "default-src 'self'; script-src 'self'; style-src 'self'; font-src 'self'; img-src 'self'; frame-src 'self'"
);
next();
});
By changing to 'report only,' the browser will no longer enforce the directives but continue to display the corresponding violation alerts and errors. This behavior is helpful for development environments where the platform's security is not essential but where the developer needs to be aware of any violations to address them adequately.
Addressing CSP Violations
It might be overwhelming to see so many error messages in the browser developer console, but don't worry. The process to address them is very simple once we understand what our site is requesting and what it actually needs. As you can already see, the report will serve as a guide to refine the policy directive and make the necessary updates.
First, confirm that the resources that the alert reports are, in fact, legitimate and necessary for the proper functioning of the application. For example, we can see that our application is requesting the following:
https://code.jquery.com/jquery-3.5.1.min.js
https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js
https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.31/moment-timezone-with-data.js
https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js
https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.bundle.min.js
https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js
https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css
https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css
https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/webfonts/fa-brands-400.woff2
https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/webfonts/fa-brands-400.woff
https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/webfonts/fa-brands-400.ttf
https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/webfonts/fa-regular-400.woff2
https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/webfonts/fa-regular-400.woff
https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/webfonts/fa-regular-400.ttf
https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/webfonts/fa-solid-900.woff2
https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/webfonts/fa-solid-900.woff
https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/webfonts/fa-solid-900.ttf
These are all valid resources and come from trusted sources. Now that we have the resources at hand, you can add them to the allowlist of each respective source as follows.
app.use(function (req, res, next) {
res.setHeader(
'Content-Security-Policy-Report-Only', "default-src 'self'; script-src 'self' https://code.jquery.com/jquery-3.5.1.min.js https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.31/moment-timezone-with-data.js https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.bundle.min.js https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js; style-src 'self' https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css; font-src 'self' https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/webfonts/fa-brands-400.woff2 https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/webfonts/fa-brands-400.woff https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/webfonts/fa-brands-400.ttf https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/webfonts/fa-regular-400.woff2 https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/webfonts/fa-regular-400.woff https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/webfonts/fa-regular-400.ttf https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/webfonts/fa-solid-900.woff2 https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/webfonts/fa-solid-900.woff https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/webfonts/fa-solid-900.ttf; img-src 'self'; frame-src 'self'"
);
next();
});
This simple change makes the alerts go away.
A blank area that no longer shows alerts from the code.
Simple right?
Moreover, if you want to allow all resources from a specific source domain, thus encompassing many files in just one line, you can do so by specifying the common domain in the directive.
app.use(function (req, res, next) {
res.setHeader(
'Content-Security-Policy-Report-Only', "default-src 'self'; script-src 'self' https://code.jquery.com https://cdnjs.cloudflare.com https://stackpath.bootstrapcdn.com https://cdn.jsdelivr.net; style-src 'self' https://stackpath.bootstrapcdn.com https://cdnjs.cloudflare.com; font-src 'self' https://cdnjs.cloudflare.com; img-src 'self'; frame-src 'self'"
);
next();
});
In-Line Violations
Now, what about the in-line code we have in our HTML? Well, there are a few things we can do to address these violations.
The first (recommended) approach is to move all in-line code and styles to a separate file and reference it. This way, you can ensure the security of your application and keep it consistent.
If this is not possible, you can move the code to a <script/> or <style/> tag and use a SHA hash key to tell the browser that the code in this tag is allowed. You can generate the hash key by hashing the code inside the tag with a SHA256 algorithm. Conveniently, the browser already calculates this hash in the alert itself so you can easily add it to the directive.
app.use(function (req, res, next) {
res.setHeader(
'Content-Security-Policy-Report-Only', "default-src 'self'; script-src 'self' https://code.jquery.com https://cdnjs.cloudflare.com https://stackpath.bootstrapcdn.com https://cdn.jsdelivr.net 'sha256-ISInfOBSUWFgeTRFLf63g+rFNSswGDl15oK0iXgYM='; style-src 'self' https://stackpath.bootstrapcdn.com https://cdnjs.cloudflare.com; font-src 'self' https://cdnjs.cloudflare.com; img-src 'self'; frame-src 'self'"
);
next();
});
Additionally, if you don't want to use the SHA hash, you can use what is known as a 'nonce' tag attribute and add it to the corresponding tag. This solution will essentially serve the same purpose but must be updated with each request with some server-side code.
In Summary
The process of ensuring the integrity and security of our platforms can be very complex in nature. It is easy to end up lost in a rabbit hole of patches and mitigations to potential exploits and vulnerabilities. However, these security issues must be addressed.
As seen in this article, NodeJS and its flexible and approachable development stack make the work of securing against Content Security Policy exploits very easy and straightforward. No need to tamper with complex configurations or scary settings.
This post was written by Juan Reyes. Juan is an engineer by profession and a dreamer by heart who crossed the seas to reach Japan following the promise of opportunity and challenge. While trying to find himself and build a meaningful life in the east, Juan borrows wisdom from his experiences as an entrepreneur, artist, hustler, father figure, husband, and friend to start writing about passion, meaning, self-development, leadership, relationships, and mental health. His many years of struggle and self-discovery have inspired him and drive to embark on a journey for wisdom.