mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
f674922462
SSL redirect: * Move `:host` and `:port` options within `redirect: { … }`. Deprecate. * Introduce `:status` and `:body` to customize the redirect response. The 301 permanent default makes it difficult to test the redirect and back out of it since browsers remember the 301. Test with a 302 or 307 instead, then switch to 301 once you're confident that all is well. HTTP Strict Transport Security (HSTS): * Shorter max-age. Shorten the default max-age from 1 year to 180 days, the low end for https://www.ssllabs.com/ssltest/ grading and greater than the 18-week minimum to qualify for browser preload lists. * Disabling HSTS. Setting `hsts: false` now sets `hsts: { expires: 0 }` instead of omitting the header. Omitting does nothing to disable HSTS since browsers hang on to your previous settings until they expire. Sending `{ hsts: { expires: 0 }}` flushes out old browser settings and actually disables HSTS: http://tools.ietf.org/html/rfc6797#section-6.1.1 * HSTS Preload. Introduce `preload: true` to set the `preload` flag, indicating that your site may be included in browser preload lists, including Chrome, Firefox, Safari, IE11, and Edge. Submit your site: https://hstspreload.appspot.com
199 lines
5.6 KiB
Ruby
199 lines
5.6 KiB
Ruby
require 'abstract_unit'
|
|
|
|
class SSLTest < ActionDispatch::IntegrationTest
|
|
HEADERS = Rack::Utils::HeaderHash.new 'Content-Type' => 'text/html'
|
|
|
|
attr_accessor :app
|
|
|
|
def build_app(headers: {}, ssl_options: {})
|
|
headers = HEADERS.merge(headers)
|
|
ActionDispatch::SSL.new lambda { |env| [200, headers, []] }, ssl_options
|
|
end
|
|
end
|
|
|
|
class RedirectSSLTest < SSLTest
|
|
def assert_not_redirected(url, headers: {})
|
|
self.app = build_app
|
|
get url, headers: headers
|
|
assert_response :ok
|
|
end
|
|
|
|
def assert_redirected(host: nil, port: nil, status: 301, body: [],
|
|
deprecated_host: nil, deprecated_port: nil,
|
|
from: 'http://a/b?c=d', to: from.sub('http', 'https'))
|
|
|
|
self.app = build_app ssl_options: {
|
|
redirect: { host: host, port: port, status: status, body: body },
|
|
host: deprecated_host, port: deprecated_port
|
|
}
|
|
|
|
get from
|
|
assert_response status
|
|
assert_redirected_to to
|
|
assert_equal body.join, @response.body
|
|
end
|
|
|
|
test 'https is not redirected' do
|
|
assert_not_redirected 'https://example.org'
|
|
end
|
|
|
|
test 'proxied https is not redirected' do
|
|
assert_not_redirected 'http://example.org', headers: { 'HTTP_X_FORWARDED_PROTO' => 'https' }
|
|
end
|
|
|
|
test 'http is redirected to https' do
|
|
assert_redirected
|
|
end
|
|
|
|
test 'redirect with non-301 status' do
|
|
assert_redirected status: 307
|
|
end
|
|
|
|
test 'redirect with custom body' do
|
|
assert_redirected body: ['foo']
|
|
end
|
|
|
|
test 'redirect to specific host' do
|
|
assert_redirected host: 'ssl', to: 'https://ssl/b?c=d'
|
|
end
|
|
|
|
test 'redirect to default port' do
|
|
assert_redirected port: 443
|
|
end
|
|
|
|
test 'redirect to non-default port' do
|
|
assert_redirected port: 8443, to: 'https://a:8443/b?c=d'
|
|
end
|
|
|
|
test 'redirect to different host and non-default port' do
|
|
assert_redirected host: 'ssl', port: 8443, to: 'https://ssl:8443/b?c=d'
|
|
end
|
|
|
|
test 'redirect to different host including port' do
|
|
assert_redirected host: 'ssl:443', to: 'https://ssl:443/b?c=d'
|
|
end
|
|
|
|
test ':host is deprecated, moved within redirect: { host: … }' do
|
|
assert_deprecated do
|
|
assert_redirected deprecated_host: 'foo', to: 'https://foo/b?c=d'
|
|
end
|
|
end
|
|
|
|
test ':port is deprecated, moved within redirect: { port: … }' do
|
|
assert_deprecated do
|
|
assert_redirected deprecated_port: 1, to: 'https://a:1/b?c=d'
|
|
end
|
|
end
|
|
end
|
|
|
|
class StrictTransportSecurityTest < SSLTest
|
|
EXPECTED = 'max-age=15552000'
|
|
|
|
def assert_hsts(expected, url: 'https://example.org', hsts: {}, headers: {})
|
|
self.app = build_app ssl_options: { hsts: hsts }, headers: headers
|
|
get url
|
|
assert_equal expected, response.headers['Strict-Transport-Security']
|
|
end
|
|
|
|
test 'enabled by default' do
|
|
assert_hsts EXPECTED
|
|
end
|
|
|
|
test 'not sent with http:// responses' do
|
|
assert_hsts nil, url: 'http://example.org'
|
|
end
|
|
|
|
test 'defers to app-provided header' do
|
|
assert_hsts 'app-provided', headers: { 'Strict-Transport-Security' => 'app-provided' }
|
|
end
|
|
|
|
test 'hsts: true enables default settings' do
|
|
assert_hsts EXPECTED, hsts: true
|
|
end
|
|
|
|
test 'hsts: false sets max-age to zero, clearing browser HSTS settings' do
|
|
assert_hsts 'max-age=0', hsts: false
|
|
end
|
|
|
|
test ':expires sets max-age' do
|
|
assert_hsts 'max-age=500', hsts: { expires: 500 }
|
|
end
|
|
|
|
test ':expires supports AS::Duration arguments' do
|
|
assert_hsts 'max-age=31557600', hsts: { expires: 1.year }
|
|
end
|
|
|
|
test 'include subdomains' do
|
|
assert_hsts "#{EXPECTED}; includeSubDomains", hsts: { subdomains: true }
|
|
end
|
|
|
|
test 'exclude subdomains' do
|
|
assert_hsts EXPECTED, hsts: { subdomains: false }
|
|
end
|
|
|
|
test 'opt in to browser preload lists' do
|
|
assert_hsts "#{EXPECTED}; preload", hsts: { preload: true }
|
|
end
|
|
|
|
test 'opt out of browser preload lists' do
|
|
assert_hsts EXPECTED, hsts: { preload: false }
|
|
end
|
|
end
|
|
|
|
class SecureCookiesTest < SSLTest
|
|
DEFAULT = %(id=1; path=/\ntoken=abc; path=/; secure; HttpOnly)
|
|
|
|
def get(**options)
|
|
self.app = build_app(**options)
|
|
super 'https://example.org'
|
|
end
|
|
|
|
def assert_cookies(*expected)
|
|
assert_equal expected, response.headers['Set-Cookie'].split("\n")
|
|
end
|
|
|
|
def test_flag_cookies_as_secure
|
|
get headers: { 'Set-Cookie' => DEFAULT }
|
|
assert_cookies 'id=1; path=/; secure', 'token=abc; path=/; secure; HttpOnly'
|
|
end
|
|
|
|
def test_flag_cookies_as_secure_at_end_of_line
|
|
get headers: { 'Set-Cookie' => 'problem=def; path=/; HttpOnly; secure' }
|
|
assert_cookies 'problem=def; path=/; HttpOnly; secure'
|
|
end
|
|
|
|
def test_flag_cookies_as_secure_with_more_spaces_before
|
|
get headers: { 'Set-Cookie' => 'problem=def; path=/; HttpOnly; secure' }
|
|
assert_cookies 'problem=def; path=/; HttpOnly; secure'
|
|
end
|
|
|
|
def test_flag_cookies_as_secure_with_more_spaces_after
|
|
get headers: { 'Set-Cookie' => 'problem=def; path=/; secure; HttpOnly' }
|
|
assert_cookies 'problem=def; path=/; secure; HttpOnly'
|
|
end
|
|
|
|
def test_flag_cookies_as_secure_with_has_not_spaces_before
|
|
get headers: { 'Set-Cookie' => 'problem=def; path=/;secure; HttpOnly' }
|
|
assert_cookies 'problem=def; path=/;secure; HttpOnly'
|
|
end
|
|
|
|
def test_flag_cookies_as_secure_with_has_not_spaces_after
|
|
get headers: { 'Set-Cookie' => 'problem=def; path=/; secure;HttpOnly' }
|
|
assert_cookies 'problem=def; path=/; secure;HttpOnly'
|
|
end
|
|
|
|
def test_flag_cookies_as_secure_with_ignore_case
|
|
get headers: { 'Set-Cookie' => 'problem=def; path=/; Secure; HttpOnly' }
|
|
assert_cookies 'problem=def; path=/; Secure; HttpOnly'
|
|
end
|
|
|
|
def test_no_cookies
|
|
get
|
|
assert_nil response.headers['Set-Cookie']
|
|
end
|
|
|
|
def test_keeps_original_headers_behavior
|
|
get headers: { 'Connection' => %w[close] }
|
|
assert_equal 'close', response.headers['Connection']
|
|
end
|
|
end
|