How to get an A+ Security Grade For Your Website on Mozilla Observatory (Part 2 - Nginx)

8 November 2021
Mozilla Observatory A+ Grade

After rewriting the FormBlob website with Hugo, I wanted to ensure that the website adhered to security best practices and was not vulnerable to any known issues. I began looking for a measure for website security and found Mozilla Observatory. On my very first scan, I received a C grade. While not particularly bad, I wanted to rectify any flaws found to achieve the best score I could. It was also an opportunity to learn a little more about website security.

So what is Mozilla Observatory?

The following quote is taken directly from the FAQ at https://observatory.mozilla.org/faq.

The Observatory tests for preventative measure against cross-site scripting attacks, man-in-the-middle attacks, cross-domain information leakage, cookie compromise, content delivery network compromise, and improperly issued certificates. However, it does not test for outdated software versions, SQL injection vulnerabilities, vulnerable content management system plugins, improper password creation policies or storage procedures, and more. These are just as important as what the Observatory tests for, and site operators should not be neglectful of them simply because they score well on the Observatory.

While it may sound like a mouthful, these tests largely measure how vulnerable your website is to some of the most common malicious attacks that prey on a website developer’s negligence in setting up secure networking configurations.

FormBlob Setup

I will split the discussion in this article into two parts - one for https://formblob.com and another for https://build.formblob.com. This is because the main site is a static site built using Hugo and deployed on Netlify, while the form builder site is a React app deployed on AWS ECS behind an elastic load balancer (ELB).

This is part two of the article discussing how you would set up Nginx to achieve an A+ grade. For part one discussing how to set up Netlify, click here.

Getting Started

In order to improve your grade for Mozilla Observatory scans, you’ll need to add HTTP response headers. To add HTTP response headers to Nginx, you will need to edit the server context in the nginx.conf file. Here, I assume that you are familiar enough with Nginx to set up the config file. I list the full list of headers and config added to achieve an A+ grade below.

server {
  listen 80;
  ...

  return 301 https://$host$request_uri;
}

server {
  listen 443:
  ...

  # Only connect to this site and subdomains via HTTPS for the next two years
  add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";

  # Only allow my site to frame itself
  add_header X-Frame-Options "SAMEORIGIN";

  # Prevent browsers from incorrectly detecting non-scripts as scripts
  add_header X-Content-Type-Options "nosniff";

  # Block pages from loading when they detect reflected XSS attacks
  add_header X-Xss-Protection "1; mode=block";

  # Only send the shortened referrer to a foreign origin, full referrer to a local host
  add_header Referrer-Policy "strict-origin-when-cross-origin";

  # Configue CSP
  add_header Content-Security-Policy "default-src 'self' formblob.com *.formblob.com fblob.me *.fblob.me; connect-src 'self' formblob.com *.formblob.com wss://*.formblob.com fblob.me *.fblob.me https://sockjs-mt1.pusher.com firebase.googleapis.com firebaseinstallations.googleapis.com https://www.googleapis.com/webfonts/ www.google-analytics.com; font-src 'self' https://fonts.gstatic.com; img-src https: data: blob: www.googletagmanager.com https://ssl.gstatic.com https://www.gstatic.com; media-src https: data: blob:; script-src 'self' 'unsafe-eval' js.stripe.com https://www.googletagmanager.com https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ 'sha256-TQ+edvHvrQ4+h8G+tZQdFsQSrUAeSfwE/D8KjAzDnT0='; style-src 'self' 'unsafe-inline' fonts.googleapis.com; frame-src 'self' formblob.com *.formblob.com fblob.me *.fblob.me js.stripe.com https://www.google.com/recaptcha/ https://recaptcha.google.com/recaptcha/; frame-ancestors 'self' formblob.com *.formblob.com fblob.me *.fblob.me;";

  ...
}

Test Scores

Mozilla Observatory scores your website on a predefined set of tests. Let’s go through each of the tests individually.

Mozilla Observatory Test Scores
Content Security Policy

