2017-07-24 16:20:53 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2016-08-06 12:54:50 -04:00
|
|
|
require "abstract_unit"
|
2012-03-16 22:22:25 -04:00
|
|
|
|
|
|
|
class SSLTest < ActionDispatch::IntegrationTest
|
2016-08-06 12:54:50 -04:00
|
|
|
HEADERS = Rack::Utils::HeaderHash.new "Content-Type" => "text/html"
|
2015-09-06 22:24:00 -04:00
|
|
|
|
|
|
|
attr_accessor :app
|
|
|
|
|
|
|
|
def build_app(headers: {}, ssl_options: {})
|
|
|
|
headers = HEADERS.merge(headers)
|
2016-02-24 23:48:53 -05:00
|
|
|
ActionDispatch::SSL.new lambda { |env| [200, headers, []] }, ssl_options.reverse_merge(hsts: { subdomains: true })
|
2015-09-06 22:24:00 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class RedirectSSLTest < SSLTest
|
2016-06-19 21:35:59 -04:00
|
|
|
def assert_not_redirected(url, headers: {}, redirect: {})
|
|
|
|
self.app = build_app ssl_options: { redirect: redirect }
|
2015-09-06 22:24:00 -04:00
|
|
|
get url, headers: headers
|
|
|
|
assert_response :ok
|
|
|
|
end
|
|
|
|
|
2016-06-19 21:35:59 -04:00
|
|
|
def assert_redirected(redirect: {}, from: "http://a/b?c=d", to: from.sub("http", "https"))
|
2015-12-24 12:39:09 -05:00
|
|
|
redirect = { status: 301, body: [] }.merge(redirect)
|
|
|
|
|
2016-06-19 21:35:59 -04:00
|
|
|
self.app = build_app ssl_options: { redirect: redirect }
|
2015-09-06 22:24:00 -04:00
|
|
|
|
|
|
|
get from
|
2015-12-24 12:39:09 -05:00
|
|
|
assert_response redirect[:status] || 301
|
2015-09-06 22:24:00 -04:00
|
|
|
assert_redirected_to to
|
2015-12-24 12:39:09 -05:00
|
|
|
assert_equal redirect[:body].join, @response.body
|
2012-03-16 22:22:25 -04:00
|
|
|
end
|
|
|
|
|
2016-02-28 05:26:12 -05:00
|
|
|
def assert_post_redirected(redirect: {}, from: "http://a/b?c=d",
|
|
|
|
to: from.sub("http", "https"))
|
|
|
|
|
|
|
|
self.app = build_app ssl_options: { redirect: redirect }
|
|
|
|
|
|
|
|
post from
|
|
|
|
assert_response redirect[:status] || 307
|
|
|
|
assert_redirected_to to
|
|
|
|
end
|
|
|
|
|
2016-08-06 12:54:50 -04:00
|
|
|
test "exclude can avoid redirect" do
|
2016-03-03 15:09:58 -05:00
|
|
|
excluding = { exclude: -> request { request.path =~ /healthcheck/ } }
|
2016-02-28 16:48:36 -05:00
|
|
|
|
2016-08-06 12:54:50 -04:00
|
|
|
assert_not_redirected "http://example.org/healthcheck", redirect: excluding
|
|
|
|
assert_redirected from: "http://example.org/", redirect: excluding
|
2016-02-28 16:48:36 -05:00
|
|
|
end
|
|
|
|
|
2016-08-06 12:54:50 -04:00
|
|
|
test "https is not redirected" do
|
|
|
|
assert_not_redirected "https://example.org"
|
2012-03-16 22:22:25 -04:00
|
|
|
end
|
|
|
|
|
2016-08-06 12:54:50 -04:00
|
|
|
test "proxied https is not redirected" do
|
|
|
|
assert_not_redirected "http://example.org", headers: { "HTTP_X_FORWARDED_PROTO" => "https" }
|
2012-03-16 22:22:25 -04:00
|
|
|
end
|
|
|
|
|
2016-08-06 12:54:50 -04:00
|
|
|
test "http is redirected to https" do
|
2015-09-06 22:24:00 -04:00
|
|
|
assert_redirected
|
2012-03-16 22:22:25 -04:00
|
|
|
end
|
|
|
|
|
2016-02-28 05:26:12 -05:00
|
|
|
test "http POST is redirected to https with status 307" do
|
|
|
|
assert_post_redirected
|
|
|
|
end
|
|
|
|
|
2016-08-06 12:54:50 -04:00
|
|
|
test "redirect with non-301 status" do
|
2015-12-24 12:39:09 -05:00
|
|
|
assert_redirected redirect: { status: 307 }
|
2012-03-16 22:22:25 -04:00
|
|
|
end
|
|
|
|
|
2016-08-06 12:54:50 -04:00
|
|
|
test "redirect with custom body" do
|
|
|
|
assert_redirected redirect: { body: ["foo"] }
|
2012-03-16 22:22:25 -04:00
|
|
|
end
|
|
|
|
|
2016-08-06 12:54:50 -04:00
|
|
|
test "redirect to specific host" do
|
|
|
|
assert_redirected redirect: { host: "ssl" }, to: "https://ssl/b?c=d"
|
2013-06-23 20:58:02 -04:00
|
|
|
end
|
|
|
|
|
2016-08-06 12:54:50 -04:00
|
|
|
test "redirect to default port" do
|
2015-12-24 12:39:09 -05:00
|
|
|
assert_redirected redirect: { port: 443 }
|
2012-03-16 22:22:25 -04:00
|
|
|
end
|
|
|
|
|
2016-08-06 12:54:50 -04:00
|
|
|
test "redirect to non-default port" do
|
|
|
|
assert_redirected redirect: { port: 8443 }, to: "https://a:8443/b?c=d"
|
2012-03-16 22:22:25 -04:00
|
|
|
end
|
|
|
|
|
2016-08-06 12:54:50 -04:00
|
|
|
test "redirect to different host and non-default port" do
|
|
|
|
assert_redirected redirect: { host: "ssl", port: 8443 }, to: "https://ssl:8443/b?c=d"
|
2012-03-16 22:22:25 -04:00
|
|
|
end
|
|
|
|
|
2016-08-06 12:54:50 -04:00
|
|
|
test "redirect to different host including port" do
|
|
|
|
assert_redirected redirect: { host: "ssl:443" }, to: "https://ssl:443/b?c=d"
|
2013-01-04 11:14:24 -05:00
|
|
|
end
|
|
|
|
|
2016-08-06 12:54:50 -04:00
|
|
|
test "no redirect with redirect set to false" do
|
|
|
|
assert_not_redirected "http://example.org", redirect: false
|
2015-12-24 12:39:09 -05:00
|
|
|
end
|
2015-09-06 22:24:00 -04:00
|
|
|
end
|
2012-03-16 22:22:25 -04:00
|
|
|
|
2015-09-06 22:24:00 -04:00
|
|
|
class StrictTransportSecurityTest < SSLTest
|
2016-08-06 12:54:50 -04:00
|
|
|
EXPECTED = "max-age=15552000"
|
|
|
|
EXPECTED_WITH_SUBDOMAINS = "max-age=15552000; includeSubDomains"
|
2012-03-16 22:22:25 -04:00
|
|
|
|
2016-08-06 12:54:50 -04:00
|
|
|
def assert_hsts(expected, url: "https://example.org", hsts: { subdomains: true }, headers: {})
|
2015-09-06 22:24:00 -04:00
|
|
|
self.app = build_app ssl_options: { hsts: hsts }, headers: headers
|
|
|
|
get url
|
2017-01-18 04:41:32 -05:00
|
|
|
if expected.nil?
|
|
|
|
assert_nil response.headers["Strict-Transport-Security"]
|
|
|
|
else
|
|
|
|
assert_equal expected, response.headers["Strict-Transport-Security"]
|
|
|
|
end
|
2012-03-16 22:22:25 -04:00
|
|
|
end
|
|
|
|
|
2016-08-06 12:54:50 -04:00
|
|
|
test "enabled by default" do
|
2016-02-24 23:48:53 -05:00
|
|
|
assert_hsts EXPECTED_WITH_SUBDOMAINS
|
2015-09-06 22:24:00 -04:00
|
|
|
end
|
2012-03-19 15:28:15 -04:00
|
|
|
|
2016-08-06 12:54:50 -04:00
|
|
|
test "not sent with http:// responses" do
|
|
|
|
assert_hsts nil, url: "http://example.org"
|
2012-03-19 15:28:15 -04:00
|
|
|
end
|
|
|
|
|
2016-08-06 12:54:50 -04:00
|
|
|
test "defers to app-provided header" do
|
|
|
|
assert_hsts "app-provided", headers: { "Strict-Transport-Security" => "app-provided" }
|
2015-09-06 22:24:00 -04:00
|
|
|
end
|
2012-03-19 15:28:15 -04:00
|
|
|
|
2016-08-06 12:54:50 -04:00
|
|
|
test "hsts: true enables default settings" do
|
2016-06-19 21:35:59 -04:00
|
|
|
assert_hsts EXPECTED_WITH_SUBDOMAINS, hsts: true
|
2013-06-26 16:59:16 -04:00
|
|
|
end
|
|
|
|
|
2016-08-06 12:54:50 -04:00
|
|
|
test "hsts: false sets max-age to zero, clearing browser HSTS settings" do
|
2016-06-19 21:35:59 -04:00
|
|
|
assert_hsts "max-age=0; includeSubDomains", hsts: false
|
2015-09-06 22:24:00 -04:00
|
|
|
end
|
2013-06-26 16:59:16 -04:00
|
|
|
|
2016-08-06 12:54:50 -04:00
|
|
|
test ":expires sets max-age" do
|
2016-06-19 21:35:59 -04:00
|
|
|
assert_hsts "max-age=500; includeSubDomains", hsts: { expires: 500 }
|
2015-09-06 22:24:00 -04:00
|
|
|
end
|
2013-06-26 16:59:16 -04:00
|
|
|
|
2016-08-06 12:54:50 -04:00
|
|
|
test ":expires supports AS::Duration arguments" do
|
2017-01-08 17:43:49 -05:00
|
|
|
assert_hsts "max-age=31556952; includeSubDomains", hsts: { expires: 1.year }
|
2013-06-26 16:59:16 -04:00
|
|
|
end
|
|
|
|
|
2016-08-06 12:54:50 -04:00
|
|
|
test "include subdomains" do
|
2015-09-06 22:24:00 -04:00
|
|
|
assert_hsts "#{EXPECTED}; includeSubDomains", hsts: { subdomains: true }
|
|
|
|
end
|
2013-06-26 16:59:16 -04:00
|
|
|
|
2016-08-06 12:54:50 -04:00
|
|
|
test "exclude subdomains" do
|
2015-09-06 22:24:00 -04:00
|
|
|
assert_hsts EXPECTED, hsts: { subdomains: false }
|
2012-03-19 15:28:15 -04:00
|
|
|
end
|
|
|
|
|
2016-08-06 12:54:50 -04:00
|
|
|
test "opt in to browser preload lists" do
|
2016-06-19 21:35:59 -04:00
|
|
|
assert_hsts "#{EXPECTED_WITH_SUBDOMAINS}; preload", hsts: { preload: true }
|
2015-09-06 22:24:00 -04:00
|
|
|
end
|
2013-06-24 05:01:30 -04:00
|
|
|
|
2016-08-06 12:54:50 -04:00
|
|
|
test "opt out of browser preload lists" do
|
2016-06-19 21:35:59 -04:00
|
|
|
assert_hsts EXPECTED_WITH_SUBDOMAINS, hsts: { preload: false }
|
2013-06-24 05:01:30 -04:00
|
|
|
end
|
2015-09-06 22:24:00 -04:00
|
|
|
end
|
2013-06-24 05:01:30 -04:00
|
|
|
|
2015-09-06 22:24:00 -04:00
|
|
|
class SecureCookiesTest < SSLTest
|
|
|
|
DEFAULT = %(id=1; path=/\ntoken=abc; path=/; secure; HttpOnly)
|
|
|
|
|
|
|
|
def get(**options)
|
|
|
|
self.app = build_app(**options)
|
2016-08-06 12:54:50 -04:00
|
|
|
super "https://example.org"
|
2015-09-06 22:24:00 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def assert_cookies(*expected)
|
2016-08-06 12:54:50 -04:00
|
|
|
assert_equal expected, response.headers["Set-Cookie"].split("\n")
|
2015-09-06 22:24:00 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def test_flag_cookies_as_secure
|
2016-08-06 12:54:50 -04:00
|
|
|
get headers: { "Set-Cookie" => DEFAULT }
|
|
|
|
assert_cookies "id=1; path=/; secure", "token=abc; path=/; secure; HttpOnly"
|
2012-03-16 22:22:25 -04:00
|
|
|
end
|
|
|
|
|
2015-09-06 22:24:00 -04:00
|
|
|
def test_flag_cookies_as_secure_at_end_of_line
|
2016-08-06 12:54:50 -04:00
|
|
|
get headers: { "Set-Cookie" => "problem=def; path=/; HttpOnly; secure" }
|
|
|
|
assert_cookies "problem=def; path=/; HttpOnly; secure"
|
2015-09-06 22:24:00 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def test_flag_cookies_as_secure_with_more_spaces_before
|
2016-08-06 12:54:50 -04:00
|
|
|
get headers: { "Set-Cookie" => "problem=def; path=/; HttpOnly; secure" }
|
|
|
|
assert_cookies "problem=def; path=/; HttpOnly; secure"
|
2012-03-16 22:22:25 -04:00
|
|
|
end
|
|
|
|
|
2015-09-06 22:24:00 -04:00
|
|
|
def test_flag_cookies_as_secure_with_more_spaces_after
|
2016-08-06 12:54:50 -04:00
|
|
|
get headers: { "Set-Cookie" => "problem=def; path=/; secure; HttpOnly" }
|
|
|
|
assert_cookies "problem=def; path=/; secure; HttpOnly"
|
2012-03-16 22:22:25 -04:00
|
|
|
end
|
|
|
|
|
2015-09-06 22:24:00 -04:00
|
|
|
def test_flag_cookies_as_secure_with_has_not_spaces_before
|
2016-08-06 12:54:50 -04:00
|
|
|
get headers: { "Set-Cookie" => "problem=def; path=/;secure; HttpOnly" }
|
|
|
|
assert_cookies "problem=def; path=/;secure; HttpOnly"
|
2012-03-16 22:22:25 -04:00
|
|
|
end
|
|
|
|
|
2015-09-06 22:24:00 -04:00
|
|
|
def test_flag_cookies_as_secure_with_has_not_spaces_after
|
2016-08-06 12:54:50 -04:00
|
|
|
get headers: { "Set-Cookie" => "problem=def; path=/; secure;HttpOnly" }
|
|
|
|
assert_cookies "problem=def; path=/; secure;HttpOnly"
|
2014-04-09 12:48:53 -04:00
|
|
|
end
|
|
|
|
|
2015-09-06 22:24:00 -04:00
|
|
|
def test_flag_cookies_as_secure_with_ignore_case
|
2016-08-06 12:54:50 -04:00
|
|
|
get headers: { "Set-Cookie" => "problem=def; path=/; Secure; HttpOnly" }
|
|
|
|
assert_cookies "problem=def; path=/; Secure; HttpOnly"
|
2012-03-16 22:22:25 -04:00
|
|
|
end
|
|
|
|
|
2015-12-24 12:39:09 -05:00
|
|
|
def test_cookies_as_not_secure_with_secure_cookies_disabled
|
2016-08-06 12:54:50 -04:00
|
|
|
get headers: { "Set-Cookie" => DEFAULT }, ssl_options: { secure_cookies: false }
|
2016-01-07 03:22:08 -05:00
|
|
|
assert_cookies(*DEFAULT.split("\n"))
|
2015-12-24 12:39:09 -05:00
|
|
|
end
|
|
|
|
|
2015-09-06 22:24:00 -04:00
|
|
|
def test_no_cookies
|
|
|
|
get
|
2016-08-06 12:54:50 -04:00
|
|
|
assert_nil response.headers["Set-Cookie"]
|
2012-03-16 22:22:25 -04:00
|
|
|
end
|
2015-06-14 10:10:27 -04:00
|
|
|
|
|
|
|
def test_keeps_original_headers_behavior
|
2016-08-06 12:54:50 -04:00
|
|
|
get headers: { "Connection" => %w[close] }
|
|
|
|
assert_equal "close", response.headers["Connection"]
|
2015-06-14 10:10:27 -04:00
|
|
|
end
|
2012-03-16 22:22:25 -04:00
|
|
|
end
|