Top Rated Plus on Upwork with a 100% Job Success ScoreView on Upwork
retzdev logo
logo
tech

Setting Security Headers For Web App: NGINX, Express, and React.

by Jarrett Retz

October 29th, 2019

HTTP headers are wonderful little devices. They can help mitigate multiple kinds of attacks and can also be used for authentication.

They can be set on the back-end by Express (in the response object), in the HTML on the front-end, or with web server software.

I recently explored securing a web app that was being served by NGINX and had to do some interesting maneuvers, in my naïveté, to get things to respond the way I wanted. I got myself in some confusing spots, but have certainly furthered my knowledge and hope to extend the readers knowledge in the rest of this article.

I will briefly discuss what, where, and why I used the headers that I did.

I am pulling all the definitions from MDN Web Docs. It's a truly useful resource.

X-Content-Type-Options

"The X-Content-Type-Options response HTTP header is a marker used by the server to indicate that the MIME types advertised in the Content-Type headers should not be changed and be followed. This allows to opt-out of MIME type sniffing, or, in other words, it is a way to say that the webmasters knew what they were doing."

MDN Web Docs
  • Set to: "nosniff"
  • Why: Because I read somewhere that I should do that. I have seen the MIME types files on the server, but have not had a chance to get too interested. However, this protective header seems to be pretty standard with little variation
  • Where: NGINX Config

Cache-Control

"The Cache-Control general-header field is used to specify directives for caching mechanisms in both requests and responses. Caching directives are unidirectional, meaning that a given directive in a request is not implying that the same directive is to be given in the response."

MDN Web Docs
  • Set to: "private, no-cache, no-store, must-revalidate, max-age=0"
  • Why: I was using the OWASP Zed Attack Proxy (ZAP) software and it was suggesting that I set the header with these flags.
  • Where: NGINX Config
   add_header Cache-Control "private, no-cache, no-store, must-revalidate, max-age=0" always;
  • Notes: I was having a hard time setting this header. NGINX sends a default 'Cache-Control' header in the response that I couldn't eliminate when I first requested the page (on subsequent requests, API calls, etc. the default 'Cache-Control' was eliminated in the response and only the one I had set remained). This header can have many configurations, but due to the nature of the application (no images, video files), I did not want the browser storing any of the data that I was sending back and forth. It's not ideal to have clients receive two headers, but if formatted correctly, the browser will combine the headers into one value. I didn't consider this to be a problem because it only happened on the the initial GET request, and all the API requests (having the sensitive data) only show the most restrictive header.

Pragma

"The Pragma HTTP/1.0 general header is an implementation-specific header that may have various effects along the request-response chain. It is used for backwards compatibility with HTTP/1.0 caches where the Cache-Control HTTP/1.1 header is not yet present."

MDN Web Docs
  • Set to: "no-cache"
  • Why: Once again, I was using the OWASP Zed Attack Proxy (ZAP) software and it was suggesting that I set this header in combination with the 'Cache-Control'. It doesn't seem necessary, because I'm always using the 'Cache-Control', but I didn't come across any obvious negative trade-offs.
  • Where: NGINX Config

X-Powered-By

  • Set to: disabled
  • Why: I came across the suggestion, reading varies articles, that this header be eliminated. I thought I could simply set the header—in the NGINX config.—to 'DENY', but that only set a duplicate header. The reason why two 'X-Powered-By' headers is different than two 'Cache-Control' headers is one of obfuscation. It's generally not good to confuse obfuscation with security, but the 'X-Powered-By' header displays 'EXPRESS' in the response header and it's, at the least, one more thing a potential hacker would need to figure out.
  • Where: In the main Express Server File, in the context of other middleware functions, the following line can be added.
app.disable('x-powered-by');

Content-Security-Policy (CSP)

The HTTP Content-Security-Policy response header allows web site administrators to control resources the user agent is allowed to load for a given page. With a few exceptions, policies mostly involve specifying server origins and script endpoints. This helps guard against cross-site scripting attacks (XSS).

