Skip to content

Commit

Permalink
Trusted hosts (#474)
Browse files Browse the repository at this point in the history
  • Loading branch information
Blacksmoke16 authored Dec 1, 2024
1 parent 5bf4495 commit 9a21585
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 4 deletions.
54 changes: 51 additions & 3 deletions src/components/framework/spec/request_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ require "./spec_helper"

struct ATH::RequestTest < ASPEC::TestCase
def tear_down : Nil
ATH::Request.set_trusted_hosts [] of Regex
ATH::Request.set_trusted_proxies [] of String, :none
ATH::Request.trusted_header_overrides.clear
end
Expand All @@ -21,9 +22,6 @@ struct ATH::RequestTest < ASPEC::TestCase
request.hostname.should eq "::1"
end

# def test_hostname_trusted : Nil
# end

@[DataProvider("mime_type_provider")]
def test_mime_type(format : String, mime_types : Indexable(String)) : Nil
request = ATH::Request.new "GET", "/"
Expand Down Expand Up @@ -446,4 +444,54 @@ struct ATH::RequestTest < ASPEC::TestCase

ATH::Request::ProxyHeader::FORWARDED_PROTO.header.should eq "foo-proto"
end

def test_truested_host_not_set : Nil
request = ATH::Request.new "GET", "/", headers: HTTP::Headers{
"host" => "evil.com",
}

request.host.should eq "evil.com"
end

def test_truested_host_untrusted : Nil
# Add trusted domain, including subdomains
ATH::Request.set_trusted_hosts([/^([a-z]{9}\.)?trusted\.com$/])

request = ATH::Request.new "GET", "/", headers: HTTP::Headers{
"host" => "evil.com",
}

# Untrusted host
expect_raises ATH::Exception::SuspiciousOperation, "Untrusted Host: 'evil.com'" do
request.host
end
end

def test_truested_host_trusted : Nil
# Add trusted domain, including subdomains
ATH::Request.set_trusted_hosts([/^([a-z]{9}\.)?trusted\.com$/])

request = ATH::Request.new "GET", "/"

# Trusted host
request.headers["host"] = "trusted.com"
request.host.should eq "trusted.com"
request.port.should eq 80

request.headers["host"] = "trusted.com:8080"
request.host.should eq "trusted.com"
request.port.should eq 8080

request.headers["host"] = "subdomain.trusted.com:8080"
request.host.should eq "subdomain.trusted.com"
end

def test_truested_host_special_characters : Nil
ATH::Request.set_trusted_hosts([/localhost(\.local){0,1}#,example.com/, /localhost/])

request = ATH::Request.new "GET", "/"

request.headers["host"] = "localhost"
request.host.should eq "localhost"
end
end
4 changes: 4 additions & 0 deletions src/components/framework/src/athena.cr
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,10 @@ module Athena::Framework

def start : Nil
# TODO: Is there a better place to do this?
{% if (trusted_hosts = ADI::CONFIG["framework"]["trusted_hosts"]) && !trusted_hosts.empty? %}
ATH::Request.set_trusted_hosts({{trusted_hosts}})
{% end %}

{% if (trusted_proxies = ADI::CONFIG["framework"]["trusted_proxies"]) && (trusted_headers = ADI::CONFIG["framework"]["trusted_headers"]) %}
ATH::Request.set_trusted_proxies({{trusted_proxies}}, {{trusted_headers}})
{% end %}
Expand Down
7 changes: 7 additions & 0 deletions src/components/framework/src/bundle.cr
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ struct Athena::Framework::Bundle < Athena::Framework::AbstractBundle
# See the [external documentation](/guides/proxies) for more information.
property trusted_headers : Athena::Framework::Request::ProxyHeader = Athena::Framework::Request::ProxyHeader[:forwarded_for, :forwarded_port, :forwarded_proto]

# By default the application can handle requests from any host.
# This property allows configuring regular expression patterns to control what hostnames the application is allowed to serve.
# This effectively prevents [host header attacks](https://www.skeletonscribe.net/2013/05/practical-http-host-header-attacks.html).
#
# If there is at least one pattern defined, requests whose hostname does _NOT_ match any of the patterns, will receive a 400 response.
property trusted_hosts : Array(Regex) = [] of Regex

# Allows overriding the header name to use for a given `ATH::Request::ProxyHeader`.
#
# See the [external documentation](/guides/proxies/#custom-headers) for more information.
Expand Down
29 changes: 28 additions & 1 deletion src/components/framework/src/request.cr
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,20 @@ class Athena::Framework::Request
# Returns the list of trusted proxy IP addresses as set via `.set_trusted_proxies`.
class_getter trusted_proxies : Array(String) = [] of String

# Returns the list of trusted host patterns set via `.set_trusted_hosts`.
class_getter trusted_host_patterns : Array(Regex) = [] of Regex

protected class_getter trusted_header_overrides : Hash(ATH::Request::ProxyHeader, String) = {} of ATH::Request::ProxyHeader => String
protected class_getter trusted_hosts : Array(String) = [] of String

# Allows setting a list of *host_patterns* used to whitelist the allowed hostnames of requests.
# If there is at least one pattern defined, requests whose hostname does _NOT_ match any of the patterns, will receive a 400 response.
#
# See [ATH::Bundle:Schema#trusted_hosts](/Framework/Bundle/Schema/#Athena::Framework::Bundle::Schema#trusted_hosts) for more information.
def self.set_trusted_hosts(host_patterns : Array(Regex)) : Nil
@@trusted_host_patterns = host_patterns.map! { |pattern| /#{pattern}/i }
@@trusted_hosts.clear
end

# Allows setting a list of *trusted_proxies*, and which `ATH::Request::ProxyHeader` should be whitelisted.
# The provided proxies are expected to be either IPv4 and/or IPv6 addresses.
Expand Down Expand Up @@ -210,7 +223,21 @@ class Athena::Framework::Request
raise ATH::Exception::SuspiciousOperation.new "Invalid Host: '#{host}'."
end

# TODO: Trusted hosts
unless @@trusted_host_patterns.empty?
return host if @@trusted_hosts.includes? host

@@trusted_host_patterns.each do |pattern|
if host.matches? pattern
@@trusted_hosts << host
return host
end
end

return unless @is_host_valid
@is_host_valid = false

raise ATH::Exception::SuspiciousOperation.new "Untrusted Host: '#{host}'."
end

host
end
Expand Down

0 comments on commit 9a21585

Please sign in to comment.