-
Notifications
You must be signed in to change notification settings - Fork 375
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2665 from DataDog/appsec-client-ip-extraction
[APPSEC-7946] Update the client IP extraction resolution base on the latest RFC.
- Loading branch information
Showing
10 changed files
with
451 additions
and
318 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'ipaddr' | ||
require_relative '../vendor/ipaddr' | ||
|
||
module Datadog | ||
module Core | ||
module Utils | ||
# Common Network utility functions. | ||
module Network | ||
DEFAULT_IP_HEADERS_NAMES = %w[ | ||
x-forwarded-for | ||
x-real-ip | ||
true-client-ip | ||
x-client-ip | ||
x-forwarded | ||
forwarded-for | ||
x-cluster-client-ip | ||
fastly-client-ip | ||
cf-connecting-ip | ||
cf-connecting-ipv6 | ||
].freeze | ||
|
||
class << self | ||
# Returns a client IP associated with the request if it was | ||
# retrieved successfully. | ||
# | ||
# | ||
# @param [Datadog::Core::HeaderCollection, #get, nil] headers The request headers | ||
# @param [Array<String>] list of headers to check. | ||
# @return [String] IP value without the port and the zone indentifier. | ||
# @return [nil] when no valid IP value found. | ||
def stripped_ip_from_request_headers(headers, ip_headers_to_check: DEFAULT_IP_HEADERS_NAMES) | ||
ip = ip_header(headers, ip_headers_to_check) | ||
|
||
ip ? ip.to_s : nil | ||
end | ||
|
||
# @param [String] IP value. | ||
# @return [String] IP value without the port and the zone indentifier. | ||
# @return [nil] when no valid IP value found. | ||
def stripped_ip(ip) | ||
ip = ip_to_ipaddr(ip) | ||
ip ? ip.to_s : nil | ||
end | ||
|
||
private | ||
|
||
# @param [String] IP value. | ||
# @return [IPaddr] | ||
# @return [nil] when no valid IP value found. | ||
def ip_to_ipaddr(ip) | ||
return unless ip | ||
|
||
clean_ip = if likely_ipv4?(ip) | ||
strip_ipv4_port(ip) | ||
else | ||
strip_zone_specifier(strip_ipv6_port(ip)) | ||
end | ||
|
||
begin | ||
IPAddr.new(clean_ip) | ||
rescue IPAddr::Error | ||
nil | ||
end | ||
end | ||
|
||
def ip_header(headers, ip_headers_to_check) | ||
return unless headers | ||
|
||
ip_headers_to_check.each do |name| | ||
value = headers.get(name) | ||
|
||
next unless value | ||
|
||
ips = value.split(',') | ||
ips.each do |ip| | ||
parsed_ip = ip_to_ipaddr(ip.strip) | ||
|
||
return parsed_ip if global_ip?(parsed_ip) | ||
end | ||
end | ||
|
||
nil | ||
end | ||
|
||
# Returns whether the given value is more likely to be an IPv4 than an IPv6 address. | ||
# | ||
# This is done by checking if a dot (`'.'`) character appears before a colon (`':'`) in the value. | ||
# The rationale is that in valid IPv6 addresses, colons will always preced dots, | ||
# and in valid IPv4 addresses dots will always preced colons. | ||
def likely_ipv4?(value) | ||
dot_index = value.index('.') || value.size | ||
colon_index = value.index(':') || value.size | ||
|
||
dot_index < colon_index | ||
end | ||
|
||
def strip_zone_specifier(ipv6) | ||
ipv6.gsub(/%.*/, '') | ||
end | ||
|
||
def strip_ipv6_port(ip) | ||
if /\[(.*)\](?::\d+)?/ =~ ip | ||
Regexp.last_match(1) | ||
else | ||
ip | ||
end | ||
end | ||
|
||
def strip_ipv4_port(ip) | ||
ip.gsub(/:\d+\z/, '') | ||
end | ||
|
||
def global_ip?(parsed_ip) | ||
parsed_ip && !private?(parsed_ip) && !loopback?(parsed_ip) && !link_local?(parsed_ip) | ||
end | ||
|
||
# TODO: remove once we drop support for ruby 2.1, 2.2, 2.3, 2.4 | ||
# replace with ip.private? | ||
def private?(ip) | ||
Datadog::Core::Vendor::IPAddr.private?(ip) | ||
end | ||
|
||
# TODO: remove once we drop support for ruby 2.1, 2.2, 2.3, 2.4 | ||
# replace with ip.link_local? | ||
def link_local?(ip) | ||
Datadog::Core::Vendor::IPAddr.link_local?(ip) | ||
end | ||
|
||
# TODO: remove once we drop support for ruby 2.1, 2.2, 2.3, 2.4 | ||
# replace with ip.loopback | ||
def loopback?(ip) | ||
Datadog::Core::Vendor::IPAddr.loopback?(ip) | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
# Copyright (c) 2002 Hajimu UMEMOTO <ume@mahoroba.org> | ||
# Copyright (c) 2007-2017 Akinori MUSHA <knu@iDaemons.org> | ||
|
||
# Redistribution and use in source and binary forms, with or without | ||
# modification, are permitted provided that the following conditions | ||
# are met: | ||
# 1. Redistributions of source code must retain the above copyright | ||
# notice, this list of conditions and the following disclaimer. | ||
# 2. Redistributions in binary form must reproduce the above copyright | ||
# notice, this list of conditions and the following disclaimer in the | ||
# documentation and/or other materials provided with the distribution. | ||
|
||
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND | ||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | ||
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE | ||
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | ||
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS | ||
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) | ||
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT | ||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY | ||
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF | ||
# SUCH DAMAGE. | ||
|
||
module Datadog | ||
module Core | ||
module Vendor | ||
# vendor code from https://github.com/ruby/ipaddr/blob/master/lib/ipaddr.rb | ||
# Ruby version below 2.5 does not have the IpAddr#private? method | ||
# We have to vendor the code because ruby versions below 2.5 did not extract ipaddr as a gem | ||
# So we can not specify a specific version for ipaddr for ruby versions: 2.1, 2.2, 2.3, 2.4 | ||
module IPAddr | ||
class << self | ||
def private?(ip) | ||
addr = ip.instance_variable_get(:@addr) | ||
|
||
case ip.family | ||
when Socket::AF_INET | ||
addr & 0xff000000 == 0x0a000000 || # 10.0.0.0/8 | ||
addr & 0xfff00000 == 0xac100000 || # 172.16.0.0/12 | ||
addr & 0xffff0000 == 0xc0a80000 # 192.168.0.0/16 | ||
when Socket::AF_INET6 | ||
addr & 0xfe00_0000_0000_0000_0000_0000_0000_0000 == 0xfc00_0000_0000_0000_0000_0000_0000_0000 | ||
else | ||
raise IPAddr::AddressFamilyError, 'unsupported address family' | ||
end | ||
end | ||
|
||
def link_local?(ip) | ||
addr = ip.instance_variable_get(:@addr) | ||
|
||
case ip.family | ||
when Socket::AF_INET | ||
addr & 0xffff0000 == 0xa9fe0000 # 169.254.0.0/16 | ||
when Socket::AF_INET6 | ||
addr & 0xffc0_0000_0000_0000_0000_0000_0000_0000 == 0xfe80_0000_0000_0000_0000_0000_0000_0000 | ||
else | ||
raise IPAddr::AddressFamilyError, 'unsupported address family' | ||
end | ||
end | ||
|
||
def loopback?(ip) | ||
addr = ip.instance_variable_get(:@addr) | ||
|
||
case ip.family | ||
when Socket::AF_INET | ||
addr & 0xff000000 == 0x7f000000 | ||
when Socket::AF_INET6 | ||
addr == 1 | ||
else | ||
raise IPAddr::AddressFamilyError, 'unsupported address family' | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.