Content Security Policy (CSP) gives you fine-grained control over which resources can be loaded on your website and from where such resources are allowed. It aims to protect your website from cross-site scripting (XSS) vulnerabilities. XSS vulnerabilities stem from unsafe inline Javascript and disabling this effectively eliminates most XSS attacks. However, disabling unsafe inline also means that all Javascript must be loaded from <script src=""> tags. Javascript inside <script> tags but not loaded via src will fail to execute.

Configuring CSP is a tedious process that requires you to evaluate the sources of all the scripts and styles loaded on your website. Before we dive into each policy, here’s a look at the complete CSP I deployed on FormBlob. Note that I split each directive onto a separate line here for readability. When deploying, the entire CSP must be in a single line.

/*
  # Configure CSP
  add_header Content-Security-Policy "
    default-src 'self' formblob.com *.formblob.com fblob.me *.fblob.me;
    connect-src 'self' formblob.com *.formblob.com wss://*.formblob.com fblob.me *.fblob.me https://sockjs-mt1.pusher.com firebase.googleapis.com firebaseinstallations.googleapis.com https://www.googleapis.com/webfonts/ www.google-analytics.com;
    font-src 'self' https://fonts.gstatic.com;
    img-src https: data: blob: www.googletagmanager.com https://ssl.gstatic.com https://www.gstatic.com;
    media-src https: data: blob:;
    script-src 'self' 'unsafe-eval' js.stripe.com https://www.googletagmanager.com https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ 'sha256-TQ+edvHvrQ4+h8G+tZQdFsQSrUAeSfwE/D8KjAzDnT0=';
    style-src 'self' 'unsafe-inline' fonts.googleapis.com;
    frame-src 'self' formblob.com *.formblob.com fblob.me *.fblob.me js.stripe.com https://www.google.com/recaptcha/ https://recaptcha.google.com/recaptcha/;
    frame-ancestors 'self' formblob.com *.formblob.com fblob.me *.fblob.me;";

I used a subset of the full list of directives that are relevant to FormBlob. I will limit my discussion here to only these directives and a few others that are common. You can read more about the full list of directives here. Also, as a start, I recommend using the header Content-Security-Policy-Report-Only instead of Content-Security-Policy to get a report of all the violations without breaking your website.

  • default-src is a fallback directive for the other fetch directives. Directives that are specified have no inheritance, while directives that are not specified will fall back to the value of default-src. Here, you want to include ‘self’ (the origin site with the same scheme and port) and other trusted domains. The recommended setting for this is none, which will require you to set almost every other directive.

  • connect-src provides control over fetch requests, XHR, eventsource, beacon and websockets connections. This defines any resources that you need to connect to. For websocket connections, you will have to set the relevant scheme. For example, wss://*.formblob.com.

  • font-src specifies which URLs to load fonts from. If you are using Google Fonts, this directive should include https://fonts.gstatic.com data:.

  • img-src specifies the URLs that images can be loaded from. If you use Google Tag Manager, this directive should include www.googletagmanager.com https://ssl.gstatic.com https://www.gstatic.com.

  • media-src specifies the URLs from which video, audio and text track resources can be loaded from.

  • manifest-src specifies the URLs that application manifests may be loaded from.

  • script-src specifies the locations from which a script can be executed from. If you use Google Tag Manager, the recommended approach is to use the nonce method for GTM scripts. I include the 'unsafe-eval' keyword here because FormBlob uses a heavily sanitized Function() method to evaluate previously set fields to personalise the experience for users completing forms. This is a quintessential feature across all form builders.

    • In this directive, I also include a hash keyword 'sha256-TQ+edvHvrQ4+h8G+tZQdFsQSrUAeSfwE/D8KjAzDnT0='. You may use the hashed script as a keyword to allow any script to be loaded. This hash is automatically generated in the report in the console for any script that violates the directive. You can copy the hash together with the single quotes directly into the directive to allow the script.
  • style-src controls from where styles get applied to a document. This includes <link> elements, @import rules, and requests originating from a Link HTTP response header field. You may notice that I include the 'unsafe-inline' keyword. This is because I directly alter inline styles using Material UI. Again, this is a potential vulnerability that you should avoid if you can. If using Google Fonts and/or Google Tag Manager, you should include these fonts.googleapis.com https://tagmanager.google.com.

  • frame-src restricts the URLs that can be embedded on the site.

  • form-action restricts the URLs which the forms can submit to.

  • frame-ancestors restricts the URLs that can embed the requested resource inside of <frame>, <iframe>, <object>, <embed>, or <applet> elements. This directive does not fallback to default-src directive. If this is set, X-Frame-Options is ignored by user agents.

  • upgrade-insecure-requests instructs user agents to rewrite URL schemes, changing HTTP to HTTPS. This directive is for websites with large numbers of old URL’s that need to be rewritten.

Cookies

For FormBlob all cookies used are created on the API server for the purpose of session tracking, and I do not set any cookies on the frontend server hosting the website. However, if you do need to set cookies using Nginx, ensure that your cookies use the Secure flag and set the appropriate SameSite attribute. Session cookies should also have the HttpOnly flag. Cookies should also have an expiration as soon as is necessary.

For the SameSite attribute, this is a direct quote from https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie.

Strict means that the browser sends the cookie only for same-site requests, that is, requests originating from the same site that set the cookie. If a request originates from a URL different from the current one, no cookies with the SameSite=Strict attribute are sent. Lax means that the cookie is not sent on cross-site requests, such as on requests to load images or frames, but is sent when a user is navigating to the origin site from an external site (for example, when following a link). This is the default behavior if the SameSite attribute is not specified.

# Add a cookie with [name] and [value] that expires in 1 day (86400 seconds)
# and restricted to formblob.com and its subdomains
add_header Set-Cookie "[name]=[value]; Max-Age=86400; Path=/; Domain=formblob.com HttpOnly; Secure";
Cross-origin Resource Sharing

Cross-origin Resource Sharing (CORS) is not needed for most websites and FormBlob is no exception. This should not be set unless specifically needed.

HTTP Public Key Pinning

HTTP Public Key Pinning is only required for maximum risk sites. It is not recommended for most sites and you would probably not need it.

HTTP Strict Transport Security

HTTP Strict Transport Security (HSTS) notifies user agents to only connect to a given site over HTTPS, even if the scheme chosen was HTTP. It works in tandem with HTTP to HTTPS redirects and should be set on response headers from the HTTPS request. The recommended setting is Strict-Transport-Security: max-age=63072000; includeSubdomains. If the includeSubdomains flag is present, all requests to subdomains will also be upgraded to HTTPS. Ensure that all subdomains can handle HTTPS traffic before including this flag.

# Only connect to this site and subdomains via HTTPS for the next two years
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
HTTPS Redirection

Sites that listen on port 80 should redirect to the same resource on HTTPS. Once the redirection has occurred, HSTS ensures that all future HTTP requests are instead sent directly to the secure site. The setting below redirects all HTTP traffic to the exact same resource on HTTPS.

server {
  listen 80;
  ...

  return 301 https://$host$request_uri;
}
Referrer Policy

There are four options for this:

  • no-referrer: never send the Referer header
  • same-origin: send referrer, but only on requests to the same origin
  • strict-origin: send referrer to all origins, but only the URL sans path (e.g. https://example.com/)
  • strict-origin-when-cross-origin: send full referrer on same origin, URL sans path on foreign origin

By default, you would want to use strict-origin-when-cross-origin. This protects user privacy on cross-origin requests but allows you to track users using analytics within your own site.

# Only send the shortened referrer to a foreign origin, full referrer to a local host
add_header Referrer-Policy "strict-origin-when-cross-origin";
Subresource Integrity

Subresource integrity (SRI) protects against attackers modifying the contents of Javascript libraries hosted on content delivery networks (CDNs) in order to create vulnerabilities in websites that make use of that hosted library. SRI uses a hash of the library’s content to verify that a library has not been changed. If it has, the website will refuse to load the library. Here’s a good resource to generate SRI hashes. Below is an example of an embedded form from FormBlob using SRI.

<script
  src="https://unpkg.com/@jeremyling/form-renderer@0.4.18/dist/web.min.js"
  integrity="sha512-GNYjePKvIQqjostGLfmWjMfBrGWYenBxLw/ObM7T+iU7PIcWV6vtigcKL+BVw0COxpCFt4ksRKDkenGZuCKl4g=="
  crossorigin="anonymous"
></script>
X-Content-Type-Options

X-Content-Type-Options is a header supported by Internet Explorer, Chrome and Firefox 50+ that tells user agents not to load scripts and stylesheets unless the server indicates the correct MIME type. Without this header, these browsers can incorrectly detect files as scripts and stylesheets, leading to XSS attacks.

# Prevent browsers from incorrectly detecting non-scripts as scripts
add_header X-Content-Type-Options "nosniff";
X-Frame-Options

X-Frame-Options controls where your site may be framed within an iframe. This helps to prevent clickjacking, in which an attacker frames your site within a malicious platform that tricks users into clicking on links which the attacker has control over.

# Only allow my site to frame itself
add_header X-Frame-Options "SAMEORIGIN";
X-XSS-Protection

X-XSS-Protection is a feature of Internet Explorer and Chrome that stops pages from loading when they detect reflected XSS attacks. While a strong CSP may make this header redundant, it protects users on older browsers that do not support CSP.

# Block pages from loading when they detect reflected XSS attacks
add_header X-Xss-Protection "1; mode=block";

Complete nginx.conf Configuration

We have successfully gone through each of the tests and set up configurations to help us pass them. If you have followed along, you should receive at least an A grade with this setup. Here’s a recap of the full configuration.

server {
  listen 80;
  ...

  return 301 https://$host$request_uri;
}

server {
  listen 443:
  ...

  # Only connect to this site and subdomains via HTTPS for the next two years
  add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";

  # Only allow my site to frame itself
  add_header X-Frame-Options "SAMEORIGIN";

  # Prevent browsers from incorrectly detecting non-scripts as scripts
  add_header X-Content-Type-Options "nosniff";

  # Block pages from loading when they detect reflected XSS attacks
  add_header X-Xss-Protection "1; mode=block";

  # Only send the shortened referrer to a foreign origin, full referrer to a local host
  add_header Referrer-Policy "strict-origin-when-cross-origin";

  # Configue CSP
  add_header Content-Security-Policy "default-src 'self' formblob.com *.formblob.com fblob.me *.fblob.me; connect-src 'self' formblob.com *.formblob.com wss://*.formblob.com fblob.me *.fblob.me https://sockjs-mt1.pusher.com firebase.googleapis.com firebaseinstallations.googleapis.com https://www.googleapis.com/webfonts/ www.google-analytics.com; font-src 'self' https://fonts.gstatic.com; img-src https: data: blob: www.googletagmanager.com https://ssl.gstatic.com https://www.gstatic.com; media-src https: data: blob:; script-src 'self' 'unsafe-eval' js.stripe.com https://www.googletagmanager.com https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ 'sha256-TQ+edvHvrQ4+h8G+tZQdFsQSrUAeSfwE/D8KjAzDnT0='; style-src 'self' 'unsafe-inline' fonts.googleapis.com; frame-src 'self' formblob.com *.formblob.com fblob.me *.fblob.me js.stripe.com https://www.google.com/recaptcha/ https://recaptcha.google.com/recaptcha/; frame-ancestors 'self' formblob.com *.formblob.com fblob.me *.fblob.me;";

  ...
}

Concluding Remarks

With an A+ security grade, FormBlob ranks as the most secure form building platform in the market. FormBlob respects data privacy and aims to ensure all data is secure from malicious attacks. We make an effort to meet and exceed all established security requirements. While not officially HIPAA or NIST certified due to how new we currently are, our infrastructure and security setup meet these requirements and we are confident of getting certified in the long run. Here are the Mozilla Observatory results of other popular form builders that we benchmark against.

Benchmarks