vpndetection-rails 2.0.12 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/vpndetection/rails/middleware.rb +18 -0
- data/lib/vpndetection/rails/selectors.rb +43 -5
- data/lib/vpndetection/rails/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 19aff4067906b1224611a73aa026e8e844cae8ac860603d738d40de3fc190c1a
|
|
4
|
+
data.tar.gz: f403e754f52b4c48bc0d45b9b9a4e6a3ba0d9c9a20b332dbd599034e6b0bfd3d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 15e492920cb49c87109e29adafe64bdf014810f693e1271862c11d34892efb3126222c22d0849cf9424b3dde90199ed1a88b67435f25530712f7f3af3bbf5f4b
|
|
7
|
+
data.tar.gz: 412e327c133f3c1053de33cc03adc42bb8d5ff711739365ab2f81bfefc4b864f280bf50656b01c52e534c8865e89888ecec720c3efa3034bf7ba595d7136fa54
|
data/README.md
CHANGED
|
@@ -70,7 +70,7 @@ on_blocked: ->(request, lookup) { [303, { "location" => "/no-vpn" }, []] }
|
|
|
70
70
|
|
|
71
71
|
This is the setting that decides whether any of the above works, and it is the one thing only you can get right.
|
|
72
72
|
|
|
73
|
-
**In Rails**, the default reads `env["action_dispatch.remote_ip"]` — the answer `ActionDispatch::RemoteIp` computed from `X-Forwarded-For` minus your `config.action_dispatch.trusted_proxies`. That is the right fix behind a load balancer: tell Rails which proxies are yours and it resolves the visitor for you. `Rack::Request#ip` does *not* read that entry, so reading Rails' own is what makes your `trusted_proxies` mean anything here. **With nothing in front of the app server, that default can be forged**: `RemoteIp` believes `X-Forwarded-For` without checking that the connection came from a proxy, so any visitor picks the address that gets looked up. There, pass `ip_selector: ->(request) { request.ip }`.
|
|
73
|
+
**In Rails**, the default reads `env["action_dispatch.remote_ip"]` — the answer `ActionDispatch::RemoteIp` computed from `X-Forwarded-For` minus your `config.action_dispatch.trusted_proxies`. That is the right fix behind a load balancer: tell Rails which proxies are yours and it resolves the visitor for you. `Rack::Request#ip` does *not* read that entry, so reading Rails' own is what makes your `trusted_proxies` mean anything here. **With nothing in front of the app server, that default can be forged**: `RemoteIp` believes `X-Forwarded-For` without checking that the connection came from a proxy, so any visitor picks the address that gets looked up. There, pass `ip_selector: ->(request) { request.ip }`. The middleware warns once when it sees that setup: a request carrying `X-Forwarded-For` or `Client-IP` from a public address outside `trusted_proxies`.
|
|
74
74
|
|
|
75
75
|
**In a bare Rack app** there is no such entry and the default falls back to `Rack::Request#ip`. Rack's `ip` returns the connecting address, `REMOTE_ADDR`, unless that is a trusted proxy, which by default means a loopback or private address. Only then does it read `X-Forwarded-For`, and it takes the right-most entry that is not a trusted proxy. The test suite pins both. Behind an edge on a public address, or one that appends rather than overwrites, name your edge's header instead:
|
|
76
76
|
|
|
@@ -21,6 +21,9 @@ module VPNDetection
|
|
|
21
21
|
def initialize(app, **options)
|
|
22
22
|
@app = app
|
|
23
23
|
@on_blocked = options.delete(:on_blocked) || method(:refuse)
|
|
24
|
+
# Only the default selector reads Rails' remote_ip.
|
|
25
|
+
@check_forgery = !options.key?(:ip_selector)
|
|
26
|
+
@on_warn = options[:on_warn]
|
|
24
27
|
@core = VPNDetection::Middleware::Core.new(
|
|
25
28
|
Rails.default_ip_selector, **options
|
|
26
29
|
)
|
|
@@ -28,6 +31,7 @@ module VPNDetection
|
|
|
28
31
|
|
|
29
32
|
def call(env)
|
|
30
33
|
request = ::Rack::Request.new(env)
|
|
34
|
+
warn_forgeable(request) if @check_forgery
|
|
31
35
|
lookup = @core.evaluate(request)
|
|
32
36
|
return @app.call(env) if lookup.nil?
|
|
33
37
|
|
|
@@ -39,6 +43,20 @@ module VPNDetection
|
|
|
39
43
|
|
|
40
44
|
private
|
|
41
45
|
|
|
46
|
+
# Once: the setup is the same on every request.
|
|
47
|
+
def warn_forgeable(request)
|
|
48
|
+
peer = Rails.forgeable_peer(request)
|
|
49
|
+
return if peer.nil?
|
|
50
|
+
|
|
51
|
+
@check_forgery = false
|
|
52
|
+
message = "a request carrying X-Forwarded-For came from #{peer}, a public address outside " \
|
|
53
|
+
'config.action_dispatch.trusted_proxies, so Rails takes the visitor from that header, ' \
|
|
54
|
+
'which anyone connecting directly can set. If a proxy sits in front of this app, ' \
|
|
55
|
+
'add its addresses to trusted_proxies; if nothing does, pass ' \
|
|
56
|
+
'ip_selector: ->(request) { request.ip }.'
|
|
57
|
+
@on_warn ? @on_warn.call(message) : Kernel.warn("[vpndetection] #{message}")
|
|
58
|
+
end
|
|
59
|
+
|
|
42
60
|
def refuse(_request, _lookup)
|
|
43
61
|
body = JSON.generate({ 'error' => 'access denied' })
|
|
44
62
|
[403, { 'content-type' => 'application/json' }, [body]]
|
|
@@ -25,12 +25,17 @@ module VPNDetection
|
|
|
25
25
|
# and using Rack's alone would silently ignore the trusted_proxies you
|
|
26
26
|
# configured.
|
|
27
27
|
#
|
|
28
|
+
# **With nothing in front of the app server, Rails' answer can be forged**:
|
|
29
|
+
# `RemoteIp` believes `X-Forwarded-For` from any peer outside
|
|
30
|
+
# trusted_proxies, so a visitor names the address looked up. The middleware
|
|
31
|
+
# says so once when it sees that setup.
|
|
32
|
+
#
|
|
28
33
|
# Outside Rails there is no such entry and this falls back to
|
|
29
|
-
# `Rack::Request#ip`.
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
# {.header_ip_selector} or your own.
|
|
34
|
+
# `Rack::Request#ip`. Under Rack 3 that is the peer itself unless the peer
|
|
35
|
+
# is a trusted proxy (loopback or private by default). Rack 2 returned the
|
|
36
|
+
# left-most `X-Forwarded-For` entry after dropping private and loopback
|
|
37
|
+
# addresses, which is whatever the caller sent: there, behind nothing,
|
|
38
|
+
# prefer {.header_ip_selector} or your own.
|
|
34
39
|
def default_ip_selector
|
|
35
40
|
SELECTORS.default
|
|
36
41
|
end
|
|
@@ -45,6 +50,39 @@ module VPNDetection
|
|
|
45
50
|
request.ip
|
|
46
51
|
end
|
|
47
52
|
|
|
53
|
+
# @api private
|
|
54
|
+
# The peer, when Rails' RemoteIp let a header that peer sent name the
|
|
55
|
+
# visitor: `X-Forwarded-For` or `Client-IP` from a public address outside
|
|
56
|
+
# the proxies RemoteIp trusts, its defaults or trusted_proxies.
|
|
57
|
+
def forgeable_peer(request)
|
|
58
|
+
remote_ip = request.get_header('action_dispatch.remote_ip')
|
|
59
|
+
return nil if remote_ip.nil? || !forwarded?(request)
|
|
60
|
+
|
|
61
|
+
peer = request.get_header('REMOTE_ADDR').to_s
|
|
62
|
+
return nil unless public_address?(peer)
|
|
63
|
+
|
|
64
|
+
# RemoteIp's own list, from the object it leaves in env; Rails has kept
|
|
65
|
+
# it there as @proxies since 4.0.
|
|
66
|
+
proxies = remote_ip.instance_variable_get(:@proxies)
|
|
67
|
+
return nil unless proxies.respond_to?(:any?)
|
|
68
|
+
return nil if proxies.any? { |proxy| proxy === peer } # rubocop:disable Style/CaseEquality
|
|
69
|
+
|
|
70
|
+
peer
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @api private
|
|
74
|
+
def forwarded?(request)
|
|
75
|
+
%w[HTTP_X_FORWARDED_FOR HTTP_CLIENT_IP].any? { |name| !request.get_header(name).to_s.empty? }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @api private
|
|
79
|
+
def public_address?(ip)
|
|
80
|
+
IPAddr.new(ip)
|
|
81
|
+
!VPNDetection::Bogon.bogon?(ip)
|
|
82
|
+
rescue IPAddr::Error
|
|
83
|
+
false
|
|
84
|
+
end
|
|
85
|
+
|
|
48
86
|
# An address from `X-Forwarded-For`, ignoring Rails' trusted-proxy list.
|
|
49
87
|
#
|
|
50
88
|
# The LEFT-MOST entry (depth 0) is whatever the caller sent, because proxies
|