CVE-2024-32977: Insufficient Information (NVD-CWE-noinfo)
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.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-05-14T16:17Z
Repository: https://github.com/OctoPrint/OctoPrint
Language: Python
Files changed: tests/util/test_net.py
Commit date: 2024-05-13 15:26:34 +0200
Commit message: Merge branch 'regressionfix/xff-issue' into staging/bugfix

Diff: diff --cc tests/util/test_net.py
index 53108a8bde,9d523c0bee..c2eb0ac71d
--- a/tests/util/test_net.py
+++ b/tests/util/test_net.py
@@@ -27,21 -26,13 +26,21 @@@ def patched_ifaddresses(addr, netmask)
          }
  
      return {}
  
  
 +def patched_ifaddresses_mask(addr):
 +    return patched_ifaddresses(addr, "mask")
 +
 +
 +def patched_ifaddresses_netmask(addr):
 +    return patched_ifaddresses(addr, "netmask")
 +
 +
- @ddt.ddt
- class UtilNetTest(unittest.TestCase):
-     @ddt.data(
+ @pytest.mark.parametrize(
+     "input_address,input_additional,expected",
+     [
          ("127.0.0.1", [], True),
          ("192.168.123.234", [], True),
          ("172.24.0.1", [], True),
          ("10.1.2.3", [], True),
          ("fc00::1", [], True),
@@@ -52,53 -43,144 +51,162 @@@
          ("11.1.2.3", ["11/8"], True),
          ("12.1.1.123", [], True),
          ("2a01:4f8:1c0c:6958::1:23", [], True),
          ("fe80::89f3:31bb:ced0:2093%wlan0", [], True),
          (None, [], True),
-     )
-     @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):
+     ],
+ )
+ def test_is_lan_address(input_address, input_additional, expected):
 -    with mock.patch("netifaces.interfaces", side_effect=patched_interfaces), mock.patch(
 -        "netifaces.ifaddresses", side_effect=patched_ifaddresses
++    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
 -        )
 +        for side_effect in (patched_ifaddresses_mask, patched_ifaddresses_netmask):
 +            with mock.patch("netifaces.ifaddresses", side_effect=side_effect):
-                 actual = octoprint.util.net.is_lan_address(
-                     input_address, additional_private=input_additional
++                assert (
++                    octoprint.util.net.is_lan_address(
++                        input_address, additional_private=input_additional
++                    )
++                    == expected
 +                )
-                 self.assertEqual(expected, actual)
  
-     @ddt.data(
+ 
+ @pytest.mark.parametrize(
+     "address,expected",
+     [
          ("fe80::89f3:31bb:ced0:2093%wlan0", "fe80::89f3:31bb:ced0:2093"),
          ("2a01:4f8:1c0c:6958::1:23", "2a01:4f8:1c0c:6958::1:23"),
          ("10.1.2.3", "10.1.2.3"),
-     )
-     @ddt.unpack
-     def test_strip_interface_tag(self, address, expected):
-         actual = octoprint.util.net.strip_interface_tag(address)
-         self.assertEqual(expected, actual)
+     ],
+ )
+ def test_strip_interface_tag(address, expected):
+     assert octoprint.util.net.strip_interface_tag(address) == expected
  
-     @ddt.data(
+ 
+ @pytest.mark.parametrize(
+     "address,expected",
+     [
          ("::ffff:192.168.1.1", "192.168.1.1"),
          ("::ffff:2a01:4f8", "::ffff:2a01:4f8"),
          ("2a01:4f8:1c0c:6958::1:23", "2a01:4f8:1c0c:6958::1:23"),
          ("11.1.2.3", "11.1.2.3"),
-     )
-     @ddt.unpack
-     def test_unmap_v4_in_v6(self, address, expected):
-         actual = octoprint.util.net.unmap_v4_as_v6(address)
-         self.assertEqual(expected, actual)
+     ],
+ )
+ def test_unmap_v4_in_v6(address, expected):
+     assert octoprint.util.net.unmap_v4_as_v6(address) == expected
+ 
  
-     @ddt.data(
++@pytest.mark.parametrize(
++    "address,expected",
++    [
 +        ({"mask": "192.168.0.0/24"}, "192.168.0.0/24"),
 +        ({"netmask": "192.168.0.0/24"}, "192.168.0.0/24"),
++    ],
++)
++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
      )
-     @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)

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