CVE-2023-28111 (published 2023-03-17T17:15Z)

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.

CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-918
cwe_name: Server-Side Request Forgery (SSRF)
description: 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.
extended_description: By providing URLs to unexpected hosts or ports, attackers can make it appear that the server is sending the request, possibly bypassing access controls such as firewalls that prevent the attackers from accessing the URLs directly. The server can be used as a proxy to conduct port scanning of hosts in internal networks, use other URLs such as that can access documents on the system (using file://), or use other protocols such as gopher:// or tftp://, which may provide greater control over the contents of requests.
         

Fix information:
repo_url: https://github.com/discourse/discourse
commit_details: repo_url: https://github.com/discourse/discourse
author: Alan Guo Xiang Tan
author_date: 2023-03-06 09:20:12+08:00
msg: 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
repository: repo_url: https://github.com/discourse/discourse
repo_name: discourse/discourse
description: A platform for community discussion. Free, open, simple.
repo_language: Ruby
owner: discourse
file_changes: filename: ssrf_detector.rb
change_type: ModificationType.MODIFY
diff: @@ -7,18 +7,47 @@ class FinalDestination
     class LookupFailedError < SocketError
     end
 
-    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
+    # 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
 
     def self.blocked_ip_blocks
       SiteSetting
@@ -54,10 +83,9 @@ class FinalDestination
 
     def self.ip_allowed?(ip)
       ip = ip.is_a?(IPAddr) ? ip : IPAddr.new(ip)
+      ip = ip.native
 
-      if ip_in_ranges?(ip, blocked_ip_blocks) || ip_in_ranges?(ip, standard_private_ranges)
-        return false
-      end
+      return false if ip_in_ranges?(ip, blocked_ip_blocks) || ip_in_ranges?(ip, PRIVATE_IP_RANGES)
 
       true
     end

programming_language: Ruby
filename: ssrf_detector_spec.rb
change_type: ModificationType.MODIFY
diff: @@ -43,9 +43,19 @@ describe FinalDestination::SSRFDetector do
       expect(subject.ip_allowed?("9001:82f3:8873::3")).to eq(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)
+    %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)
     end
   end
 

programming_language: Ruby