vpndetection-rails 2.1.9 → 2.1.10
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 +10 -2
- data/lib/vpndetection/rails/middleware.rb +24 -4
- data/lib/vpndetection/rails/selectors.rb +28 -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: ffe89ccd6dcbfc2dce60cbb4db75d7aa929032b0c7e1e4f5346ff9e57de466c6
|
|
4
|
+
data.tar.gz: 8ee5639bc3a0ac71989c31b24e040b99553fef163cc164df2b090fadb1a21666
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b7ed6166a51da872c7910ae3cc745de7593090f8bd5d4004c3514cffcd813c38836b956bfe52da5aa8ade99e9447e5246f5046bdc6266100349be44970873f7f
|
|
7
|
+
data.tar.gz: 9cc1985e154a0f14fd0ce5366dcd92786abbff16987e5ec60a3df68f18732e35688382d8925cb4506f67e0c1a1ce0ca656d5bb5a252eb7e2035a9bc0ae852687
|
data/README.md
CHANGED
|
@@ -70,9 +70,9 @@ 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 }`. The middleware warns once when it sees that setup: a request carrying `X-Forwarded-For
|
|
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`, `Client-IP` or `Forwarded` from a public address outside `trusted_proxies`.
|
|
74
74
|
|
|
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
|
|
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 the forwarding headers, 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
|
|
|
77
77
|
```ruby
|
|
78
78
|
ip_selector: VPNDetection::Rails.header_ip_selector("CF-Connecting-IP")
|
|
@@ -80,6 +80,14 @@ ip_selector: VPNDetection::Rails.header_ip_selector("CF-Connecting-IP")
|
|
|
80
80
|
|
|
81
81
|
`VPNDetection::Rails.xff_ip_selector` reads `X-Forwarded-For` directly, and `xff_ip_selector(1)` counts one trusted hop from the right. Anything else, pass your own callable — it receives the Rack request and returns an address.
|
|
82
82
|
|
|
83
|
+
**Behind a proxy that writes only `X-Forwarded-For`, a visitor's own `Forwarded` header names the address.** Rack 3 reads `Forwarded` first, for `Rack::Request#ip` and Rails' `remote_ip` alike, and nginx passes a visitor's copy through untouched, so a request sending `Forwarded: for=1.1.1.1` is looked up as `1.1.1.1`. Clear the header at the proxy, `proxy_set_header Forwarded "";` in nginx, or tell Rack to read `X-Forwarded-For` alone at the top of `config.ru`:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
Rack::Request.forwarded_priority = [:x_forwarded]
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The middleware warns once when a request through a proxy carries both headers.
|
|
90
|
+
|
|
83
91
|
If the address resolves to a private one, the middleware says so once. That is expected locally and is the signal to fix your configuration anywhere else.
|
|
84
92
|
|
|
85
93
|
## When a lookup fails
|
|
@@ -21,8 +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.
|
|
24
|
+
# Only the default selector reads Rails' remote_ip, or Rack's ip.
|
|
25
25
|
@check_forgery = !options.key?(:ip_selector)
|
|
26
|
+
@check_passed_through = @check_forgery
|
|
26
27
|
@on_warn = options[:on_warn]
|
|
27
28
|
@core = VPNDetection::Middleware::Core.new(
|
|
28
29
|
Rails.default_ip_selector, **options
|
|
@@ -32,6 +33,7 @@ module VPNDetection
|
|
|
32
33
|
def call(env)
|
|
33
34
|
request = ::Rack::Request.new(env)
|
|
34
35
|
warn_forgeable(request) if @check_forgery
|
|
36
|
+
warn_passed_through(request) if @check_passed_through
|
|
35
37
|
lookup = @core.evaluate(request)
|
|
36
38
|
return @app.call(env) if lookup.nil?
|
|
37
39
|
|
|
@@ -49,11 +51,29 @@ module VPNDetection
|
|
|
49
51
|
return if peer.nil?
|
|
50
52
|
|
|
51
53
|
@check_forgery = false
|
|
52
|
-
message = "a request carrying
|
|
53
|
-
'config.action_dispatch.trusted_proxies, so Rails takes the visitor
|
|
54
|
-
'which anyone connecting directly can set. If a proxy sits in front of this app, ' \
|
|
54
|
+
message = "a request carrying #{Rails.forwarding_header(request)} came from #{peer}, a public " \
|
|
55
|
+
'address outside config.action_dispatch.trusted_proxies, so Rails takes the visitor ' \
|
|
56
|
+
'from that header, which anyone connecting directly can set. If a proxy sits in front of this app, ' \
|
|
55
57
|
'add its addresses to trusted_proxies; if nothing does, pass ' \
|
|
56
58
|
'ip_selector: ->(request) { request.ip }.'
|
|
59
|
+
say(message)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Once, as above.
|
|
63
|
+
def warn_passed_through(request)
|
|
64
|
+
proxy = Rails.passed_through_forwarded(request)
|
|
65
|
+
return if proxy.nil?
|
|
66
|
+
|
|
67
|
+
@check_passed_through = false
|
|
68
|
+
message = "a request through #{proxy} carries Forwarded beside X-Forwarded-For, and Rack " \
|
|
69
|
+
'reads Forwarded first. A proxy that writes only X-Forwarded-For, as nginx does, ' \
|
|
70
|
+
"passes a visitor's own Forwarded through, and that names the address looked up. " \
|
|
71
|
+
'If yours does, clear the header there (nginx: proxy_set_header Forwarded "";), ' \
|
|
72
|
+
'or set Rack::Request.forwarded_priority = [:x_forwarded] in config.ru.'
|
|
73
|
+
say(message)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def say(message)
|
|
57
77
|
@on_warn ? @on_warn.call(message) : Kernel.warn("[vpndetection] #{message}")
|
|
58
78
|
end
|
|
59
79
|
|
|
@@ -52,11 +52,12 @@ module VPNDetection
|
|
|
52
52
|
|
|
53
53
|
# @api private
|
|
54
54
|
# The peer, when Rails' RemoteIp let a header that peer sent name the
|
|
55
|
-
# visitor: `X-Forwarded-For
|
|
56
|
-
# the proxies RemoteIp trusts, its defaults or
|
|
55
|
+
# visitor: `X-Forwarded-For`, `Client-IP` or a `Forwarded` Rack reads, from
|
|
56
|
+
# a public address outside the proxies RemoteIp trusts, its defaults or
|
|
57
|
+
# trusted_proxies.
|
|
57
58
|
def forgeable_peer(request)
|
|
58
59
|
remote_ip = request.get_header('action_dispatch.remote_ip')
|
|
59
|
-
return nil if remote_ip.nil? ||
|
|
60
|
+
return nil if remote_ip.nil? || forwarding_header(request).nil?
|
|
60
61
|
|
|
61
62
|
peer = request.get_header('REMOTE_ADDR').to_s
|
|
62
63
|
return nil unless public_address?(peer)
|
|
@@ -71,8 +72,30 @@ module VPNDetection
|
|
|
71
72
|
end
|
|
72
73
|
|
|
73
74
|
# @api private
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
# The first header on the request that can name the visitor, or nil.
|
|
76
|
+
def forwarding_header(request)
|
|
77
|
+
names = { 'X-Forwarded-For' => 'HTTP_X_FORWARDED_FOR', 'Client-IP' => 'HTTP_CLIENT_IP' }
|
|
78
|
+
names['Forwarded'] = 'HTTP_FORWARDED' if forwarded_priority.include?(:forwarded)
|
|
79
|
+
names.find { |_, key| !request.get_header(key).to_s.empty? }&.first
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @api private
|
|
83
|
+
# The proxy, when a request through it carries `Forwarded` beside
|
|
84
|
+
# `X-Forwarded-For` and Rack reads `Forwarded` first, as Rack 3 does by
|
|
85
|
+
# default, and so does Rails' RemoteIp, which asks Rack. A proxy writing
|
|
86
|
+
# only `X-Forwarded-For`, as nginx does, passes the visitor's own
|
|
87
|
+
# `Forwarded` through, and that names the address looked up.
|
|
88
|
+
def passed_through_forwarded(request)
|
|
89
|
+
return nil unless forwarded_priority.first == :forwarded
|
|
90
|
+
return nil if %w[HTTP_FORWARDED HTTP_X_FORWARDED_FOR].any? { |name| request.get_header(name).to_s.empty? }
|
|
91
|
+
|
|
92
|
+
request.get_header('REMOTE_ADDR').to_s
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @api private
|
|
96
|
+
# Rack 2 reads only `X-Forwarded-For`, and has no such setting (2.2.24).
|
|
97
|
+
def forwarded_priority
|
|
98
|
+
::Rack::Request.respond_to?(:forwarded_priority) ? ::Rack::Request.forwarded_priority : [:x_forwarded]
|
|
76
99
|
end
|
|
77
100
|
|
|
78
101
|
# @api private
|