MDN Web Docs
  • Set to: I'm going to abbreviate because it's a long string: "default-src 'self'; script-src 'self' 'unsafe-inline'; [...] script-src-elem 'unsafe-inline' kit.fontawesome.com 'self' https://www.google.com/recaptcha/api.js https://www.gstatic.com;"
  • Why: XSS attacks get a lot of attention in cyber-security realms, and having control of one's CSP can be very advantageous. Each part of the string is it's own directive that acts as a white-list for allowed sources on the page. A default deny directive can be set or a default directive can be set to only allow sources from the web application itself. The ZAP tool that I was using made me aware of the precision to which I could set the directives.
  • Where: NGINX Config.. HOWEVER, the CSP was appearing twice, but this time I was able to remove the duplicate from inside the Express Server File. I am serving the app with React-Router and the browser needs to be able to find the right file. Here, I was able to remove the duplicate header in the response.
app.get('/router/*', (req, res) => {
    ...
    res.removeHeader('Content-Security-Policy');
    res.sendFile(path.resolve(__dirname, 'client', 'build', 'index.html'));
  });

Strict-Transport-Security

"The HTTP Strict-Transport-Security response header (often abbreviated as HSTS) lets a web site tell browsers that it should only be accessed using HTTPS, instead of using HTTP."

MDN Web Docs
  • Set to: "max-age=31536000; includeSubdomains; preload"
  • Why: This seemed straightforward, as well. After obtaining an SSL-certificate, all traffic would be directed to the HTTPS port. This directive further cemented that process. I came across a good article on the danger of not doing this, and how traffic could be redirected or cookies stolen. I don't have any subdomains now, but maybe in the future.
  • Where: NGINX Config

X-Frame-Options

"The X-Frame-Options HTTP response header can be used to indicate whether or not a browser should be allowed to render a page in a <frame><iframe><embed> or <object>. Sites can use this to avoid clickjacking attacks, by ensuring that their content is not embedded into other sites."

MDN Web Docs
  • Set to: "SAMEORIGIN"
  • Why: I had set an iframe directive in my Content-Security-Policy, so this wasn't necessary with browsers that supported this header. However, some older browser versions may not, therefore, this header was set to continue to 'cover-my-bases'
  • Where: NGINX Config

X-XSS-Protection

The HTTP X-XSS-Protection response header is a feature of Internet Explorer, Chrome and Safari that stops pages from loading when they detect reflected cross-site scripting (XSS) attacks. Although these protections are largely unnecessary in modern browsers when sites implement a strong Content-Security-Policy that disables the use of inline JavaScript ('unsafe-inline'), they can still provide protections for users of older web browsers that don't yet support CSP.

MDN Web Docs
  • Set to: "1"
  • Why: This protection is enabled—by default—on modern browsers, but some attacks are launched from older browsers to try and nullify modern safeguards, This header is in place for that very reason.
  • Where: NGINX Config

Access-Control-Allow-[Methods, Origin]

The Access-Control-Allow-Headers response header is used in response to a preflight request which includes the Access-Control-Request-Headers to indicate which HTTP headers can be used during the actual request.

MDN Web Docs

These headers can be seen in the response headers on preflight requests, but where did they come from? Unlike many of the other headers, these were set by Cross-Origin-Resource-Sharing (CORS) middleware.

They can be incredibly useful in securing an API. There is a function, found in the linked module documentation, that is as follows:

var whitelist = ['http://example1.com', 'http://example2.com']
var corsOptions = {
  origin: function (origin, callback) {
    if (whitelist.indexOf(origin) !== -1) {
      callback(null, true)
    } else {
      callback(new Error('Not allowed by CORS'))
    }
  }
}

There are more options that can be set to identify allowed methods, and to allow cookies.

var corsOptions = {
  ...,
  credentials: true,            // allow cookies
  methods: ['GET', 'POST']      // allowed methods
}
  • Why: With a CORS policy in place, requests can be denied—or accepted—based on the origin. The API routes in my application are only receiving requests from a small handful of urls. This means the list of 'allowed origins' can be an exclusive group. Being able to whitelist IPs like this can be very beneficial. It can also prevent requests meaning to tamper with information (i.e DELETE, PUT).
  • Where: In the Express server files. Either in the main file, or as middleware on other routes.

I hope you enjoyed reading about some of the basic headers and how I implemented them in my application.

Jarrett Retz

Jarrett Retz is a freelance web application developer and blogger based out of Spokane, WA.

jarrett@retz.dev

Subscribe to get instant updates

Contact

jarrett@retz.dev

Legal

Any code contained in the articles on this site is released under the MIT license. Copyright 2024. Jarrett Retz Tech Services L.L.C. All Rights Reserved.