Secure Response Headers

Check out secure web development and secure response body for more notes.

This document assumes HTTPS has been enabled. Ruby samples focus on Roda. Check its documentation for further details regarding where/how to use the code samples.

Table of Contents

Protect Browser

  • Ensure browsers only access your site through HTTPS to prevent SslStrip attacks.
response["Strict-Transport-Security"] = "max-age=31536000; includeSubdomains; preload"
  • Prevent Clickjacking. Restrict where the response can be embedded to. Don’t use the associated meta tag, it has no effect.
response["X-Frame-Options"] = "deny"
  • Control which referrer info should be included with requests made. More granular referrer policies can be set at view level.
response["Referrer-Policy"] = "no-referrer"
  • Deny clients, such as PDF readers, permission to handle data across domains.
response["X-Permitted-Cross-Domain-Policies"] = none

Cross-Site Scripting (XSS)

  • Enable XSS protection and block rendering malicious pages.
response["X-XSS-Protection"] = "1; mode=block"
  • Stop browser from guessing content type in case users manage to store XSS attacks.
response["X-Content-Type-Options"] = "nosniff"

Content Security Policy

Restrict where the browser can load resources from to prevent the execution of unknown scripts.

# Web apps customize these according to their needs
cdns = %w[trusted-cdn.com other-cdn.com]
response["Content-Security-Policy"] = "default-src 'none'; script-src 'self' #{cdns}; connect-src 'self'; img-src 'self'; style-src 'self';"

# For pure JSON APIs
response["Content-Security-Policy"] = "default-src 'none'"

To help prevent web cache deception attacks add the require-sri-for directive.

response["Content-Security-Policy"] = "require-sri-for script style; ..."

The secure response body cheat sheet has some details on how to add scripts, and styles using the sub-resource integrity (SRI) check.

  • Never store sensitive data in a cookie to help prevent replay attacks.
  • Never hide cryptographic credentials in client software nor on client systems.
  • Serve cookies only over HTTPS using the Secure flag.
  • Limit cookie access level to HttpOnly.
  • Use session cookies over persistent ones.
  • Session secret:
    • Use a different secret token in each environment.
    • Inject secrets using env vars. Avoid hard coding them.
    • Set a securerandom number as a fallback.
require "roda"

class Web < Roda
  use Rack::Session::Cookie, {
    key: "_app_session",
    path: "/",
    httponly: true,
    secure: true,
    expire_after: nil, # Session cookie
    secret: ENV.fetch("COOKIE_KEY") { SecureRandom.hex 64 }
  }
end

Cross-Site Request Forgery (CSRF)

CSRF attacks affect end-points which change state on the server. Which has more to do with a verb’s safety than its idempotency.

According to OWASP, there are numerous ways to defend against CSRF. Yet, a session cookie by itself is not enough to prevent this type of attack. Although, when combined with token synchronization it makes a sound defence.

To avoid hidden pitfalls rely in a gem, toolkit, or framework that takes care of CSRF protection. Make sure any non GET requests always require a valid token, though.

require "roda"

class WebSiteWithAjax < Roda

  # cookie configuration here

  plugin :type_routing,
    default_type: :html,
    exclude: :xml,
    types: {
      json: "application/json; charset=utf-8",
      html: "text/html; charset=utf-8"
    }

  plugin :csrf,
    raise: false
end

Cross-Origin Resource Sharing (CORS)

CORS enables communication and resource sharing between trusted partners.

Public Resources

These are dangerous since they allow connections from anywhere…

response["Access-Control-Allow-Origin"] = "*"
response["Access-Control-Allow-Origin"] = "null"

and neutralize credentials.

response["Access-Control-Allow-Credentials"] = true # actually behaves like false

Wildcards aren’t partials. They aren’t safe to use:

# Invalid
response["Access-Control-Allow-Origin"] = "https://*.example.com"

# Valid but UNSAFE. Accepts connections from http://evil.example.com
response["Access-Control-Allow-Origin"] = "*.example.com"

Single Origin

By default CORS only works with a single origin. Sub-domains aren’t included.

response["Access-Control-Allow-Origin"] = "https://www.example.com/"
response["Access-Control-Allow-Credentials"] = true
response["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE"

Multiple Origins

A common way of dealing with the single-origin limitation is to dynamically consume the request header. Since that technique depends on input:

  • Avoid trusting multiple origins.
  • Rely on a well tested gem that:
    • Validates URL format.
    • Whitelist sub-domains.
    • Adds the Vary header with Origin value.

Gems & Tools

Gems:

Secure headers tools:

Resources

Guides & Cheat Sheets

Articles