Skip to content

Commit 70543ba

Browse files
authored
Merge pull request #4969 from DataDog/appsec-58263-add-forwarded-header-support-for-remote-ip
Add support of Forwarded header for remote IP detection
2 parents e0c1a24 + 69d6e1f commit 70543ba

File tree

3 files changed

+59
-0
lines changed

3 files changed

+59
-0
lines changed

lib/datadog/core/utils/network.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ module Network
1313
true-client-ip
1414
x-client-ip
1515
x-forwarded
16+
forwarded
1617
forwarded-for
1718
x-cluster-client-ip
1819
fastly-client-ip
@@ -73,6 +74,8 @@ def ip_header(headers, ip_headers_to_check)
7374
next unless value
7475

7576
ips = value.split(',')
77+
ips = process_forwarded_header_values(ips) if name == 'forwarded'
78+
7679
ips.each do |ip|
7780
parsed_ip = ip_to_ipaddr(ip.strip)
7881

@@ -83,6 +86,22 @@ def ip_header(headers, ip_headers_to_check)
8386
nil
8487
end
8588

89+
def process_forwarded_header_values(values)
90+
values.each_with_object([]) do |value, acc|
91+
value.downcase!
92+
93+
value.split(';').each do |tuple_str|
94+
tuple_str.strip!
95+
next unless tuple_str.start_with?('for=')
96+
97+
tuple_str.delete_prefix!('for=')
98+
tuple_str.delete!('"')
99+
100+
acc << tuple_str
101+
end
102+
end
103+
end
104+
86105
# Returns whether the given value is more likely to be an IPv4 than an IPv6 address.
87106
#
88107
# This is done by checking if a dot (`'.'`) character appears before a colon (`':'`) in the value.

sig/datadog/core/utils/network.rbs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ module Datadog
1010
private
1111

1212
def self.ip_header: (Datadog::Core::HeaderCollection headers, ::Array[::String] ip_headers_to_check) -> untyped?
13+
def self.process_forwarded_header_values: (::Array[::String] values) -> ::Array[::String]
1314
def self.strip_zone_specifier: (::String ipv6) -> ::String
1415
def self.strip_ipv6_port: (::String ip) -> ::String
1516
def self.strip_ipv4_port: (::String ip) -> ::String

spec/datadog/core/utils/network_spec.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,45 @@
2727
end
2828
end
2929

30+
context 'with Forwaded header' do
31+
it 'correctly parses a single for IP' do
32+
headers = Datadog::Core::HeaderCollection.from_hash({'Forwarded' => 'for=43.43.43.43;proto=http;by=203.0.113.43'})
33+
34+
result = described_class.stripped_ip_from_request_headers(headers)
35+
expect(result).to eq('43.43.43.43')
36+
end
37+
38+
it 'is case-insencitive to keys in the header' do
39+
headers = Datadog::Core::HeaderCollection.from_hash({'Forwarded' => 'For=43.43.43.43; Proto=http; By=203.0.113.43'})
40+
41+
result = described_class.stripped_ip_from_request_headers(headers)
42+
expect(result).to eq('43.43.43.43')
43+
end
44+
45+
it 'correctly parses multiple for IPs' do
46+
headers = Datadog::Core::HeaderCollection.from_hash(
47+
{'Forwarded' => 'for=127.0.0.1;host="example.host";by=2.2.2.2;proto=http,for="1.1.1.1:6543"'}
48+
)
49+
50+
result = described_class.stripped_ip_from_request_headers(headers)
51+
expect(result).to eq('1.1.1.1')
52+
end
53+
54+
it 'correctly parses IPv6' do
55+
headers = Datadog::Core::HeaderCollection.from_hash({'Forwarded' => 'for="[2001:db8:cafe::17]:4711"'})
56+
57+
result = described_class.stripped_ip_from_request_headers(headers)
58+
expect(result).to eq('2001:db8:cafe::17')
59+
end
60+
61+
it 'returns nil for invalid values' do
62+
headers = Datadog::Core::HeaderCollection.from_hash({'Forwarded' => 'foobar'})
63+
64+
result = described_class.stripped_ip_from_request_headers(headers)
65+
expect(result).to be_nil
66+
end
67+
end
68+
3069
context 'with custom header value' do
3170
it 'returns the IP value if valid public address' do
3271
headers = Datadog::Core::HeaderCollection.from_hash(

0 commit comments

Comments
 (0)