CVE-2023-28111 (Ruby, published 2023-03-17T17:15Z)
Description: value: Discourse is an open-source discussion platform. Prior to version 3.1.0.beta3 of the `beta` and `tests-passed` branches, attackers are able to bypass Discourse's server-side request forgery (SSRF) protection for private IPv4 addresses by using a IPv4-mapped IPv6 address. The issue is patched in the latest beta and tests-passed version of Discourse. version 3.1.0.beta3 of the `beta` and `tests-passed` branches. There are no known workarounds.
Severity: nan (CVSS v3 base score: 7.5)
Weakness: CWE-918 \u2014 Server-Side Request Forgery (SSRF)
The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
Repository: https://github.com/discourse/discourse
Files changed:
lib/final_destination/ssrf_detector.rb
spec/lib/final_destination/ssrf_detector_spec.rb
Commit message: SECURITY: SSRF protection bypass with IPv4-mapped IPv6 addresses

As part of this commit, we've also expanded our list of private IP
ranges based on
https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
and https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml


Vulnerable code:
    def self.standard_private_ranges
      @private_ranges ||= [
        IPAddr.new("0.0.0.0/8"),
        IPAddr.new("127.0.0.1"),
        IPAddr.new("172.16.0.0/12"),
        IPAddr.new("192.168.0.0/16"),
        IPAddr.new("10.0.0.0/8"),
        IPAddr.new("::1"),
        IPAddr.new("fc00::/7"),
        IPAddr.new("fe80::/10"),
      ]
    end
      if ip_in_ranges?(ip, blocked_ip_blocks) || ip_in_ranges?(ip, standard_private_ranges)
        return false
      end
    it "returns false for standard internal IPs" do
      expect(subject.ip_allowed?("172.31.100.31")).to eq(false)
      expect(subject.ip_allowed?("fd02:77fa:ffea::f")).to eq(false)

Fixed code:
    # This is a list of private IPv4 IP ranges that are not allowed to be globally reachable as given by
    # https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml.
    PRIVATE_IPV4_RANGES = [
      IPAddr.new("0.0.0.0/8"),
      IPAddr.new("10.0.0.0/8"),
      IPAddr.new("100.64.0.0/10"),
      IPAddr.new("127.0.0.0/8"),
      IPAddr.new("169.254.0.0/16"),
      IPAddr.new("172.16.0.0/12"),
      IPAddr.new("192.0.0.0/24"),
      IPAddr.new("192.0.0.0/29"),
      IPAddr.new("192.0.0.8/32"),
      IPAddr.new("192.0.0.170/32"),
      IPAddr.new("192.0.0.171/32"),
      IPAddr.new("192.0.2.0/24"),
      IPAddr.new("192.168.0.0/16"),
      IPAddr.new("192.175.48.0/24"),
      IPAddr.new("198.18.0.0/15"),
      IPAddr.new("198.51.100.0/24"),
      IPAddr.new("203.0.113.0/24"),
      IPAddr.new("240.0.0.0/4"),
      IPAddr.new("255.255.255.255/32"),
    ]

    # This is a list of private IPv6 IP ranges that are not allowed to be globally reachable as given by
    # https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml.
    #
    # ::ffff:0:0/96 is excluded from the list because it is used for IPv4-mapped IPv6 addresses which is something we want to allow.
    PRIVATE_IPV6_RANGES = [
      IPAddr.new("::1/128"),
      IPAddr.new("::/128"),
      IPAddr.new("64:ff9b:1::/48"),
      IPAddr.new("100::/64"),
      IPAddr.new("2001::/23"),
      IPAddr.new("2001:2::/48"),
      IPAddr.new("2001:db8::/32"),
      IPAddr.new("fc00::/7"),
      IPAddr.new("fe80::/10"),
    ]

    PRIVATE_IP_RANGES = PRIVATE_IPV4_RANGES + PRIVATE_IPV6_RANGES
      ip = ip.native
      return false if ip_in_ranges?(ip, blocked_ip_blocks) || ip_in_ranges?(ip, PRIVATE_IP_RANGES)
    %w[0.0.0.0 10.0.0.0 127.0.0.0 172.31.100.31 255.255.255.255 ::1 ::].each do |internal_ip|
      it "returns false for '#{internal_ip}'" do
        expect(subject.ip_allowed?(internal_ip)).to eq(false)
      end
    end

    it "returns false for private IPv4-mapped IPv6 addresses" do
      expect(subject.ip_allowed?("::ffff:172.31.100.31")).to eq(false)
      expect(subject.ip_allowed?("::ffff:0.0.0.0")).to eq(false)
    end

    it "returns true for public IPv4-mapped IPv6 addresses" do
      expect(subject.ip_allowed?("::ffff:52.52.167.244")).to eq(true)