1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
|
# frozen_string_literal: true
# :markup: markdown
module ActionDispatch
# # Action Dispatch SSL
#
# This middleware is added to the stack when `config.force_ssl = true`, and is
# passed the options set in `config.ssl_options`. It does three jobs to enforce
# secure HTTP requests:
#
# 1. **TLS redirect**: Permanently redirects `http://` requests to `https://`
# with the same URL host, path, etc. Enabled by default. Set
# `config.ssl_options` to modify the destination URL (e.g. `redirect: {
# host: "secure.widgets.com", port: 8080 }`), or set `redirect: false` to
# disable this feature.
#
# Requests can opt-out of redirection with `exclude`:
#
# config.ssl_options = { redirect: { exclude: -> request { request.path == "/up" } } }
#
# Cookies will not be flagged as secure for excluded requests.
#
# 2. **Secure cookies**: Sets the `secure` flag on cookies to tell browsers
# they must not be sent along with `http://` requests. Enabled by default.
# Set `config.ssl_options` with `secure_cookies: false` to disable this
# feature.
#
# 3. **HTTP Strict Transport Security (HSTS)**: Tells the browser to remember
# this site as TLS-only and automatically redirect non-TLS requests. Enabled
# by default. Configure `config.ssl_options` with `hsts: false` to disable.
#
# Set `config.ssl_options` with `hsts: { ... }` to configure HSTS:
#
# * `expires`: How long, in seconds, these settings will stick. The
# minimum required to qualify for browser preload lists is 1 year.
# Defaults to 2 years (recommended).
#
# * `subdomains`: Set to `true` to tell the browser to apply these
# settings to all subdomains. This protects your cookies from
# interception by a vulnerable site on a subdomain. Defaults to `true`.
#
# * `preload`: Advertise that this site may be included in browsers'
# preloaded HSTS lists. HSTS protects your site on every visit *except
# the first visit* since it hasn't seen your HSTS header yet. To close
# this gap, browser vendors include a baked-in list of HSTS-enabled
# sites. Go to https://hstspreload.org to submit your site for
# inclusion. Defaults to `false`.
#
#
# To turn off HSTS, omitting the header is not enough. Browsers will
# remember the original HSTS directive until it expires. Instead, use the
# header to tell browsers to expire HSTS immediately. Setting `hsts: false`
# is a shortcut for `hsts: { expires: 0 }`.
#
class SSL
# :stopdoc: Default to 2 years as recommended on hstspreload.org.
HSTS_EXPIRES_IN = 63072000
PERMANENT_REDIRECT_REQUEST_METHODS = %w[GET HEAD] # :nodoc:
def self.default_hsts_options
{ expires: HSTS_EXPIRES_IN, subdomains: true, preload: false }
end
def initialize(app, redirect: {}, hsts: {}, secure_cookies: true, ssl_default_redirect_status: nil)
@app = app
@redirect = redirect
@exclude = @redirect && @redirect[:exclude] || proc { !@redirect }
@secure_cookies = secure_cookies
@hsts_header = build_hsts_header(normalize_hsts_options(hsts))
@ssl_default_redirect_status = ssl_default_redirect_status
end
def call(env)
request = Request.new env
if request.ssl?
@app.call(env).tap do |status, headers, body|
set_hsts_header! headers
flag_cookies_as_secure! headers if @secure_cookies && !@exclude.call(request)
end
else
return redirect_to_https request unless @exclude.call(request)
@app.call(env)
end
end
private
def set_hsts_header!(headers)
headers[Constants::STRICT_TRANSPORT_SECURITY] ||= @hsts_header
end
def normalize_hsts_options(options)
case options
# Explicitly disabling HSTS clears the existing setting from browsers by setting
# expiry to 0.
when false
self.class.default_hsts_options.merge(expires: 0)
# Default to enabled, with default options.
when nil, true
self.class.default_hsts_options
else
self.class.default_hsts_options.merge(options)
end
end
# https://tools.ietf.org/html/rfc6797#section-6.1
def build_hsts_header(hsts)
value = +"max-age=#{hsts[:expires].to_i}"
value << "; includeSubDomains" if hsts[:subdomains]
value << "; preload" if hsts[:preload]
value
end
def flag_cookies_as_secure!(headers)
cookies = headers[Rack::SET_COOKIE]
return unless cookies
if Gem::Version.new(Rack::RELEASE) < Gem::Version.new("3")
cookies = cookies.split("\n")
headers[Rack::SET_COOKIE] = cookies.map { |cookie|
if !/;\s*secure\s*(;|$)/i.match?(cookie)
"#{cookie}; secure"
else
cookie
end
}.join("\n")
else
headers[Rack::SET_COOKIE] = Array(cookies).map do |cookie|
if !/;\s*secure\s*(;|$)/i.match?(cookie)
"#{cookie}; secure"
else
cookie
end
end
end
end
def redirect_to_https(request)
[ @redirect.fetch(:status, redirection_status(request)),
{ Rack::CONTENT_TYPE => "text/html; charset=utf-8",
Constants::LOCATION => https_location_for(request) },
(@redirect[:body] || []) ]
end
def redirection_status(request)
if PERMANENT_REDIRECT_REQUEST_METHODS.include?(request.raw_request_method)
301 # Issue a permanent redirect via a GET request.
elsif @ssl_default_redirect_status
@ssl_default_redirect_status
else
307 # Issue a fresh request redirect to preserve the HTTP method.
end
end
def https_location_for(request)
host = @redirect[:host] || request.host
port = @redirect[:port] || request.port
location = +"https://#{host}"
location << ":#{port}" if port != 80 && port != 443
location << request.fullpath
location
end
end
end
|