pax_global_header00006660000000000000000000000064151636376420014527gustar00rootroot0000000000000052 comment=b87b175f955743c8abee4b37053d92fe3da512c9 arkadiyt-ssrf_filter-b87b175/000077500000000000000000000000001516363764200162205ustar00rootroot00000000000000arkadiyt-ssrf_filter-b87b175/.dockerignore000066400000000000000000000000441516363764200206720ustar00rootroot00000000000000.git/ vendor/ coverage Gemfile.lock arkadiyt-ssrf_filter-b87b175/.github/000077500000000000000000000000001516363764200175605ustar00rootroot00000000000000arkadiyt-ssrf_filter-b87b175/.github/workflows/000077500000000000000000000000001516363764200216155ustar00rootroot00000000000000arkadiyt-ssrf_filter-b87b175/.github/workflows/build-test.yml000066400000000000000000000016551516363764200244230ustar00rootroot00000000000000name: Build-test on: push: pull_request: workflow_call: jobs: build-test: strategy: matrix: ruby-version: [2.7.0, 3.0.0, 3.1.0, 3.2.0, 3.3.0, 3.4.0, 3.5.0, 4.0.0, head] runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby-version }} bundler-cache: true - name: Test run: make -f Makefile.docker test - name: Coveralls uses: coverallsapp/github-action@master with: parallel: true flag-name: ${{ matrix.ruby-version }} github-token: ${{ secrets.GITHUB_TOKEN }} - name: Lint run: make -f Makefile.docker lint finish: needs: build-test runs-on: ubuntu-latest steps: - name: Coveralls Finished uses: coverallsapp/github-action@master with: github-token: ${{ secrets.GITHUB_TOKEN }} parallel-finished: true arkadiyt-ssrf_filter-b87b175/.gitignore000066400000000000000000000000671516363764200202130ustar00rootroot00000000000000.DS_Store .bundle Gemfile.lock coverage/ vendor/bundle arkadiyt-ssrf_filter-b87b175/.rspec000066400000000000000000000001151516363764200173320ustar00rootroot00000000000000--color --warning --order random --require spec_helper --format documentationarkadiyt-ssrf_filter-b87b175/.rubocop.yml000066400000000000000000000022631516363764200204750ustar00rootroot00000000000000inherit_from: - .rubocop_todo.yml require: - rubocop-rspec AllCops: NewCops: enable Gemspec/DevelopmentDependencies: EnforcedStyle: gemspec Metrics/AbcSize: Enabled: false Metrics/BlockLength: Enabled: false Metrics/ClassLength: Enabled: false Metrics/CyclomaticComplexity: Enabled: false Metrics/MethodLength: Enabled: false Naming/MethodParameterName: Enabled: false Metrics/PerceivedComplexity: Enabled: false Layout/ArgumentAlignment: EnforcedStyle: with_fixed_indentation Layout/CaseIndentation: EnforcedStyle: end Layout/EndAlignment: EnforcedStyleAlignWith: variable Layout/FirstArrayElementIndentation: EnforcedStyle: consistent Layout/LineLength: Max: 120 Layout/MultilineMethodCallIndentation: EnforcedStyle: indented Layout/SpaceInsideHashLiteralBraces: EnforcedStyle: no_space RSpec/BeforeAfterAll: Enabled: false RSpec/IndexedLet: Enabled: false RSpec/MultipleExpectations: Enabled: false RSpec/ExampleLength: Max: 40 RSpec/MessageSpies: EnforcedStyle: receive RSpec/StubbedMock: Enabled: False Style/Documentation: Enabled: false Style/NumericLiterals: Enabled: false Style/WhileUntilModifier: Enabled: false arkadiyt-ssrf_filter-b87b175/.rubocop_todo.yml000066400000000000000000000006061516363764200215210ustar00rootroot00000000000000# This configuration was generated by # `rubocop --auto-gen-config` # on 2017-07-24 01:01:01 -0700 using RuboCop version 0.49.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. arkadiyt-ssrf_filter-b87b175/CHANGELOG.md000066400000000000000000000053161516363764200200360ustar00rootroot00000000000000### 1.5.0 (4/2/2026) * Fix an issue where sensitive headers could have been sent to unintended origins during redirects ([xkiluar](https://hackerone.com/xkiluar), [arkadiyt](https://github.com/arkadiyt/ssrf_filter/pull/86)) ### 1.4.0 (3/30/2026) * Fix some missing denylist entries ([tipsen](https://hackerone.com/tipsen), [arkadiyt](https://github.com/arkadiyt/ssrf_filter/pull/84)) ### 1.3.0 (5/10/2025) * Correctly handle 3xx responses with no Location header (resolves [#79](https://github.com/arkadiyt/ssrf_filter/issues/79)) ([arkadiyt](https://github.com/arkadiyt/ssrf_filter/pull/80)) ### 1.2.0 (11/7/2024) * Drop support for ruby 2.6, add support for ruby 3.3 ([arkadiyt](https://github.com/arkadiyt/ssrf_filter/pull/73)) * Stop patching OpenSSL (resolves [#72](https://github.com/arkadiyt/ssrf_filter/issues/72)) ([arkadiyt](https://github.com/arkadiyt/ssrf_filter/pull/73)) ### 1.1.2 (9/11/2023) * Fix a bug introduced in 1.1.0 when reading non-streaming bodies from responses ([mshibuya](https://github.com/arkadiyt/ssrf_filter/pull/60)) * Test against ruby 3.2 ([petergoldstein](https://github.com/arkadiyt/ssrf_filter/pull/62)) * Fix a [bug](https://github.com/arkadiyt/ssrf_filter/issues/61) preventing DNS resolution in some cases ([arkadiyt](https://github.com/arkadiyt/ssrf_filter/pull/70)) * Add an option to not throw an exception if you hit the maximum number of redirects ([elliterate](https://github.com/arkadiyt/ssrf_filter/pull/63)) ### 1.1.1 (8/31/2022) * Fix network connection errors if you were making https requests while using [net-http](https://github.com/ruby/net-http) 2.2 or higher ([arkadiyt](https://github.com/arkadiyt/ssrf_filter/pull/54)) ### 1.1.0 (8/28/2022) * Add support for chunked responses ([mrhaddad](https://github.com/arkadiyt/ssrf_filter/pull/30)) ### 1.0.8 (8/3/2022) * Add support for HEAD requests ([jakeyheath](https://github.com/arkadiyt/ssrf_filter/pull/38)) ### 1.0.7 (10/21/2019) * Allow passing custom options to Net::HTTP.start ([groe](https://github.com/arkadiyt/ssrf_filter/pull/26)) ### 1.0.6 (2/24/2018) * Backport a fix for a [bug](https://bugs.ruby-lang.org/issues/10054) in Ruby's http library ### 1.0.5 (1/17/2018) * Don't send the port number in the Host header if it's HTTPS and on port 443 ### 1.0.4 (1/17/2018) * Handle relative redirects ### 1.0.3 (12/4/2017) * Use `frozen_string_literal` pragma in all ruby files * Handle new ruby 2.5 behavior when encountering newlines in header names ### 1.0.2 (8/3/2017) * Block newlines and carriage returns in header names/values ### 1.0.1 (7/26/2017) * Fixed a bug in how ipv4-compatible and ipv4-mapped addresses were handled * Fixed a bug where the Host header did not include the port number ### 1.0.0 (7/24/2017) * Initial release arkadiyt-ssrf_filter-b87b175/CODE_OF_CONDUCT.md000066400000000000000000000000351516363764200210150ustar00rootroot00000000000000Treat everyone with respect. arkadiyt-ssrf_filter-b87b175/CONTRIBUTING.md000066400000000000000000000026051516363764200204540ustar00rootroot00000000000000# How to contribute Thank you for your interest in contributing to ssrf_filter! ### Code of conduct Please adhere to the [code of conduct](https://github.com/arkadiyt/ssrf_filter/blob/master/CODE_OF_CONDUCT.md). ### Bugs **Known issues:** Before reporting new bugs, search if your issue already exists in the [open issues](https://github.com/arkadiyt/ssrf_filter/issues). **Reporting new issues:** Provide a reduced test case with clear reproduction steps. **Security issues:** If you believe you've found a security issue please disclose it privately first, either through my [vulnerability disclosure program](https://hackerone.com/arkadiyt-projects) on Hackerone or by direct messaging me on [twitter](https://twitter.com/arkadiyt). ### Proposing a change If you plan on making large changes, please file an issue before submitting a pull request so we can reach agreement on your proposal. ### Sending a pull request 1. Fork this repository 2. Check out a feature branch: `git checkout -b your-feature-branch` 3. Make changes on your branch 4. Add/update tests - this project maintains 100% code coverage 5. Make sure all status checks pass locally: - `bundle exec bundler-audit` - `bundle exec rubocop` - `bundle exec rspec` 6. Submit a pull request with a description of your changes ### Getting in touch Feel free to tweet or direct message me: [@arkadiyt](https://twitter.com/arkadiyt) arkadiyt-ssrf_filter-b87b175/Dockerfile000066400000000000000000000003121516363764200202060ustar00rootroot00000000000000FROM ruby:3.4.9 RUN apt update && apt-get install -y vim tmux tig WORKDIR app COPY Gemfile ssrf_filter.gemspec . COPY lib/ssrf_filter/version.rb lib/ssrf_filter/version.rb RUN bundle install ENV CI=1 arkadiyt-ssrf_filter-b87b175/Gemfile000066400000000000000000000001051516363764200175070ustar00rootroot00000000000000# frozen_string_literal: true source 'https://rubygems.org' gemspec arkadiyt-ssrf_filter-b87b175/LICENSE.md000066400000000000000000000020441516363764200176240ustar00rootroot00000000000000Copyright (c) 2017 Arkadiy Tetelman Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. arkadiyt-ssrf_filter-b87b175/Makefile000066400000000000000000000002101516363764200176510ustar00rootroot00000000000000build: docker build --tag ssrf_filter . %: $(MAKE) build docker run --rm -v $${PWD}:/app -it ssrf_filter make -f Makefile.docker $@ arkadiyt-ssrf_filter-b87b175/Makefile.docker000066400000000000000000000002511516363764200211240ustar00rootroot00000000000000lint: bundle exec rubocop bundle exec bundler-audit test: bundle exec rspec bash: bash bundle: bundle install console: irb -r 'bundler/setup' -r 'ssrf_filter' arkadiyt-ssrf_filter-b87b175/README.md000066400000000000000000000141141516363764200175000ustar00rootroot00000000000000# ssrf_filter [![Gem](https://img.shields.io/gem/v/ssrf_filter.svg)](https://rubygems.org/gems/ssrf_filter) [![Tests](https://github.com/arkadiyt/ssrf_filter/actions/workflows/build-test.yml/badge.svg)](https://github.com/arkadiyt/ssrf_filter/actions/workflows/build-test.yml/badge.svg) [![Coverage Status](https://coveralls.io/repos/github/arkadiyt/ssrf_filter/badge.svg?branch=main)](https://coveralls.io/github/arkadiyt/ssrf_filter?branch=main) [![Downloads](https://img.shields.io/gem/dt/ssrf_filter?style=flat-square)](https://rubygems.org/gems/ssrf_filter) [![License](https://img.shields.io/github/license/arkadiyt/ssrf_filter.svg)](https://github.com/arkadiyt/ssrf_filter/blob/master/LICENSE.md) ## Table of Contents - [What's it for](https://github.com/arkadiyt/ssrf_filter#whats-it-for) - [Quick start](https://github.com/arkadiyt/ssrf_filter#quick-start) - [API Reference](https://github.com/arkadiyt/ssrf_filter#api-reference) - [Changelog](https://github.com/arkadiyt/ssrf_filter#changelog) - [Contributing](https://github.com/arkadiyt/ssrf_filter#contributing) ### What's it for ssrf_filter makes it easy to defend against server side request forgery (SSRF) attacks. SSRF vulnerabilities happen when you accept URLs as user input and fetch them on your server (for instance, when a user enters a link into a Twitter/Facebook status update and a content preview is generated). Users can pass in URLs or IPs such that your server will make requests to the internal network. For example if you're hosted on AWS they can request the [instance metadata endpoint](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html) `http://169.254.169.254/latest/meta-data/` and get your IAM credentials. Attempts to guard against this are often implemented incorrectly, by blocking all ip addresses, not handling IPv6 or http redirects correctly, or having TOCTTOU bugs and other issues. This gem provides a safe and easy way to fetch content from user-submitted urls. It: - handles URIs/IPv4/IPv6, redirects, DNS, etc, correctly - has 0 runtime dependencies - has a comprehensive test suite (100% code coverage) - is tested against ruby `2.7`, `3.0`, `3.1`, `3.2`, `3.3`, `3.4`, `3.5`, `4.0`, and `ruby-head` ### Quick start 1) Add the gem to your Gemfile: ```ruby gem 'ssrf_filter', '~> 1.5.0' ``` 2) In your code: ```ruby require 'ssrf_filter' response = SsrfFilter.get(params[:url]) # throws an exception for unsafe fetches response.code => "200" response.body => "\n\n\n..." ``` ### API reference `SsrfFilter.get/.put/.post/.delete/.head/.patch(url, options = {}, &block)` Fetches the requested url using a get/put/post/delete/head/patch request, respectively. Params: - `url` — the url to fetch. - `options` — options hash (described below). - `block` — a block that will receive the [HTTPRequest](https://ruby-doc.org/stdlib-2.4.1/libdoc/net/http/rdoc/Net/HTTPGenericRequest.html) object before it's sent, if you need to do any pre-processing on it (see examples below). Options hash: - `:scheme_whitelist` — an array of schemes to allow. Defaults to `%w[http https]`. - `:resolver` — a proc that receives a hostname string and returns an array of [IPAddr](https://ruby-doc.org/stdlib-2.4.1/libdoc/ipaddr/rdoc/IPAddr.html) objects. Defaults to resolving with Ruby's [Resolv](https://ruby-doc.org/stdlib-2.4.1/libdoc/resolv/rdoc/Resolv.html). See examples below for a custom resolver. - `:max_redirects` — Maximum number of redirects to follow. Defaults to 10. - `:params` — Hash of params to send with the request. - `:headers` — Hash of headers to send with the request. - `:body` — Body to send with the request. - `:http_options` – Options to pass to [Net::HTTP.start](https://ruby-doc.org/stdlib-2.6.4/libdoc/net/http/rdoc/Net/HTTP.html#method-c-start). Use this to set custom timeouts or SSL options. - `:request_proc` - a proc that receives the request object, for custom modifications before sending the request. - `:allow_unfollowed_redirects` - If true and your request hits the maximum number of redirects, the last response will be returned instead of raising an error. Defaults to false. - `:sensitive_headers` — array of header names (case-insensitive) that will not be forwarded when following a cross-origin redirect (when the scheme, host, or port changes). Defaults to `%w[authorization cookie]`. Pass `[]` to disable this protection. - `:on_cross_origin_redirect` — controls behavior when a cross-origin redirect would send sensitive headers. `:strip` (default) silently removes them and follows the redirect; `:raise` raises `SsrfFilter::CredentialLeakage` instead. Returns: An [HTTPResponse](https://ruby-doc.org/stdlib-2.4.1/libdoc/net/http/rdoc/Net/HTTPResponse.html) object if the url was fetched safely, or throws an exception if it was unsafe. All exceptions inherit from `SsrfFilter::Error`. Examples: ```ruby # GET www.example.com SsrfFilter.get('https://www.example.com') # Pass params - these are equivalent SsrfFilter.get('https://www.example.com?param=value') SsrfFilter.get('https://www.example.com', params: {'param' => 'value'}) # POST, send custom header, and don't follow redirects begin SsrfFilter.post('https://www.example.com', max_redirects: 0, headers: {'content-type' => 'application/json'}) rescue SsrfFilter::Error => e # Got an unsafe url end # Custom DNS resolution and request processing resolver = proc do |hostname| [IPAddr.new('2001:500:8f::53')] # Static resolver end # Do some extra processing on the request request_proc = proc do |request| request['content-type'] = 'application/json' request.basic_auth('username', 'password') end SsrfFilter.get('https://www.example.com', resolver: resolver, request_proc: request_proc) # Stream response SsrfFilter.get('https://www.example.com') do |response| response.read_body do |chunk| puts chunk end end ``` ### Changelog Please see [CHANGELOG.md](https://github.com/arkadiyt/ssrf_filter/blob/master/CHANGELOG.md). This project follows [semantic versioning](https://semver.org/). ### Contributing Please see [CONTRIBUTING.md](https://github.com/arkadiyt/ssrf_filter/blob/master/CONTRIBUTING.md). arkadiyt-ssrf_filter-b87b175/lib/000077500000000000000000000000001516363764200167665ustar00rootroot00000000000000arkadiyt-ssrf_filter-b87b175/lib/ssrf_filter.rb000066400000000000000000000001371516363764200216360ustar00rootroot00000000000000# frozen_string_literal: true require 'ssrf_filter/ssrf_filter' require 'ssrf_filter/version' arkadiyt-ssrf_filter-b87b175/lib/ssrf_filter/000077500000000000000000000000001516363764200213105ustar00rootroot00000000000000arkadiyt-ssrf_filter-b87b175/lib/ssrf_filter/ssrf_filter.rb000066400000000000000000000223001516363764200241540ustar00rootroot00000000000000# frozen_string_literal: true require 'ipaddr' require 'net/http' require 'resolv' require 'uri' class SsrfFilter private_class_method def self.prefixlen_from_ipaddr(ipaddr) mask_addr = ipaddr.instance_variable_get('@mask_addr') raise ArgumentError, 'Invalid mask' if mask_addr.zero? while mask_addr.nobits?(0x1) mask_addr >>= 1 end length = 0 while mask_addr & 0x1 == 0x1 length += 1 mask_addr >>= 1 end length end private_class_method def self.ipv4_from_rfc6052(ipv6_addr, prefix_len) n = ipv6_addr.to_i ipv4_int = case prefix_len when 32 then (n >> 64) & 0xFFFF_FFFF when 40 then (((n >> 64) & 0xFFFFFF) << 8) | ((n >> 48) & 0xFF) when 48 then (((n >> 64) & 0xFFFF) << 16) | ((n >> 40) & 0xFFFF) when 56 then (((n >> 64) & 0xFF) << 24) | ((n >> 32) & 0xFFFFFF) when 64 then (n >> 24) & 0xFFFF_FFFF when 96 then n & 0xFFFF_FFFF end ::IPAddr.new(ipv4_int, Socket::AF_INET) if ipv4_int end # https://en.wikipedia.org/wiki/Reserved_IP_addresses IPV4_BLACKLIST = [ ::IPAddr.new('0.0.0.0/8'), # Current network (only valid as source address) ::IPAddr.new('10.0.0.0/8'), # Private network ::IPAddr.new('100.64.0.0/10'), # Shared Address Space ::IPAddr.new('127.0.0.0/8'), # Loopback ::IPAddr.new('169.254.0.0/16'), # Link-local ::IPAddr.new('172.16.0.0/12'), # Private network ::IPAddr.new('192.0.0.0/24'), # IETF Protocol Assignments ::IPAddr.new('192.0.2.0/24'), # TEST-NET-1, documentation and examples ::IPAddr.new('192.168.0.0/16'), # Private network ::IPAddr.new('198.18.0.0/15'), # Network benchmark tests ::IPAddr.new('198.51.100.0/24'), # TEST-NET-2, documentation and examples ::IPAddr.new('203.0.113.0/24'), # TEST-NET-3, documentation and examples ::IPAddr.new('224.0.0.0/4'), # IP multicast (former Class D network) ::IPAddr.new('240.0.0.0/4'), # Reserved (former Class E network) ::IPAddr.new('255.255.255.255') # Broadcast ].freeze # NAT64 local-use prefix (RFC 8215), uses RFC 6052 /48 encoding (checked at runtime). NAT64_LOCAL_PREFIX = ::IPAddr.new('64:ff9b:1::/48').freeze IPV6_BLACKLIST = ([ ::IPAddr.new('::1/128'), # Loopback ::IPAddr.new('100::/64'), # Discard prefix (RFC 6666) ::IPAddr.new('2001::/32'), # Teredo tunneling ::IPAddr.new('2001:10::/28'), # Deprecated (previously ORCHID) ::IPAddr.new('2001:20::/28'), # ORCHIDv2 ::IPAddr.new('2001:db8::/32'), # Addresses used in documentation and example source code ::IPAddr.new('2002::/16'), # 6to4 ::IPAddr.new('fc00::/7'), # Unique local address ::IPAddr.new('fe80::/10'), # Link-local address ::IPAddr.new('ff00::/8') # Multicast ] + IPV4_BLACKLIST.flat_map do |ipaddr| prefixlen = prefixlen_from_ipaddr(ipaddr) # Don't call ipaddr.ipv4_compat because it prints out a deprecation warning on ruby 2.5+ ipv4_compatible = IPAddr.new(ipaddr.to_i, Socket::AF_INET6).mask(96 + prefixlen) ipv4_mapped = ipaddr.ipv4_mapped.mask(80 + prefixlen) # IPv4-translated (RFC 2765): ::ffff:0:x.x.x.x/96+n ipv4_translated = IPAddr.new("::ffff:0:#{ipaddr}").mask(96 + prefixlen) # NAT64 well-known prefix (RFC 6052): 64:ff9b::x.x.x.x/96+n nat64_well_known = IPAddr.new("64:ff9b::#{ipaddr}").mask(96 + prefixlen) [ipv4_compatible, ipv4_mapped, ipv4_translated, nat64_well_known] end).freeze DEFAULT_SCHEME_WHITELIST = %w[http https].freeze DEFAULT_RESOLVER = proc do |hostname| ::Resolv.getaddresses(hostname).map { |ip| ::IPAddr.new(ip) } end DEFAULT_ALLOW_UNFOLLOWED_REDIRECTS = false DEFAULT_MAX_REDIRECTS = 10 DEFAULT_SENSITIVE_HEADERS = %w[authorization cookie].freeze DEFAULT_ON_CROSS_ORIGIN_REDIRECT = :strip VERB_MAP = { get: ::Net::HTTP::Get, put: ::Net::HTTP::Put, post: ::Net::HTTP::Post, delete: ::Net::HTTP::Delete, head: ::Net::HTTP::Head, patch: ::Net::HTTP::Patch }.freeze class Error < ::StandardError end class InvalidUriScheme < Error end class PrivateIPAddress < Error end class UnresolvedHostname < Error end class TooManyRedirects < Error end class CRLFInjection < Error end class CredentialLeakage < Error end VERB_MAP.each_key do |method| define_singleton_method(method) do |url, options = {}, &block| url = url.to_s original_url = url original_uri = URI(url) scheme_whitelist = options.fetch(:scheme_whitelist, DEFAULT_SCHEME_WHITELIST) resolver = options.fetch(:resolver, DEFAULT_RESOLVER) allow_unfollowed_redirects = options.fetch(:allow_unfollowed_redirects, DEFAULT_ALLOW_UNFOLLOWED_REDIRECTS) max_redirects = options.fetch(:max_redirects, DEFAULT_MAX_REDIRECTS) sensitive_headers = options.fetch(:sensitive_headers, DEFAULT_SENSITIVE_HEADERS) response = nil (max_redirects + 1).times do uri = URI(url) unless scheme_whitelist.include?(uri.scheme) raise InvalidUriScheme, "URI scheme '#{uri.scheme}' not in whitelist: #{scheme_whitelist}" end hostname = uri.hostname ip_addresses = resolver.call(hostname) raise UnresolvedHostname, "Could not resolve hostname '#{hostname}'" if ip_addresses.empty? public_addresses = ip_addresses.reject(&method(:unsafe_ip_address?)) raise PrivateIPAddress, "Hostname '#{hostname}' has no public ip addresses" if public_addresses.empty? headers_to_strip = if !sensitive_headers.empty? && different_origin?(original_uri, uri) sensitive_headers else [] end response, url = fetch_once(uri, public_addresses.sample.to_s, method, options.merge(headers_to_strip: headers_to_strip), &block) return response if url.nil? end return response if allow_unfollowed_redirects raise TooManyRedirects, "Got #{max_redirects} redirects fetching #{original_url}" end end private_class_method def self.unsafe_ip_address?(ip_address) return true if ipaddr_has_mask?(ip_address) return IPV4_BLACKLIST.any? { |range| range.include?(ip_address) } if ip_address.ipv4? if ip_address.ipv6? return true if IPV6_BLACKLIST.any? { |range| range.include?(ip_address) } # RFC 6052 /48 encoding for NAT64 local-use prefix (RFC 8215): 64:ff9b:1::/48 # IPv4 is split around u-bits at positions 64-71, so must be decoded at runtime if NAT64_LOCAL_PREFIX.dup.include?(ip_address) ipv4 = ipv4_from_rfc6052(ip_address, 48) return unsafe_ip_address?(ipv4) end return false end true end private_class_method def self.ipaddr_has_mask?(ipaddr) range = ipaddr.to_range range.first != range.last end private_class_method def self.different_origin?(uri1, uri2) uri1.scheme != uri2.scheme || uri1.hostname != uri2.hostname || uri1.port != uri2.port end private_class_method def self.normalized_hostname(uri) # Attach port for non-default as per RFC2616 if (uri.port == 80 && uri.scheme == 'http') || (uri.port == 443 && uri.scheme == 'https') uri.hostname else "#{uri.hostname}:#{uri.port}" end end private_class_method def self.fetch_once(uri, ip, verb, options, &block) if options[:params] params = uri.query ? ::URI.decode_www_form(uri.query).to_h : {} params.merge!(options[:params]) uri.query = ::URI.encode_www_form(params) end request = VERB_MAP[verb].new(uri) request['host'] = normalized_hostname(uri) Array(options[:headers]).each do |header, value| request[header] = value end request.body = options[:body] if options[:body] options[:request_proc].call(request) if options[:request_proc].respond_to?(:call) headers_to_strip = Array(options[:headers_to_strip]) unless headers_to_strip.empty? if options[:on_cross_origin_redirect] == :raise leaking = headers_to_strip.select { |h| request[h] } unless leaking.empty? raise CredentialLeakage, "Cross-origin redirect would leak sensitive headers: #{leaking.join(', ')}" end else headers_to_strip.each { |h| request.delete(h) } end end validate_request(request) http_options = (options[:http_options] || {}).merge( use_ssl: uri.scheme == 'https', ipaddr: ip ) ::Net::HTTP.start(uri.hostname, uri.port, **http_options) do |http| response = http.request(request) do |res| block&.call(res) end case response when ::Net::HTTPRedirection url = response['location'] # Handle relative redirects url = "#{uri.scheme}://#{normalized_hostname(uri)}#{url}" if url&.start_with?('/') else url = nil end return response, url end end private_class_method def self.validate_request(request) # RFC822 allows multiline "folded" headers: # https://tools.ietf.org/html/rfc822#section-3.1 # In practice if any user input is ever supplied as a header key/value, they'll get # arbitrary header injection and possibly connect to a different host, so we block it request.each do |header, value| if header.count("\r\n") != 0 || value.count("\r\n") != 0 raise CRLFInjection, "CRLF injection in header #{header} with value #{value}" end end end end arkadiyt-ssrf_filter-b87b175/lib/ssrf_filter/version.rb000066400000000000000000000001101516363764200233120ustar00rootroot00000000000000# frozen_string_literal: true class SsrfFilter VERSION = '1.5.0' end arkadiyt-ssrf_filter-b87b175/spec/000077500000000000000000000000001516363764200171525ustar00rootroot00000000000000arkadiyt-ssrf_filter-b87b175/spec/lib/000077500000000000000000000000001516363764200177205ustar00rootroot00000000000000arkadiyt-ssrf_filter-b87b175/spec/lib/ssrf_filter_spec.rb000066400000000000000000000551201516363764200236040ustar00rootroot00000000000000# frozen_string_literal: true require 'timeout' require 'webrick/https' describe SsrfFilter do before :all do described_class.make_all_class_methods_public! end let(:public_ipv4) { IPAddr.new('172.217.6.78') } let(:private_ipv4) { IPAddr.new('127.0.0.1') } let(:public_ipv6) { IPAddr.new('2606:2800:220:1:248:1893:25c8:1946') } let(:private_ipv6) { IPAddr.new('::1') } describe 'unsafe_ip_address?' do it 'returns true if the ipaddr has a mask' do expect(described_class.unsafe_ip_address?(IPAddr.new("#{public_ipv4}/16"))).to be(true) end it 'returns true for private ipv4 addresses' do expect(described_class.unsafe_ip_address?(private_ipv4)).to be(true) end it 'returns false for public ipv4 addresses' do expect(described_class.unsafe_ip_address?(public_ipv4)).to be(false) end it 'returns true for private ipv6 addresses' do expect(described_class.unsafe_ip_address?(private_ipv6)).to be(true) end it 'returns true for mapped/compat ipv4 addresses' do described_class::IPV4_BLACKLIST.each do |addr| %i[ipv4_compat ipv4_mapped].each do |method| first = addr.to_range.first.send(method).mask(128) expect(described_class.unsafe_ip_address?(first)).to be(true) last = addr.to_range.last.send(method).mask(128) expect(described_class.unsafe_ip_address?(last)).to be(true) end end end it 'returns true for RFC 6052 /48 encoded private IPv4 addresses (nat64 local prefix)' do # 10.0.0.1 -> upper=0x0a00, lower=0x0001 -> 64:ff9b:1:a00:0:100:: expect(described_class.unsafe_ip_address?(IPAddr.new('64:ff9b:1:a00:0:100::'))).to be(true) # 192.168.1.1 -> upper=0xc0a8, lower=0x0101 -> 64:ff9b:1:c0a8:1:100:: expect(described_class.unsafe_ip_address?(IPAddr.new('64:ff9b:1:c0a8:1:100::'))).to be(true) end it 'returns false for RFC 6052 /48 encoded public IPv4 addresses (nat64 local prefix)' do # 8.8.8.8 -> upper=0x0808, lower=0x0808 -> 64:ff9b:1:808:8:800:: expect(described_class.unsafe_ip_address?(IPAddr.new('64:ff9b:1:808:8:800::'))).to be(false) end it 'returns false for public ipv6 addresses' do expect(described_class.unsafe_ip_address?(public_ipv6)).to be(false) end it 'returns true for unknown ip families' do allow(public_ipv4).to receive_messages(ipv4?: false, ipv6?: false) expect(described_class.unsafe_ip_address?(public_ipv4)).to be(true) end end describe 'ipv4_from_rfc6052' do it 'extracts the embedded IPv4 for each prefix length' do # All encode 10.0.0.1 (0x0a000001) using RFC 6052 for the given prefix length { 32 => IPAddr.new('64:ff9b:a00:1::'), # IPv4 at bits 32-63 40 => IPAddr.new('64:ff9b:10a:0:1::'), # upper 24 at 40-63, lower 8 at 72-79 48 => IPAddr.new('64:ff9b:1:a00:0:100::'), # upper 16 at 48-63, lower 16 at 72-87 56 => IPAddr.new('64:ff9b:0:a:0:1::'), # upper 8 at 56-63, lower 24 at 72-95 64 => IPAddr.new('1:2:3:4:a:0:100:0'), # IPv4 at bits 72-103 (after u-bits at 64-71) 96 => IPAddr.new('64:ff9b::a00:1') # IPv4 at bits 96-127 }.each do |prefix_len, ipv6| expect(described_class.ipv4_from_rfc6052(ipv6, prefix_len)).to eq(IPAddr.new('10.0.0.1')) end end end describe 'prefixlen_from_ipaddr' do it 'returns the prefix length' do expect(described_class.prefixlen_from_ipaddr(IPAddr.new('0.0.0.0/8'))).to eq(8) expect(described_class.prefixlen_from_ipaddr(IPAddr.new('198.18.0.0/15'))).to eq(15) expect(described_class.prefixlen_from_ipaddr(IPAddr.new('255.255.255.255'))).to eq(32) expect(described_class.prefixlen_from_ipaddr(IPAddr.new('::1'))).to eq(128) expect(described_class.prefixlen_from_ipaddr(IPAddr.new('64:ff9b::/96'))).to eq(96) expect(described_class.prefixlen_from_ipaddr(IPAddr.new('fc00::/7'))).to eq(7) end end describe 'ipaddr_has_mask?' do it 'returns true if the ipaddr has a mask' do expect(described_class.ipaddr_has_mask?(IPAddr.new("#{private_ipv4}/8"))).to be(true) end it 'returns false if the ipaddr has no mask' do expect(described_class.ipaddr_has_mask?(private_ipv4)).to be(false) expect(described_class.ipaddr_has_mask?(IPAddr.new("#{private_ipv4}/32"))).to be(false) expect(described_class.ipaddr_has_mask?(IPAddr.new("#{private_ipv6}/128"))).to be(false) end end describe 'fetch_once' do it 'sets the host header' do stub_request(:post, 'https://www.example.com').with(headers: {host: 'www.example.com'}) .to_return(status: 200, body: 'response body') response, url = described_class.fetch_once(URI('https://www.example.com'), public_ipv4.to_s, :post, {}) expect(response.code).to eq('200') expect(response.body).to eq('response body') expect(url).to be_nil end it 'does not send the port in the host header for default ports (http)' do stub_request(:post, 'http://www.example.com').with(headers: {host: 'www.example.com'}) .to_return(status: 200, body: 'response body') response, url = described_class.fetch_once(URI('http://www.example.com'), public_ipv4.to_s, :post, {}) expect(response.code).to eq('200') expect(response.body).to eq('response body') expect(url).to be_nil end it 'sends the port in the host header for non-default ports' do stub_request(:post, 'https://www.example.com:80').to_return(status: 200, body: 'response body') response, url = described_class.fetch_once(URI('https://www.example.com:80'), public_ipv4.to_s, :post, {}) expect(response.code).to eq('200') expect(response.body).to eq('response body') expect(url).to be_nil end it 'passes headers, params, and blocks' do stub_request(:get, 'https://www.example.com/?key=value').with(headers: {host: 'www.example.com', header: 'value', header2: 'value2'}).to_return(status: 200, body: 'response body') options = { headers: {'header' => 'value'}, params: {'key' => 'value'}, request_proc: proc do |req| req['header2'] = 'value2' end } uri = URI('https://www.example.com/?key=value') response, url = described_class.fetch_once(uri, public_ipv4.to_s, :get, options) expect(response.code).to eq('200') expect(response.body).to eq('response body') expect(url).to be_nil end it 'merges params' do stub_request(:get, 'https://www.example.com/?key=value&key2=value2') .to_return(status: 200, body: 'response body') uri = URI('https://www.example.com/?key=value') response, url = described_class.fetch_once(uri, public_ipv4.to_s, :get, params: {'key2' => 'value2'}) expect(response.code).to eq('200') expect(response.body).to eq('response body') expect(url).to be_nil end it 'does not use tls for http urls' do expect(Net::HTTP).to receive(:start).with('www.example.com', 80, hash_including(use_ssl: false)) described_class.fetch_once(URI('http://www.example.com'), public_ipv4.to_s, :get, {}) end it 'uses tls for https urls' do expect(Net::HTTP).to receive(:start).with('www.example.com', 443, hash_including(use_ssl: true)) described_class.fetch_once(URI('https://www.example.com'), public_ipv4.to_s, :get, {}) end it 'returns for 3xx responses with no Location header' do stub_request(:get, 'https://www.example.com/') .to_return(status: 304) uri = URI('https://www.example.com/') response, url = described_class.fetch_once(uri, public_ipv4.to_s, :get, {}) expect(response.code).to eq('304') expect(url).to be_nil end end describe 'validate_request' do it 'disallows header names with newlines and carriage returns' do expect do described_class.get("https://#{public_ipv4}", headers: {"nam\ne" => 'value'}) end.to raise_error(described_class::CRLFInjection) expect do described_class.get("https://#{public_ipv4}", headers: {"nam\re" => 'value'}) end.to raise_error(described_class::CRLFInjection) end it 'disallows header values with newlines and carriage returns' do # In more recent versions of ruby, assigning a header value with newlines throws an ArgumentError major, minor = RUBY_VERSION.scan(/\A(\d+)\.(\d+)\.\d+\Z/).first.map(&:to_i) exception = major >= 3 || (major >= 2 && minor >= 3) ? ArgumentError : described_class::CRLFInjection expect do described_class.get("https://#{public_ipv4}", headers: {'name' => "val\nue"}) end.to raise_error(exception) expect do described_class.get("https://#{public_ipv4}", headers: {'name' => "val\rue"}) end.to raise_error(exception) end end describe 'integration tests' do # To hit 100% code coverage, we need to make a real connection to a TLS-enabled server. # To do this we create a private key and certificate, spin up a web server in # a thread (serving traffic on localhost), and make a request to the server. This requires several things: # 1) creating a custom trust store with our certificate and using that for validation # 2) allowing (non-mocked) network connections # 3) stubbing out the IPV4_BLACKLIST to allow connections to localhost allow_net_connections_for_context(self) def make_keypair(subject) private_key = OpenSSL::PKey::RSA.new(2048) public_key = private_key.public_key subject = OpenSSL::X509::Name.parse(subject) certificate = OpenSSL::X509::Certificate.new certificate.subject = subject certificate.issuer = subject certificate.not_before = Time.now certificate.not_after = Time.now + (60 * 60 * 24) certificate.public_key = public_key certificate.serial = 0x0 certificate.version = 2 certificate.sign(private_key, OpenSSL::Digest.new('SHA256')) [private_key, certificate] end def make_web_server(port, private_key, certificate, opts = {}, &block) server = WEBrick::HTTPServer.new({ BindAddress: '127.0.0.1', Port: port, SSLEnable: true, SSLCertificate: certificate, SSLPrivateKey: private_key, StartCallback: block }.merge(opts)) server.mount_proc '/' do |req, res| res.status = 200 res['X-Subject'] = certificate.subject res['X-Host'] = req['host'] end server end def inject_custom_trust_store(*certificates) store = OpenSSL::X509::Store.new certificates.each do |certificate| store.add_cert(certificate) end expect(Net::HTTP).to receive(:start).exactly(certificates.length).times .and_wrap_original do |orig, *args, &block| args.last[:cert_store] = store # Inject our custom trust store orig.call(*args, &block) end end it 'validates TLS certificates' do hostname = 'ssrf-filter.example.com' port = 8443 private_key, certificate = make_keypair("CN=#{hostname}") stub_const('SsrfFilter::IPV4_BLACKLIST', []) inject_custom_trust_store(certificate) begin queue = Queue.new # Used as a semaphore web_server_thread = Thread.new do make_web_server(port, private_key, certificate) do queue.push(nil) end.start end Timeout.timeout(2) do queue.pop response = described_class.get("https://#{hostname}:#{port}", resolver: proc { [IPAddr.new('127.0.0.1')] }) expect(response.code).to eq('200') expect(response['X-Subject']).to eq("/CN=#{hostname}") expect(response['X-Host']).to eq("#{hostname}:#{port}") end ensure web_server_thread&.kill end end it 'connects when using SNI' do require 'webrick/https' port = 8443 private_key, certificate = make_keypair('CN=localhost') virtualhost_private_key, virtualhost_certificate = make_keypair('CN=virtualhost') stub_const('SsrfFilter::IPV4_BLACKLIST', []) inject_custom_trust_store(certificate, virtualhost_certificate) begin queue = Queue.new # Used as a semaphore web_server_thread = Thread.new do server = make_web_server(port, private_key, certificate, ServerName: 'localhost') do queue.push(nil) end options = {ServerName: 'virtualhost', DoNotListen: true} virtualhost = make_web_server(port, virtualhost_private_key, virtualhost_certificate, options) server.virtual_host(virtualhost) server.start end Timeout.timeout(2) do queue.pop options = { resolver: proc { [IPAddr.new('127.0.0.1')] } } response = described_class.get("https://localhost:#{port}", options) expect(response.code).to eq('200') expect(response['X-Subject']).to eq('/CN=localhost') expect(response['X-Host']).to eq("localhost:#{port}") response = described_class.get("https://virtualhost:#{port}", options) expect(response.code).to eq('200') expect(response['X-Subject']).to eq('/CN=virtualhost') expect(response['X-Host']).to eq("virtualhost:#{port}") end ensure web_server_thread&.kill end end it 'supports chunked responses' do hostname = 'ssrf-filter.example.com' port = 8443 private_key, certificate = make_keypair("CN=#{hostname}") inject_custom_trust_store(certificate) stub_const('SsrfFilter::IPV4_BLACKLIST', []) begin queue = Queue.new # Used as a semaphore chunks = ['chunk 1', 'chunk 2', 'chunk 3'] web_server_thread = Thread.new do server = make_web_server(port, private_key, certificate) do queue.push(nil) end server.mount_proc '/chunked' do |_, res| res.status = 200 res.chunked = true res.body = proc do |chunked_wrapper| chunks.each { |chunk| chunked_wrapper.write(chunk) } end end server.start end Timeout.timeout(2) do queue.pop chunk_index = 0 url = "https://#{hostname}:#{port}/chunked" described_class.get(url, resolver: proc { [IPAddr.new('127.0.0.1')] }) do |response| expect(response.code).to eq('200') response.read_body do |chunk| expect(chunk).to eq(chunks[chunk_index]) chunk_index += 1 end end expect(chunk_index).to eq(chunks.length) end ensure web_server_thread&.kill end end it 'does not break when reading the body without using a block' do port = 8443 private_key, certificate = make_keypair('CN=localhost') inject_custom_trust_store(certificate) stub_const('SsrfFilter::IPV4_BLACKLIST', []) begin queue = Queue.new # Used as a semaphore web_server_thread = Thread.new do server = make_web_server(port, private_key, certificate) do queue.push(nil) end server.mount('/README.md', WEBrick::HTTPServlet::FileHandler, 'README.md') server.start end Timeout.timeout(2) do queue.pop options = { resolver: proc { [IPAddr.new('127.0.0.1')] } } response = described_class.get("https://localhost:#{port}/README.md", options) expect(response.code).to eq('200') expect(response.body).to match(/ssrf_filter/) end ensure web_server_thread&.kill end end end describe 'get/put/post/delete' do it 'fails if the scheme is not in the default whitelist' do expect do described_class.get('ftp://example.com') end.to raise_error(described_class::InvalidUriScheme) end it 'fails if the scheme is not in a custom whitelist' do expect do described_class.get('https://example.com', scheme_whitelist: []) end.to raise_error(described_class::InvalidUriScheme) end it 'fails if the hostname does not resolve' do expect(Resolv).to receive(:getaddresses).and_return([]) expect do described_class.get('https://example.com') end.to raise_error(described_class::UnresolvedHostname) end it 'fails if the hostname does not resolve with a custom resolver' do called = false resolver = proc do called = true [] end expect(described_class::DEFAULT_RESOLVER).not_to receive(:call) expect do described_class.get('https://example.com', resolver: resolver) end.to raise_error(described_class::UnresolvedHostname) expect(called).to be(true) end it 'fails if the hostname has no public ip address' do expect(described_class::DEFAULT_RESOLVER).to receive(:call).and_return([private_ipv4]) expect do described_class.get('https://example.com') end.to raise_error(described_class::PrivateIPAddress) end it 'fails if there are too many redirects' do stub_request(:get, 'https://www.example.com').to_return(status: 301, headers: {location: 'https://example2.com'}) expect do described_class.get('https://www.example.com', max_redirects: 0) end.to raise_error(described_class::TooManyRedirects) end it 'returns the last response if there are too many redirects and unfollowed redirects are allowed' do stub_request(:get, 'https://www.example.com').to_return(status: 301, headers: {location: 'https://www.example2.com'}) response = described_class.get( 'https://www.example.com', allow_unfollowed_redirects: true, max_redirects: 0 ) expect(response.code).to eq('301') expect(response['location']).to eq('https://www.example2.com') end it 'fails if the redirected url is not in the scheme whitelist' do stub_request(:put, 'https://www.example.com').to_return(status: 301, headers: {location: 'ftp://www.example.com'}) expect do described_class.put('https://www.example.com') end.to raise_error(described_class::InvalidUriScheme) end it 'fails if the redirected url has no public ip address' do stub_request(:delete, 'https://www.example.com').to_return(status: 301, headers: {location: 'https://www.example2.com'}) resolver = proc { [private_ipv6] } expect do described_class.delete('https://www.example.com', resolver: resolver) end.to raise_error(described_class::PrivateIPAddress) end it 'fails when the hostname or path contain linefeeds and carriage returns' do [ "https://www.exam\nple.com", "https://www.exam\rple.com", "https://www.example.com/te\nst", "https://www.example.com/te\rst" ].each do |uri| expect do described_class.get(uri) end.to raise_error(URI::InvalidURIError) end end it 'follows redirects and succeed on a public hostname' do stub_request(:post, 'https://www.example.com/path?key=value').to_return(status: 301, headers: {location: 'https://www.example2.com/path2?key2=value2'}) stub_request(:post, 'https://www.example2.com/path2?key2=value2').to_return(status: 200, body: 'response body') response = described_class.post('https://www.example.com/path?key=value') expect(response.code).to eq('200') expect(response.body).to eq('response body') end it 'follows relative redirects and succeed' do stub_request(:post, 'https://www.example.com/path?key=value').to_return(status: 301, headers: {location: '/path2?key2=value2'}) stub_request(:post, 'https://www.example.com/path2?key2=value2').to_return(status: 200, body: 'response body') response = described_class.post('https://www.example.com/path?key=value') expect(response.code).to eq('200') expect(response.body).to eq('response body') end it 'strips sensitive headers set via request_proc on cross-origin redirect' do stub_request(:get, 'https://www.example.com/').to_return(status: 301, headers: {location: 'https://www.example2.com/'}) stub_request(:get, 'https://www.example2.com/').to_return(status: 200) auth_header = {'Authorization' => 'Bearer token'} request_proc = proc { |req| req['Authorization'] = 'Bearer token' } described_class.get('https://www.example.com/', request_proc: request_proc) expect(a_request(:get, 'https://www.example2.com/').with(headers: auth_header)).not_to have_been_made expect(a_request(:get, 'https://www.example2.com/')).to have_been_made end it 'raises CredentialLeakage on cross-origin redirect when on_cross_origin_redirect is :raise' do stub_request(:get, 'https://www.example.com/').to_return(status: 301, headers: {location: 'https://www.example2.com/'}) stub_request(:get, 'https://www.example2.com/').to_return(status: 200) expect do described_class.get('https://www.example.com/', headers: {'Authorization' => 'Bearer token'}, on_cross_origin_redirect: :raise) end.to raise_error(described_class::CredentialLeakage) end it 'preserves sensitive headers for same-origin hops but strips them on a subsequent cross-origin hop' do stub_request(:get, 'https://www.example.com/').to_return(status: 301, headers: {location: 'https://www.example.com/other'}) stub_request(:get, 'https://www.example.com/other').to_return(status: 301, headers: {location: 'https://www.example2.com/'}) stub_request(:get, 'https://www.example2.com/').to_return(status: 200) auth_header = {'Authorization' => 'Bearer token'} described_class.get('https://www.example.com/', headers: auth_header) expect(a_request(:get, 'https://www.example.com/other').with(headers: auth_header)).to have_been_made expect(a_request(:get, 'https://www.example2.com/').with(headers: auth_header)).not_to have_been_made end it 'strips sensitive headers on a same-origin redirect after an earlier cross-origin redirect' do stub_request(:get, 'https://www.example.com/').to_return(status: 301, headers: {location: 'https://www.example2.com/'}) stub_request(:get, 'https://www.example2.com/').to_return(status: 301, headers: {location: 'https://www.example2.com/other'}) stub_request(:get, 'https://www.example2.com/other').to_return(status: 200) auth_header = {'Authorization' => 'Bearer token'} described_class.get('https://www.example.com/', headers: auth_header) expect(a_request(:get, 'https://www.example2.com/').with(headers: auth_header)).not_to have_been_made expect(a_request(:get, 'https://www.example2.com/other').with(headers: auth_header)).not_to have_been_made end end end arkadiyt-ssrf_filter-b87b175/spec/spec_helper.rb000066400000000000000000000014501516363764200217700ustar00rootroot00000000000000# frozen_string_literal: true require 'simplecov' require 'simplecov-lcov' SimpleCov.start do SimpleCov::Formatter::LcovFormatter.config do |c| c.report_with_single_file = true c.single_report_path = 'coverage/lcov.info' end SimpleCov.formatters = SimpleCov::Formatter::MultiFormatter.new([ SimpleCov::Formatter::HTMLFormatter, SimpleCov::Formatter::LcovFormatter ]) add_filter %w[spec] end require 'webmock/rspec' require 'ssrf_filter' def allow_net_connections_for_context(context) context.before :all do WebMock.disable! end context.after :all do WebMock.enable! end end Object.class_eval do def self.make_all_class_methods_public! private_methods.each(&method(:public_class_method)) protected_methods.each(&method(:public_class_method)) end end arkadiyt-ssrf_filter-b87b175/ssrf_filter.gemspec000066400000000000000000000025341516363764200221130ustar00rootroot00000000000000# frozen_string_literal: true $LOAD_PATH.unshift File.expand_path('lib', __dir__) require 'ssrf_filter/version' Gem::Specification.new do |gem| gem.name = 'ssrf_filter' gem.platform = Gem::Platform::RUBY gem.version = SsrfFilter::VERSION gem.authors = ['Arkadiy Tetelman'] gem.required_ruby_version = '>= 2.7.0' gem.summary = 'A gem that makes it easy to prevent server side request forgery (SSRF) attacks' gem.description = gem.summary gem.homepage = 'https://github.com/arkadiyt/ssrf_filter' gem.license = 'MIT' gem.files = Dir['lib/**/*.rb'] gem.metadata = {'changelog_uri' => "#{gem.homepage}/blob/main/CHANGELOG.md", 'rubygems_mfa_required' => 'true'} gem.add_development_dependency('base64', '~> 0.2.0') # For ruby >= 3.4 gem.add_development_dependency('bundler-audit', '~> 0.9.2') gem.add_development_dependency('rspec', '~> 3.13.0') gem.add_development_dependency('rubocop', '~> 1.68.0') gem.add_development_dependency('rubocop-rspec', '~> 3.2.0') gem.add_development_dependency('simplecov', '~> 0.22.0') gem.add_development_dependency('simplecov-lcov', '~> 0.8.0') gem.add_development_dependency('tsort', '0.1.0') # Removed from standard gems in ruby >= 4.1 gem.add_development_dependency('webmock', '>= 3.24.0') gem.add_development_dependency('webrick') end