faraday-ssrf-filter 0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2dcc958c9132d2f306a52896efa1b21455232705fa5565cde86cf6205ac74c7e
4
+ data.tar.gz: 37663afac9a815654d26dda87d91ab901003d60a1c80f955871156a8af23f8b7
5
+ SHA512:
6
+ metadata.gz: b882da77d01e75f0da42f90d6164c4e2b5af195ec0609e0a632b4f24275c687b921b74cb3aacd8beed3184f58534cd78ed33246863f02fccf7c630325cfe34a0
7
+ data.tar.gz: f073b6ffd4c3253ba46b550c66a882d5f4f4c8eebbc386eb92e1098e99844c0397238feac3178ae5f95cf397f7e3861b7442783769ddc19b81d6428aa654fd10
data/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-05-17)
4
+
5
+ - Initial release
6
+ - Block requests to private/reserved IPv4 and IPv6 ranges
7
+ - DNS resolution with hostname replacement (HTTP) to prevent DNS rebinding
8
+ - TLS SNI preservation for HTTPS requests
9
+ - Redirect validation — block 3xx responses pointing to private/reserved IPs
10
+ - IPv4-mapped/compatible/translated IPv6 address detection
11
+ - NAT64 well-known prefix detection
12
+ - Configurable allowlist/denylist
13
+ - Custom DNS resolver support
14
+ - Scheme validation
15
+ - CI with Ruby 3.0-3.4 x Faraday 1/2 matrix
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Quentin Rousseau
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,175 @@
1
+ # Faraday SSRF Filter
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/faraday-ssrf-filter.svg)](https://rubygems.org/gems/faraday-ssrf-filter)
4
+
5
+ A [Faraday](https://github.com/lostisland/faraday) middleware that prevents Server-Side Request Forgery (SSRF) attacks by validating resolved IP addresses against known private and reserved IP ranges before allowing requests to proceed.
6
+
7
+ Inspired by [ssrf_filter](https://github.com/arkadiyt/ssrf_filter) (which uses `Net::HTTP` directly), this gem brings the same level of SSRF protection to any Faraday-based HTTP client.
8
+
9
+ ## Features
10
+
11
+ - Blocks requests to **all private/reserved IPv4 and IPv6 ranges** (RFC 1918, RFC 6598, loopback, link-local, multicast, etc.)
12
+ - Detects **IPv4-mapped/compatible/translated IPv6 addresses** (e.g., `::ffff:127.0.0.1`)
13
+ - Detects **NAT64 well-known prefix** addresses (e.g., `64:ff9b::10.0.0.1`)
14
+ - **DNS resolution with IP pinning** to prevent DNS rebinding attacks
15
+ - Blocks **direct IP address** usage by default
16
+ - Configurable **allowlist/denylist** for fine-grained control
17
+ - **Custom DNS resolver** support
18
+ - **Scheme validation** (only `http`/`https` by default)
19
+ - Works with **all Faraday adapters**
20
+
21
+ ## Installation
22
+
23
+ Add to your Gemfile:
24
+
25
+ ```ruby
26
+ gem 'faraday-ssrf-filter'
27
+ ```
28
+
29
+ Or install directly:
30
+
31
+ ```bash
32
+ gem install faraday-ssrf-filter
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ ### Basic Usage
38
+
39
+ ```ruby
40
+ require 'faraday/ssrf_filter'
41
+
42
+ conn = Faraday.new(url: 'https://api.example.com') do |f|
43
+ f.request :ssrf_filter
44
+ f.adapter Faraday.default_adapter
45
+ end
46
+
47
+ # Safe requests work normally
48
+ response = conn.get('/data')
49
+
50
+ # Requests to private IPs are blocked
51
+ # e.g., if evil.com resolves to 127.0.0.1
52
+ # => raises Faraday::SsrfFilter::PrivateIPError
53
+ ```
54
+
55
+ ### Configuration Options
56
+
57
+ ```ruby
58
+ conn = Faraday.new(url: 'https://api.example.com') do |f|
59
+ f.request :ssrf_filter,
60
+ # Allow specific private ranges (e.g., internal services)
61
+ allowlist: ['10.0.0.0/8'],
62
+
63
+ # Block specific public ranges
64
+ denylist: ['93.184.216.0/24'],
65
+
66
+ # Allow direct IP addresses in URLs (default: false)
67
+ allow_ip_addresses: true,
68
+
69
+ # Restrict allowed URI schemes (default: ['http', 'https'])
70
+ allowed_schemes: %w[http https],
71
+
72
+ # Custom DNS resolver (default: Resolv.getaddresses)
73
+ resolver: ->(hostname) { Resolv.getaddresses(hostname) }
74
+
75
+ f.adapter Faraday.default_adapter
76
+ end
77
+ ```
78
+
79
+ ### Error Handling
80
+
81
+ All errors inherit from `Faraday::SsrfFilter::SSRFError` (which inherits from `Faraday::Error`):
82
+
83
+ ```ruby
84
+ begin
85
+ conn.get('/data')
86
+ rescue Faraday::SsrfFilter::PrivateIPError => e
87
+ # Hostname resolved to a private/reserved IP
88
+ rescue Faraday::SsrfFilter::DirectIPError => e
89
+ # URL contains a direct IP address (blocked by default)
90
+ rescue Faraday::SsrfFilter::InvalidSchemeError => e
91
+ # URI scheme not in allowed list
92
+ rescue Faraday::SsrfFilter::DNSResolutionError => e
93
+ # Could not resolve hostname
94
+ rescue Faraday::SsrfFilter::UnsafeRedirectError => e
95
+ # Response redirects to a private/reserved IP or disallowed scheme
96
+ rescue Faraday::SsrfFilter::SSRFError => e
97
+ # Catch-all for any SSRF error
98
+ end
99
+ ```
100
+
101
+ ## Blocked IP Ranges
102
+
103
+ ### IPv4
104
+
105
+ | Range | Purpose |
106
+ |---|---|
107
+ | `0.0.0.0/8` | Current network |
108
+ | `10.0.0.0/8` | Private (RFC 1918) |
109
+ | `100.64.0.0/10` | Carrier-grade NAT (RFC 6598) |
110
+ | `127.0.0.0/8` | Loopback |
111
+ | `169.254.0.0/16` | Link-local (includes cloud metadata endpoints) |
112
+ | `172.16.0.0/12` | Private (RFC 1918) |
113
+ | `192.0.0.0/24` | IETF protocol assignments |
114
+ | `192.0.2.0/24` | TEST-NET-1 |
115
+ | `192.168.0.0/16` | Private (RFC 1918) |
116
+ | `198.18.0.0/15` | Benchmarking |
117
+ | `198.51.100.0/24` | TEST-NET-2 |
118
+ | `203.0.113.0/24` | TEST-NET-3 |
119
+ | `224.0.0.0/4` | Multicast |
120
+ | `240.0.0.0/4` | Reserved |
121
+ | `255.255.255.255/32` | Broadcast |
122
+
123
+ ### IPv6
124
+
125
+ | Range | Purpose |
126
+ |---|---|
127
+ | `::/128` | Unspecified |
128
+ | `::1/128` | Loopback |
129
+ | `100::/64` | Discard prefix |
130
+ | `2001::/32` | Teredo tunneling |
131
+ | `2001:2::/48` | Benchmarking |
132
+ | `2001:10::/28` | ORCHID |
133
+ | `2001:20::/28` | ORCHIDv2 |
134
+ | `2001:db8::/32` | Documentation |
135
+ | `2002::/16` | 6to4 tunneling |
136
+ | `3fff::/20` | Documentation |
137
+ | `5f00::/16` | Segment Routing (SRv6) |
138
+ | `fc00::/7` | Unique local |
139
+ | `fe80::/10` | Link-local |
140
+ | `ff00::/8` | Multicast |
141
+ | `64:ff9b:1::/48` | NAT64 local prefix |
142
+
143
+ Additionally, all IPv4 blacklisted ranges are also blocked in their IPv4-compatible (`::x.x.x.x`), IPv4-mapped (`::ffff:x.x.x.x`), IPv4-translated (`::ffff:0:x.x.x.x`), and NAT64 (`64:ff9b::x.x.x.x`) IPv6 representations.
144
+
145
+ ## How It Works
146
+
147
+ 1. **Scheme validation** — Only `http` and `https` are allowed by default
148
+ 2. **Direct IP blocking** — URLs with IP addresses instead of hostnames are blocked by default
149
+ 3. **DNS resolution** — The hostname is resolved to IP addresses using `Resolv.getaddresses`
150
+ 4. **IP validation** — Each resolved IP is checked against the comprehensive denylist
151
+ 5. **Hostname replacement (HTTP)** — For HTTP requests, the URL hostname is replaced with the validated IP and the `Host` header is set to the original hostname, preventing DNS rebinding
152
+ 6. **TLS preservation (HTTPS)** — For HTTPS requests, the hostname is preserved in the URL to maintain correct TLS SNI and certificate verification. The resolved IP is stored in the `X-Faraday-SSRF-Resolved-IP` header
153
+ 7. **Redirect validation** — Redirect responses (3xx with `Location` header) are inspected. The redirect target is resolved and validated against the same denylist, raising `UnsafeRedirectError` if it points to a private/reserved IP
154
+
155
+ ## Middleware Ordering
156
+
157
+ When using a redirect-following middleware (e.g., `faraday-follow_redirects`), place it **before** the SSRF filter so each redirect is validated:
158
+
159
+ ```ruby
160
+ conn = Faraday.new(url: 'https://api.example.com') do |f|
161
+ f.response :follow_redirects # outer — follows redirects
162
+ f.request :ssrf_filter # inner — validates each request including redirects
163
+ f.adapter Faraday.default_adapter
164
+ end
165
+ ```
166
+
167
+ The SSRF filter also validates redirect `Location` headers in responses as defense-in-depth, regardless of middleware ordering.
168
+
169
+ ## Acknowledgments
170
+
171
+ This gem is heavily inspired by [ssrf_filter](https://github.com/arkadiyt/ssrf_filter) by [Arkadiy Tetelman](https://github.com/arkadiyt). The comprehensive IP blacklist, IPv4-mapped IPv6 detection, and NAT64 handling are all based on his excellent work. Thank you for building such a thorough and well-tested SSRF protection library for the Ruby ecosystem.
172
+
173
+ ## License
174
+
175
+ MIT License. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ipaddr'
4
+ require 'resolv'
5
+ require 'uri'
6
+
7
+ module Faraday
8
+ module SsrfFilter
9
+ class SSRFError < Faraday::Error; end
10
+ class PrivateIPError < SSRFError; end
11
+ class DirectIPError < SSRFError; end
12
+ class InvalidSchemeError < SSRFError; end
13
+ class DNSResolutionError < SSRFError; end
14
+ class UnsafeRedirectError < SSRFError; end
15
+
16
+ class Middleware < Faraday::Middleware
17
+ DEFAULT_SCHEMES = %w[http https].freeze
18
+ REDIRECT_STATUSES = (300..399)
19
+
20
+ IPV4_DENYLIST = [
21
+ IPAddr.new('0.0.0.0/8'),
22
+ IPAddr.new('10.0.0.0/8'),
23
+ IPAddr.new('100.64.0.0/10'),
24
+ IPAddr.new('127.0.0.0/8'),
25
+ IPAddr.new('169.254.0.0/16'),
26
+ IPAddr.new('172.16.0.0/12'),
27
+ IPAddr.new('192.0.0.0/24'),
28
+ IPAddr.new('192.0.2.0/24'),
29
+ IPAddr.new('192.168.0.0/16'),
30
+ IPAddr.new('198.18.0.0/15'),
31
+ IPAddr.new('198.51.100.0/24'),
32
+ IPAddr.new('203.0.113.0/24'),
33
+ IPAddr.new('224.0.0.0/4'),
34
+ IPAddr.new('240.0.0.0/4'),
35
+ IPAddr.new('255.255.255.255/32')
36
+ ].freeze
37
+
38
+ IPV6_DENYLIST = [
39
+ IPAddr.new('::1/128'),
40
+ IPAddr.new('::/128'),
41
+ IPAddr.new('100::/64'),
42
+ IPAddr.new('2001::/32'),
43
+ IPAddr.new('2001:2::/48'),
44
+ IPAddr.new('2001:10::/28'),
45
+ IPAddr.new('2001:20::/28'),
46
+ IPAddr.new('2001:db8::/32'),
47
+ IPAddr.new('2002::/16'),
48
+ IPAddr.new('3fff::/20'),
49
+ IPAddr.new('5f00::/16'),
50
+ IPAddr.new('fc00::/7'),
51
+ IPAddr.new('fe80::/10'),
52
+ IPAddr.new('ff00::/8'),
53
+ IPAddr.new('64:ff9b:1::/48'),
54
+ *IPV4_DENYLIST.flat_map do |range|
55
+ pfx = range.prefix
56
+ ip = range.to_s
57
+ [
58
+ IPAddr.new("::#{ip}/#{pfx + 96}"),
59
+ IPAddr.new("::ffff:#{ip}/#{pfx + 96}"),
60
+ IPAddr.new("::ffff:0:#{ip}/#{pfx + 96}"),
61
+ IPAddr.new("64:ff9b::#{ip}/#{pfx + 96}")
62
+ ]
63
+ end
64
+ ].freeze
65
+
66
+ def initialize(app, options = {})
67
+ super(app)
68
+ @schemes = (options[:allowed_schemes] || DEFAULT_SCHEMES).freeze
69
+ @resolver = options[:resolver] || method(:default_resolver)
70
+ @allow_ip_addresses = options[:allow_ip_addresses] == true
71
+ @allowlist = parse_ip_list(options[:allowlist]).freeze
72
+ @denylist = parse_ip_list(options[:denylist]).freeze
73
+ end
74
+
75
+ def call(env)
76
+ validate_and_pin!(env)
77
+ @app.call(env).on_complete { |response_env| validate_redirect!(response_env) }
78
+ end
79
+
80
+ private
81
+
82
+ def default_resolver(hostname)
83
+ Resolv.getaddresses(hostname)
84
+ end
85
+
86
+ def parse_ip_list(list)
87
+ (list || []).map { |r| r.is_a?(IPAddr) ? r : IPAddr.new(r) }
88
+ end
89
+
90
+ def validate_and_pin!(env)
91
+ validate_scheme!(env[:url])
92
+ hostname = env[:url].hostname
93
+ addr = parse_ip(hostname)
94
+
95
+ if addr
96
+ validate_direct_ip!(addr, hostname)
97
+ else
98
+ resolve_and_pin!(env, hostname)
99
+ end
100
+ end
101
+
102
+ def validate_scheme!(uri)
103
+ raise InvalidSchemeError, "URI scheme '#{uri.scheme}' not allowed" unless @schemes.include?(uri.scheme)
104
+ end
105
+
106
+ def validate_direct_ip!(addr, hostname)
107
+ raise DirectIPError, "Direct IP addresses are not allowed: #{hostname}" unless @allow_ip_addresses
108
+ raise PrivateIPError, "IP address '#{hostname}' is private/reserved" unless safe_addr?(addr)
109
+ end
110
+
111
+ def resolve_and_pin!(env, hostname)
112
+ addresses = Array(@resolver.call(hostname))
113
+ raise DNSResolutionError, "Could not resolve hostname: #{hostname}" if addresses.empty?
114
+
115
+ safe_address = addresses.find { |a| safe_ip?(a.to_s) }
116
+ raise PrivateIPError, "Hostname '#{hostname}' resolves to a private/reserved IP address" unless safe_address
117
+
118
+ pin_ip!(env, safe_address.to_s, hostname)
119
+ end
120
+
121
+ def pin_ip!(env, ip, original_hostname)
122
+ uri = env[:url]
123
+ env[:request_headers] ||= {}
124
+ env[:request_headers]['X-Faraday-SSRF-Original-Host'] = original_hostname
125
+ env[:request_headers]['X-Faraday-SSRF-Resolved-IP'] = ip
126
+
127
+ # Only rewrite hostname for HTTP. For HTTPS, rewriting breaks TLS SNI
128
+ # and certificate verification since the adapter would negotiate TLS
129
+ # with the IP address instead of the original hostname.
130
+ return unless uri.scheme == 'http'
131
+
132
+ env[:request_headers]['Host'] = normalized_host(uri)
133
+ env[:url] = uri.dup.tap { |u| u.hostname = ip }
134
+ end
135
+
136
+ def validate_redirect!(env)
137
+ return unless REDIRECT_STATUSES.cover?(env[:status])
138
+
139
+ location = env[:response_headers]&.[]('location')
140
+ return if location.nil? || location.empty?
141
+
142
+ uri = resolve_redirect_uri(location, env[:url])
143
+ validate_redirect_target!(uri)
144
+ end
145
+
146
+ def resolve_redirect_uri(location, original_uri)
147
+ uri = URI.parse(location)
148
+ return uri if uri.host
149
+
150
+ URI.join("#{original_uri.scheme}://#{original_uri.host}:#{original_uri.port}", location)
151
+ rescue URI::InvalidURIError
152
+ raise UnsafeRedirectError, "Invalid redirect location: #{location}"
153
+ end
154
+
155
+ def validate_redirect_target!(uri)
156
+ raise UnsafeRedirectError, "Redirect to disallowed scheme: #{uri.scheme}" unless @schemes.include?(uri.scheme)
157
+
158
+ hostname = uri.hostname || uri.host
159
+ return unless hostname
160
+
161
+ addr = parse_ip(hostname)
162
+ if addr
163
+ raise UnsafeRedirectError, "Redirect to private IP: #{hostname}" unless safe_addr?(addr)
164
+ else
165
+ validate_redirect_hostname!(hostname)
166
+ end
167
+ end
168
+
169
+ def validate_redirect_hostname!(hostname)
170
+ addresses = Array(@resolver.call(hostname))
171
+ raise UnsafeRedirectError, "Cannot resolve redirect hostname: #{hostname}" if addresses.empty?
172
+
173
+ safe = addresses.any? { |a| safe_ip?(a.to_s) }
174
+ raise UnsafeRedirectError, "Redirect to '#{hostname}' resolves to a private IP" unless safe
175
+ end
176
+
177
+ def normalized_host(uri)
178
+ host = uri.hostname
179
+ port = uri.port
180
+ return host if port.nil?
181
+ return host if uri.scheme == 'http' && port == 80
182
+ return host if uri.scheme == 'https' && port == 443
183
+
184
+ "#{host}:#{port}"
185
+ end
186
+
187
+ def parse_ip(hostname)
188
+ IPAddr.new(hostname)
189
+ rescue IPAddr::InvalidAddressError
190
+ nil
191
+ end
192
+
193
+ def safe_ip?(ip_string)
194
+ safe_addr?(IPAddr.new(ip_string))
195
+ rescue IPAddr::InvalidAddressError
196
+ false
197
+ end
198
+
199
+ def safe_addr?(addr)
200
+ return true if @allowlist.any? { |range| range.include?(addr) }
201
+ return false if @denylist.any? { |range| range.include?(addr) }
202
+
203
+ !unsafe_ip?(addr)
204
+ end
205
+
206
+ def unsafe_ip?(addr)
207
+ if addr.ipv4?
208
+ IPV4_DENYLIST.any? { |range| range.include?(addr) }
209
+ elsif addr.ipv6?
210
+ IPV6_DENYLIST.any? { |range| range.include?(addr) }
211
+ else
212
+ true
213
+ end
214
+ end
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Faraday
4
+ module SsrfFilter
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require_relative 'ssrf_filter/middleware'
5
+ require_relative 'ssrf_filter/version'
6
+
7
+ module Faraday
8
+ module SsrfFilter
9
+ Faraday::Request.register_middleware(ssrf_filter: Faraday::SsrfFilter::Middleware)
10
+ end
11
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: faraday-ssrf-filter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Quentin Rousseau
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: faraday
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '1.0'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '3'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '1.0'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '3'
32
+ description: A Faraday middleware that prevents Server-Side Request Forgery (SSRF)
33
+ attacks by validating resolved IP addresses against known private and reserved IP
34
+ ranges before allowing the request to proceed.
35
+ email:
36
+ - contact@quent.in
37
+ executables: []
38
+ extensions: []
39
+ extra_rdoc_files: []
40
+ files:
41
+ - CHANGELOG.md
42
+ - LICENSE
43
+ - README.md
44
+ - lib/faraday/ssrf_filter.rb
45
+ - lib/faraday/ssrf_filter/middleware.rb
46
+ - lib/faraday/ssrf_filter/version.rb
47
+ homepage: https://github.com/kwent/faraday-ssrf-filter
48
+ licenses:
49
+ - MIT
50
+ metadata:
51
+ homepage_uri: https://github.com/kwent/faraday-ssrf-filter
52
+ source_code_uri: https://github.com/kwent/faraday-ssrf-filter
53
+ changelog_uri: https://github.com/kwent/faraday-ssrf-filter/blob/main/CHANGELOG.md
54
+ rubygems_mfa_required: 'true'
55
+ rdoc_options: []
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 3.0.0
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ requirements: []
69
+ rubygems_version: 3.6.9
70
+ specification_version: 4
71
+ summary: Faraday middleware to prevent SSRF attacks
72
+ test_files: []