Welcome (back) to my technical series on security for full-stack devs. If this is your first time joining us, then please check out article one in the series here. The first article is a checklist of all of the considerations we should make when starting a new web app. In this article, we’re going to look at the best practices I’d recommend for securing your web app at the server level — from both the front end and the back end.
The web server is arguably the most influential choice one can make when embarking on a new web project. Not only for security reasons, but for things like performance and reliability. We need our Server to be blazing fast, but we cannot trade off security in doing so.
One of the big, and often the most common issues here, is the fact that often we simply launch the default Server used by our chosen framework or piece of technology, without actually knowing much (if anything) about the server itself, or without including any security features whatsoever.
That being said, we can’t view security as the only priority on our list (although it should be near the top). As full-stack and front-end developers, we have rather a lot of considerations to make including, as stated above, performance and reliability. If any of the security features I touch on in this article will impact on either of those factors, I’ll make sure I mention that and include any pitfalls you may encounter.
In my last article, I briefly touched on subjects such as XSS and HTTPS. In this article, I’d like to show you, in more depth, how we can mitigate these issues alongside some more key points such as HSTS (Strict Transport Security), CSP (Content Security Policies), and referring back to taking a tech blueprint in my last article; how the points I will cover aid that. So let’s dive in, and get on with ensuring our web app is as secure as possible, starting from the server outward.
HSTS is a security header that allows us to enforce HTTPS across our entire web app. If you read my previous article, you’ll remember I advocate the idea of HTTPS everywhere, and showed you how to get a trusted, secure SSL certificate free of charge from Let’s Encrypt. The reason we need HTTPS everywhere is that our users are vulnerable to cookie stealing and man-in-the-middle attacks if we don’t have it implemented.
Now, as you’re probably aware, simply owning an SSL cert will not immediately make all of your web app HTTPS only — we need to tell our app to do that ourselves. One of the best ways of doing this is by using the HTTP Header of HSTS. By using this header, we can force all traffic on our app to use HTTPS and upgrade non-HTTPS. This header may also even provide a performance boost, as we no longer would have to send our users through a manual redirect.
So, you’re probably thinking, “Wow! I need this!” Well, whilst I agree — alongside the Content Security Policy I’ll talk about later — this needs to be implemented with caution. Allow me to explain. Here’s what a sample HSTS Header looks like:
Strict-Transport-Security: max-age=630720; includeSubDomains; preload
And in Node.js:
function requestHandler(req, res) { res.setHeader('Strict-Transport-Security', 'max-age=630720; includeSubDomains; preload'); }
In this Header, we have three directives that apply: max-age
, includeSubDomains
, and preload
.
max-age
By specifying a max-age
, we are telling the user’s browser to cache the fact that we use only HTTPS. This means that if the user tries to visit a non-HTTPS version of the site, their browser will be automatically redirected to the HTTPS site before it even sends a message to the server. Therein lies the slight performance boost I mentioned earlier. Now, while this does sound fantastic in theory, what we need to be aware of here is the fact that if a user ever needed to access a non-HTTPS page, their browser simply won’t let them until this max-age
expires.
If you are going to activate this feature and set a long max-age
(required by the pre-load sites I’ll talk about in a second), you really need to be sure that you have your SSL cert set up correctly, and HTTPS enabled on all of your web app before you take action!
includeSubDomains
The includeSubDomains
directive does exactly what it says on-the-tin. It simply offers additional protection by enforcing the policy across your subdomains too. This is useful if you run a web app that sets cookies from one section (perhaps a gaming section), to another section (perhaps a profile section), that need to be kept secure. Again, the issue with this lies similarly to the above, in that you must be sure every subdomain you own and run, is entirely ready for this to be applied.
preload
The most dangerous directive of them all! Basically, the preload
directive is an in-browser-built directive that comes straight from the browser creators. This means that your web app can be hardcoded into the actual browser to always use HTTPS. Again, whilst this would mean no redirects, and therefore a performance boost, once you’re on this list; it’s very difficult to get back off it! Considering that Chrome takes around three months from build to table — and that’s only for the people who auto-update — you’ve got a huge wait time if you make a mistake.
So we have ourselves here an incredibly powerful, yet actively quite dangerous Security feature. The key here is ensuring you know your HTTPS measures inside-out, and using discretion. Whilst I don’t recommend you submit your site to the preload
directive, if you wish to, you can here.
Note: It is not a requirement to use preload to utilize HSTS. The only header you need apply is the max-age
header.
If you are going to use the HSTS protocol, start out with a small max-age
— something like a few hours, and continue to ramp it up over a period of time. This is the official advice Google Chrome give. If you use the includeSubDomains
directive, be sure you don’t have internal (company.mysite.com) subdomains that would be unreachable if affected. If you’re going to submit your web app to preload
, follow the official guidelines, and make sure you know exactly what you’re doing — which I’m not entirely confident of myself!
As I mentioned in my last article, XSS (cross-site scripting) is the most common of all web app attacks. XSS occurs when a malicious entity injects scripts to be run, into your web app. A few years back, most web browsers added a security filter for XSS attacks built into the browser itself. Now whilst in theory this was a good step, they did tend to throw up false positives quite often. Due to this, the filter can be turned off by the user (and the option should be available, in my opinion).
To ensure our users are protected, we can force this filter (worth it), on our web app by using the X-XSS-Protection
header. This header is widely supported by common browsers, and something I’d recommend using every time.
To apply this header to your Node.js app, you should include the following:
function requestHandler(req, res) { res.setHeader( 'X-XSS-Protection', '1; mode=block' ); }
Note the two directives in this header: 1
is simply acts as a boolean 1 or 0 value to reflect on or off. mode=block
will stop the entire page loading instead of simply sanitizing the page, as it would if you excluded this directive altogether.
If you’re a security freak like myself, and a user of the Chromium browser, you could even go one step further than this and set the directives like so:
X-XSS-Protection: 1; report=<reporting-uri>
Now, if the browser detects an XSS attack, the page will be sanitized, and a report sent of the violation. Note that this uses the functionality of the CSP report-uri
directive to send a report that I will talk about in the Content Security Policy section below.
Clickjacking occurs when a malicious agent injects objects/iframes into your web app, made to look like your web app, that actually send the User to a malicious site when clicked. Another common — and possibly more scary — example is that malicious agents insert something that looks like a payment form into your web app that looks realistic but steals payment details.
Now, whilst this could be a very dangerous issue, it’s very easy to mitigate, with almost no impact on your web app. Servers offer browsers a header protocol named X-Frame-Options
. This protocol allows us to specify domains to accept iFrames from. It also allows us to state which sites our web app can be embedded on. With this protocol, we get three fairly self-explanatory options/directives: DENY
, ALLOW-FROM
, and SAMEORIGIN
.
If we choose DENY
, we can block all framing. If we use ALLOW-FROM
, we can supply a list of domains to allow framing within. I tend to use the SAMEORIGIN
directive, as this means framing can only be done within the current domain. This can be utilized with the following:
function requestHandler(req, res) { res.setHeader( 'X-Frame-Options', 'SAMEORIGIN' ); }
CSP is another major topic when it comes to Server-Browser security for web apps. At a high-level, Content Security Policies tell the browser which content is authorized to execute on a web app and which will be blocked. Primarily, this can be used to prevent XSS, in which an attacker could place a <script>
tag on your web app. The Content Security Policy is a server browser header that we can set to ensure our server tells the browser exactly which media, scripts, and their origins, we will allow to be executed on our web app.
The whitelisting of resources and execution URIs provides a good level of security, that will in most parts, defend against the majority of attacks.
To include a Content Security Policy that allows only internal and Google Analytics, in an Express.js server you could do the following:
var express = require('express'); var app = express(); app.use(function(req, res, next) { res.setHeader( "Content-Security-Policy", "script-src 'self' https://analytics.google.com" ); return next(); }); app.use(express.static(__dirname + '/')); app.listen(process.env.PORT || 3000);,
However, if we do not wish to allow any external sites to execute scripts on our web app, we could simply include the following:
function requestHandler(req, res) { res.setHeader( 'Content-Security-Policy', "script-src 'self'" ); }
Note the script-src
directive here, that we have set to self
, therefore only allowing scripts from within our own domain. Of course, CSP is not without its own problems. Firstly, it would be very easy for us to forget about some of the media we have in our web app and to simply exclude them accidentally. Now that the web is so rich in media, this would be reasonably easy to do. Secondly, many of us use third-party plugins on our web app. Again, unless we have a full blueprint of these, we could very easily block them.
So, once activated, this server header could potentially be very detrimental to us. However, there are two great ways of testing this. You can set a strict policy, and use the built in directives; report-only
and report-uri
to test them. The report-uri
directive tells the browser to send a JSON report of all of the blocked scripts to a URi that we specify. The report-only
directive does the same, but will not block the scripts on the site. This is very useful for testing, before we put this header into production.
There’s a good write-up on the reporting, here.
Content Security Policies are great, but must be used cautiously. Much the same as HSTS mentioned above, we need to ensure we are aware of the ins and outs of the situation before activating. If you are loading in external images, scripts etc. you need to understand that unless you include these in the policy, they will be blocked.
I’ve utilized caching on almost every web app I’ve ever built. As we know, it saves Server load and improves load-times for our users to a huge degree. But, like most things, it does have its downsides. For instance, many people force aggressive caching on pages of web apps that contain sensitive data. By caching sensitive data, we could allow a malicious agent to access private details of our users.
It makes sense for us to ensure our server tells the Browser not to cache any pages that contain sensitive data i.e. User payment details pages or something similar. Happily, we can do this quite simply by utilizing the Cache-Control
header. For example in Node.js, you may include the following on sensitive pages:
function requestHandler(req, res) { res.setHeader( 'Cache-Control', 'no-cache, no-store, max-age=0, must-revalidate' ); res.setHeader( 'Pragma' , 'no-cache' ); res.setHeader( 'Expires' , 'Sat, 01 Jan 2000 00:00:00 GMT' ); }
By doing this, we are telling the Browser that the page must not be cached, and that it must re-validate the page on reload. The Expires
directive is allowing us to specify a timestamp after which the page is considered totally invalid. I just enter a simple long-past date here.
So, from enforcing HTTPS with Strict Transport Security, to securing our webapp with a Content Security Policy; we’ve covered the main topics, in my opinion to ensuring server-browser security for our web applications. These topics are all techniques I utilise myself and would advocate for use in your applications on an ongoing basis.
One topic I have not been able to cover in this article is cross-site request forgery (CSRF). This is such a big topic, that I think it needs its own article. Hopefully, I’ll be able to supply that for you in the near future! Alongside implementing server security best practices, keeping up to date with vulnerabilities for your chosen server and language is also mega important. The best places to do so are the GitHub repo of the server and CVEDetails.com.
The last article in this series is going to cover a huge element of web security: users, authentication, authorization from both a front-end and back-end perspective. I hope you’ll join me back here on the LogRocket blog in the coming weeks!
Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third-party services are successful, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowThe useReducer React Hook is a good alternative to tools like Redux, Recoil, or MobX.
Node.js v22.5.0 introduced a native SQLite module, which is is similar to what other JavaScript runtimes like Deno and Bun already have.
Understanding and supporting pinch, text, and browser zoom significantly enhances the user experience. Let’s explore a few ways to do so.
Playwright is a popular framework for automating and testing web applications across multiple browsers in JavaScript, Python, Java, and C#. […]