CVE-2024-32977 (Python, published 2024-05-14T16:17Z)
Description: value: OctoPrint provides a web interface for controlling consumer 3D printers. OctoPrint versions up until and including 1.10.0 contain a vulnerability that allows an unauthenticated attacker to completely bypass the authentication if the `autologinLocal` option is enabled within `config.yaml`, even if they come from networks that are not configured as `localNetworks`, spoofing their IP via the `X-Forwarded-For` header. If autologin is not enabled, this vulnerability does not have any impact. The vulnerability has been patched in version 1.10.1. Until the patch has been applied, OctoPrint administrators who have autologin enabled on their instances should disable it and/or to make the instance inaccessible from potentially hostile networks like the internet.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/OctoPrint/OctoPrint
Files changed:
tests/util/test_net.py
Commit message: Merge branch 'regressionfix/xff-issue' into staging/bugfix


Vulnerable code:
 @ddt.ddt
 class UtilNetTest(unittest.TestCase):
     @ddt.data(
     )
     @ddt.unpack
     @mock.patch("netifaces.interfaces", side_effect=patched_interfaces)
     @mock.patch.object(octoprint.util.net, "HAS_V6", True)
     def test_is_lan_address(self, input_address, input_additional, expected, nifs):
                 actual = octoprint.util.net.is_lan_address(
                     input_address, additional_private=input_additional
                 self.assertEqual(expected, actual)
     @ddt.data(
     )
     @ddt.unpack
     def test_unmap_v4_in_v6(self, address, expected):
         actual = octoprint.util.net.unmap_v4_as_v6(address)
         self.assertEqual(expected, actual)
     @ddt.data(
     @ddt.unpack
     def test_get_netmask(self, address, expected):
         actual = octoprint.util.net.get_netmask(address)
         self.assertEqual(expected, actual)
 
     def test_get_netmask_broken_address(self):
         try:
             octoprint.util.net.get_netmask({"nm": "192.168.0.0/24"})
             self.fail("Expected ValueError")
         except Exception as e:
             self.assertIsInstance(e, ValueError)

Fixed code:
 @pytest.mark.parametrize(
     "input_address,input_additional,expected",
     [
     ],
 )
 def test_is_lan_address(input_address, input_additional, expected):
+    with mock.patch(
+        "netifaces.interfaces", side_effect=patched_interfaces
     ), mock.patch.object(octoprint.util.net, "HAS_V6", True):
+                assert (
+                    octoprint.util.net.is_lan_address(
+                        input_address, additional_private=input_additional
+                    )
+                    == expected
 
 @pytest.mark.parametrize(
     "address,expected",
     [
     ],
 )
 def test_unmap_v4_in_v6(address, expected):
     assert octoprint.util.net.unmap_v4_as_v6(address) == expected
 
+@pytest.mark.parametrize(
+    "address,expected",
+    [
+    ],
+)
+def test_get_netmask(address, expected):
+    assert octoprint.util.net.get_netmask(address) == expected
+
+
+def test_get_netmask_broken_address():
+    with pytest.raises(ValueError):
+        octoprint.util.net.get_netmask({"nm": "192.168.0.0/24"})
+
+
 @pytest.mark.parametrize(
     "remote_addr,header,trusted_proxies,expected",
     [
         (
             "127.0.0.1",
             None,
             ["127.0.0.1", "::1"],
             "127.0.0.1",
         ),  # direct access via ipv4 localhost
         (
             "::1",
             None,
             ["127.0.0.1", "::1"],
             "::1",
         ),  # direct access via ipv6 localhost
         (
             "192.168.1.10",
             None,
             ["127.0.0.1", "::1"],
             "192.168.1.10",
         ),  # direct access via lan
         (
             "127.0.0.1",
             "192.168.1.10",
             ["127.0.0.1", "::1"],
             "192.168.1.10",
         ),  # access through reverse proxy on 127.0.0.1
         (
             "127.0.0.1",
             "10.1.2.3, 192.168.1.10",
             ["127.0.0.1", "::1", "192.168.1.10"],
             "10.1.2.3",
         ),  # access through trusted reverse proxies on 127.0.0.1 and 192.168.1.10
         (
             "127.0.0.1",
             "10.1.2.3, 192.168.1.10",
             ["127.0.0.1", "::1", "192.168.1.0/24"],
             "10.1.2.3",
         ),  # access through trusted reverse proxies on 127.0.0.1 and something on 192.168.1.0/24
         (
             "127.0.0.1",
             "10.1.2.3, 192.168.1.10",
             ["127.0.0.1", "::1", "unknown", "192.168.1.0/24"],
             "10.1.2.3",
         ),  # access through trusted reverse proxies on 127.0.0.1 and something on 192.168.1.0/24, invalid proxy in between
         (
             "::1",
             "fd12:3456:789a:2::1, fd12:3456:789a:1::1",
             ["127.0.0.1", "::1", "fd12:3456:789a:1::/64"],
             "fd12:3456:789a:2::1",
         ),  # access through trusted reverse proxies on ::1 and something on fd12:3456:789a:1::/64
         (
             "127.100.100.1",
             "10.1.2.3, 192.168.1.10",
             ["0.0.0.0/0"],
             "127.100.100.1",
         ),  # everything is trusted (BAD IDEA!)
         (
             "192.168.1.10",
             "127.0.0.1",
             ["127.0.0.1", "::1"],
             "192.168.1.10",
         ),  # spoofing attempt #1: direct access via lan, spoofed to 127.0.0.1
         (
             "::ffff:192.168.1.10",
             "::1",
             ["127.0.0.1", "::1"],
             "192.168.1.10",
         ),  # spoofing attempt #2: direct access via lan, spoofed to ::1
         (
             "127.0.0.1",
             "127.0.0.1, 192.168.1.10",
             ["127.0.0.1", "::1"],
             "192.168.1.10",
         ),  # spoofing attempt #3: access through reverse proxy on 127.0.0.1, real ip 192.168.1.10, spoofed to 127.0.0.1
         (
             "::1",
             "::1, ::ffff:192.168.1.10",
             ["127.0.0.1", "::1"],
             "192.168.1.10",
         ),  # spoofing attempt #4: access through reverse proxy on ::1, real ip 192.168.1.10, spoofed to ::1
         (
             "127.0.0.1",
             "127.0.0.1, 10.1.2.3, 192.168.1.10",
             ["127.0.0.1", "::1", "192.168.1.10"],
             "10.1.2.3",
         ),  # spoofing attempt #5: access through trusted reverse proxies on 127.0.0.1 and 192.168.1.10, real ip 10.1.2.3, spoofed to 127.0.0.1
         (
             "::1",
             "::1, fd12:3456:789a:2::1, fd12:3456:789a:1::1",
             ["127.0.0.1", "::1", "fd12:3456:789a:1::/64"],
             "fd12:3456:789a:2::1",
         ),  # spoofing attempt #6: access through trusted reverse proxies on ::1 and something on fd12:3456:789a:1::/64, spoofed to ::1
     ],
 )
 def test_get_http_client_ip(remote_addr, header, trusted_proxies, expected):
     assert (
         octoprint.util.net.get_http_client_ip(remote_addr, header, trusted_proxies)
         == expected