Version in base suite: 26.0.3-0+deb13u2 Base version: neutron_26.0.3-0+deb13u2 Target version: neutron_26.0.3-0+deb13u3 Base file: /srv/ftp-master.debian.org/ftp/pool/main/n/neutron/neutron_26.0.3-0+deb13u2.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/n/neutron/neutron_26.0.3-0+deb13u3.dsc changelog | 12 patches/CVE-2026-55707-stable-2025.1.patch | 94 ++++ patches/OSSN-0102-Fix_PF_GET_PUT_parent_floating_IP_validation.patch | 92 ++++ patches/OSSN-0102_Fix_cross-project_access_to_router_conntrack_helpers.patch | 203 ++++++++++ patches/series | 3 5 files changed, 404 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp5cb9oc1e/neutron_26.0.3-0+deb13u2.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp5cb9oc1e/neutron_26.0.3-0+deb13u3.dsc: no acceptable signature found diff -Nru neutron-26.0.3/debian/changelog neutron-26.0.3/debian/changelog --- neutron-26.0.3/debian/changelog 2026-06-05 09:00:14.000000000 +0000 +++ neutron-26.0.3/debian/changelog 2026-08-03 07:47:00.000000000 +0000 @@ -1,3 +1,15 @@ +neutron (2:26.0.3-0+deb13u3) trixie-security; urgency=medium + + * CVE-2026-55707: subnetpool onboarding cross-project subnet mutation. + Applied upstream fix: CVE-2026-55707-stable-2025.1.patch. + (Closes: #1143170). + * OSSN-0102: sub-resource APIs do not verify parent ownership. Applied + upstream patches (Closes: #1142937): + - "Fix PF GET/PUT parent floating IP validation". + - "Fix cross-project access to router conntrack helpers" + + -- Thomas Goirand Mon, 03 Aug 2026 09:47:00 +0200 + neutron (2:26.0.3-0+deb13u2) trixie-security; urgency=medium * New upstream point release. diff -Nru neutron-26.0.3/debian/patches/CVE-2026-55707-stable-2025.1.patch neutron-26.0.3/debian/patches/CVE-2026-55707-stable-2025.1.patch --- neutron-26.0.3/debian/patches/CVE-2026-55707-stable-2025.1.patch 1970-01-01 00:00:00.000000000 +0000 +++ neutron-26.0.3/debian/patches/CVE-2026-55707-stable-2025.1.patch 2026-08-03 07:47:00.000000000 +0000 @@ -0,0 +1,94 @@ +Description: Prevent cross-project subnet onboard on shared networks + Non-admin callers with visibility to a shared or RBAC network + could onboard subnets owned by another project into their own + subnetpool via ``onboard_network_subnets()``. This allowed the + caller to alter the address-scope and L3 routing state of the + network owner's routers. + . + Add a project ownership check that rejects non-admin requests + when the caller's ``project_id`` does not match the network's + ``project_id``. +Author: Rodolfo Alonso Hernandez +Date: Tue, 21 Jul 2026 12:30:04 +0200 +Signed-off-by: Rodolfo Alonso Hernandez +Change-Id: Ie484bff86f082c38a69d238e414d543200403402 +Bug: https://launchpad.net/bugs/2152113 +Bug-Debian: https://bugs.debian.org/1143170 +Origin: upstream, https://review.opendev.org/c/openstack/neutron/+/999133 +Last-Update: 2026-07-31 + +diff --git a/neutron/db/db_base_plugin_v2.py b/neutron/db/db_base_plugin_v2.py +index 9b559dc4e24..e1d2f425937 100644 +--- a/neutron/db/db_base_plugin_v2.py ++++ b/neutron/db/db_base_plugin_v2.py +@@ -1454,6 +1454,17 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon, + if not self._network_exists(context, network_id): + raise exc.NetworkNotFound(net_id=network_id) + ++ # Prevent cross-project subnet mutation: non-admin callers must own ++ # the network to onboard its subnets. Without this check a caller ++ # with visibility to a shared/RBAC network can reassign subnets ++ # owned by another project to their own subnetpool, potentially ++ # altering address-scope and L3 routing state for victim routers. ++ if not context.is_admin: ++ network = network_obj.Network.get_object( ++ context.elevated(), id=network_id) ++ if network.project_id != context.project_id: ++ raise exc.NotAuthorized() ++ + subnetpool = subnetpool_obj.SubnetPool.get_object(context, + id=subnetpool_id) + if not subnetpool: +diff --git a/neutron/tests/unit/extensions/test_subnet_onboard.py b/neutron/tests/unit/extensions/test_subnet_onboard.py +index f61491ba5de..8fa17d492d3 100644 +--- a/neutron/tests/unit/extensions/test_subnet_onboard.py ++++ b/neutron/tests/unit/extensions/test_subnet_onboard.py +@@ -18,6 +18,7 @@ import contextlib + + import netaddr + from neutron_lib.db import api as db_api ++from neutron_lib import context as n_context + from neutron_lib import exceptions as exc + from oslo_utils import uuidutils + +@@ -198,6 +199,37 @@ class SubnetOnboardTestsBase: + self._test_onboard_subnet_non_existing_network, + subnetpool['id'], self.cidr_to_onboard) + ++ def test_onboard_subnet_cross_project_not_authorized(self): ++ """Non-admin caller cannot onboard subnets from another project's net. ++ A project member must not be able to mutate subnets owned by a ++ different project via a shared network, even when the network is ++ RBAC-visible to the caller. ++ """ ++ _project_id = 'project1-' + _uuid() ++ _ctx = n_context.Context('user1', _project_id, is_admin=False) ++ ++ with self.subnetpool(self.ip_version, ++ prefixes=self.subnetpool_prefixes, ++ project_id=_project_id) as pool: ++ # Create a shared network owned by the default test project. ++ # Sharing is required so the user1 context can see (but not ++ # own) the network, reproducing the real-world condition. ++ with self.network(shared=True, as_admin=True) as shared_net: ++ network_id = shared_net['network']['id'] ++ with self.subnet(network=shared_net, ++ cidr=self.cidr_to_onboard, ++ ip_version=self.ip_version): ++ # The user1 can see the shared network but does ++ # not own it: the call must be rejected. ++ self.assertRaises( ++ exc.NotAuthorized, ++ self.driver.onboard_network_subnets, ++ _ctx, pool['id'], {'network_id': network_id}) ++ ++ # Admin call on the same pool/network must succeed. ++ self._test_onboard_network_subnets( ++ network_id, pool['id']) ++ + def _test_onboard_subnet_no_network_id(self, subnetpool_id, + cidr_to_onboard): + with self.subnet(cidr=cidr_to_onboard, +-- +2.43.0 + diff -Nru neutron-26.0.3/debian/patches/OSSN-0102-Fix_PF_GET_PUT_parent_floating_IP_validation.patch neutron-26.0.3/debian/patches/OSSN-0102-Fix_PF_GET_PUT_parent_floating_IP_validation.patch --- neutron-26.0.3/debian/patches/OSSN-0102-Fix_PF_GET_PUT_parent_floating_IP_validation.patch 1970-01-01 00:00:00.000000000 +0000 +++ neutron-26.0.3/debian/patches/OSSN-0102-Fix_PF_GET_PUT_parent_floating_IP_validation.patch 2026-08-03 07:47:00.000000000 +0000 @@ -0,0 +1,92 @@ +Author: Brian Haley +Date: Thu, 21 May 2026 17:50:47 -0400 +Description: OSSN-0102 Fix PF GET/PUT parent floating IP validation + Verify the port-forwarding floating IP id matches the + child floating IP id so we satisfy the policy requirements + for GET and PUT operations. +Bug: https://launchpad.net/bugs/2152109 +Bug-Debian: https://bugs.debian.org/1142937 +Change-Id: Ie68e57da6222965e79d015a5568190d8bc29b9e8 +Signed-off-by: Brian Haley +Assisted-by: Claude Sonnet 4.6 +Origin: upstream, https://review.opendev.org/c/openstack/neutron/+/991778 +Last-Update: 2026-07-28 + +diff --git a/neutron/services/portforwarding/pf_plugin.py b/neutron/services/portforwarding/pf_plugin.py +index 76d1b32..8957df1 100644 +--- a/neutron/services/portforwarding/pf_plugin.py ++++ b/neutron/services/portforwarding/pf_plugin.py +@@ -416,7 +416,7 @@ + with db_api.CONTEXT_WRITER.using(context): + fip_obj = self._get_fip_obj(context, floatingip_id) + pf_obj = pf.PortForwarding.get_object(context, id=id) +- if not pf_obj: ++ if not pf_obj or pf_obj.floatingip_id != floatingip_id: + raise pf_exc.PortForwardingNotFound(id=id) + original_pf_obj = copy.deepcopy(pf_obj) + ori_internal_port_id = pf_obj.internal_port_id +@@ -669,7 +669,7 @@ + fields=None): + self._get_fip_obj(context, floatingip_id) + obj = pf.PortForwarding.get_object(context, id=id) +- if not obj: ++ if not obj or obj.floatingip_id != floatingip_id: + raise pf_exc.PortForwardingNotFound(id=id) + return obj + +diff --git a/neutron/tests/unit/services/portforwarding/test_pf_plugin.py b/neutron/tests/unit/services/portforwarding/test_pf_plugin.py +index 9287397..8146abc 100644 +--- a/neutron/tests/unit/services/portforwarding/test_pf_plugin.py ++++ b/neutron/tests/unit/services/portforwarding/test_pf_plugin.py +@@ -90,10 +90,20 @@ + + @mock.patch.object(port_forwarding.PortForwarding, 'get_object') + def test_get_floatingip_port_forwarding(self, get_object_mock): ++ get_object_mock.return_value = mock.Mock(floatingip_id='test-fip-id') + self.pf_plugin.get_floatingip_port_forwarding( + self.ctxt, 'pf_id', 'test-fip-id', fields=None) + get_object_mock.assert_called_once_with(self.ctxt, id='pf_id') + ++ @mock.patch.object(port_forwarding.PortForwarding, 'get_object') ++ def test_negative_get_floatingip_port_forwarding_wrong_parent( ++ self, get_object_mock): ++ get_object_mock.return_value = mock.Mock(floatingip_id='other-fip-id') ++ self.assertRaises( ++ pf_exc.PortForwardingNotFound, ++ self.pf_plugin.get_floatingip_port_forwarding, ++ self.ctxt, 'pf_id', 'test-fip-id', fields=None) ++ + @mock.patch.object(port_forwarding.PortForwarding, 'get_object', + return_value=None) + def test_negative_get_floatingip_port_forwarding(self, get_object_mock): +@@ -186,6 +196,7 @@ + pf_obj.internal_ip_address = "10.0.0.1" + pf_obj.internal_port = 22 + pf_obj.external_port = 222 ++ pf_obj.floatingip_id = 'fip_id' + mock_pf_get_object.return_value = pf_obj + port_dict = {'id': 'ID', 'fixed_ips': [{"subnet_id": "test-subnet-id", + "ip_address": "10.0.0.1"}]} +@@ -293,6 +304,22 @@ + self.pf_plugin.update_floatingip_port_forwarding, + self.ctxt, 'pf_id', **pf_input) + ++ @mock.patch.object(port_forwarding.PortForwarding, 'get_object') ++ def test_negative_update_floatingip_port_forwarding_wrong_parent( ++ self, mock_pf_get_object): ++ pf_input = { ++ 'port_forwarding': ++ {'port_forwarding': { ++ 'internal_ip_address': '1.1.1.1', ++ 'floatingip_id': 'fip_id'}}, ++ 'floatingip_id': 'fip_id'} ++ mock_pf_get_object.return_value = mock.Mock( ++ floatingip_id='other-fip-id') ++ self.assertRaises( ++ pf_exc.PortForwardingNotFound, ++ self.pf_plugin.update_floatingip_port_forwarding, ++ self.ctxt, 'pf_id', **pf_input) ++ + @mock.patch.object(pf_plugin.PortForwardingPlugin, + '_check_port_has_binding_floating_ip') + @mock.patch.object(obj_base.NeutronDbObject, 'update_objects') diff -Nru neutron-26.0.3/debian/patches/OSSN-0102_Fix_cross-project_access_to_router_conntrack_helpers.patch neutron-26.0.3/debian/patches/OSSN-0102_Fix_cross-project_access_to_router_conntrack_helpers.patch --- neutron-26.0.3/debian/patches/OSSN-0102_Fix_cross-project_access_to_router_conntrack_helpers.patch 1970-01-01 00:00:00.000000000 +0000 +++ neutron-26.0.3/debian/patches/OSSN-0102_Fix_cross-project_access_to_router_conntrack_helpers.patch 2026-08-03 07:47:00.000000000 +0000 @@ -0,0 +1,203 @@ +Author: Rodolfo Alonso Hernandez +Date: Thu, 04 Jun 2026 13:36:05 +0200 +Description: OSSN-0102: Fix cross-project access to router conntrack helpers + Singleton conntrack helper API operations (GET/PUT/DELETE on + /routers/{router_id}/conntrack_helpers/{id}) authorized the request + against the URL ``router_id`` but loaded the helper by child ID only. + A project member with access to one router could therefore operate on + another tenant's helper by reusing its UUID in the path. + . + Validate that the loaded helper's ``router_id`` matches the URL + ``router_id`` before returning, updating, or deleting it, following + the same pattern used for floating IP port forwarding sub-resources. +Bug: https://launchpad.net/bugs/2150121 +Bug-Debian: https://bugs.debian.org/1142937 +Assisted-By: Claude Opus 4.6 +Signed-off-by: Rodolfo Alonso Hernandez +Change-Id: Icb4a7d2e8f1c35609d8a4b7e2f6c9d0a1b3e5f78 +Origin: upstream, https://review.opendev.org/c/openstack/neutron/+/990127 +Last-Update: 2026-07-28 + +diff --git a/neutron/services/conntrack_helper/plugin.py b/neutron/services/conntrack_helper/plugin.py +index d05d1b8..88b7121 100644 +--- a/neutron/services/conntrack_helper/plugin.py ++++ b/neutron/services/conntrack_helper/plugin.py +@@ -108,9 +108,9 @@ + if objs: + return (objs[0], param) + +- def _get_conntrack_helper(self, context, id): ++ def _get_conntrack_helper(self, context, id, router_id): + cth_obj = cth.ConntrackHelper.get_object(context, id=id) +- if not cth_obj: ++ if not cth_obj or cth_obj.router_id != router_id: + raise cth_exc.ConntrackHelperNotFound(id=id) + return cth_obj + +@@ -153,7 +153,7 @@ + conntrack_helper = conntrack_helper.get(apidef.RESOURCE_NAME) + try: + with db_api.CONTEXT_WRITER.using(context): +- cth_obj = self._get_conntrack_helper(context, id) ++ cth_obj = self._get_conntrack_helper(context, id, router_id) + cth_obj.update_fields(conntrack_helper, reset_changes=True) + self._check_conntrack_helper_constraints(cth_obj) + cth_obj.update() +@@ -171,7 +171,7 @@ + @db_base_plugin_common.make_result_with_fields + @db_base_plugin_common.convert_result_to_dict + def get_router_conntrack_helper(self, context, id, router_id, fields=None): +- return self._get_conntrack_helper(context, id) ++ return self._get_conntrack_helper(context, id, router_id) + + @db_base_plugin_common.make_result_with_fields + @db_base_plugin_common.convert_result_to_dict +@@ -185,7 +185,7 @@ + router_id=router_id, **filters) + + def delete_router_conntrack_helper(self, context, id, router_id): +- cth_obj = self._get_conntrack_helper(context, id) ++ cth_obj = self._get_conntrack_helper(context, id, router_id) + with db_api.CONTEXT_WRITER.using(context): + cth_obj.delete() + self.push_api.push(context, [cth_obj], rpc_events.DELETED) +diff --git a/neutron/tests/functional/services/conntrack_helper/test_conntrack_helper.py b/neutron/tests/functional/services/conntrack_helper/test_conntrack_helper.py +index 8e5345e..799e58b 100644 +--- a/neutron/tests/functional/services/conntrack_helper/test_conntrack_helper.py ++++ b/neutron/tests/functional/services/conntrack_helper/test_conntrack_helper.py +@@ -127,3 +127,30 @@ + self.assertRaises(cth_exc.ConntrackHelperNotFound, + self.cth_plugin.delete_router_conntrack_helper, + self.context, INVALID_ID, self.router['id']) ++ ++ def test_negative_singleton_operations_wrong_router(self): ++ res = self.cth_plugin.create_router_conntrack_helper( ++ self.context, self.router['id'], self.conntack_helper) ++ other_router = self._create_router(distributed=True) ++ new_conntack_helper = { ++ apidef.RESOURCE_NAME: ++ {apidef.PROTOCOL: 'udp', ++ apidef.PORT: 6969, ++ apidef.HELPER: 'tftp'} ++ } ++ self.assertRaises( ++ cth_exc.ConntrackHelperNotFound, ++ self.cth_plugin.get_router_conntrack_helper, ++ self.context, res['id'], other_router['id']) ++ self.assertRaises( ++ cth_exc.ConntrackHelperNotFound, ++ self.cth_plugin.update_router_conntrack_helper, ++ self.context, res['id'], other_router['id'], ++ new_conntack_helper) ++ self.assertRaises( ++ cth_exc.ConntrackHelperNotFound, ++ self.cth_plugin.delete_router_conntrack_helper, ++ self.context, res['id'], other_router['id']) ++ helper = self.cth_plugin.get_router_conntrack_helper( ++ self.context, res['id'], self.router['id']) ++ self.assertEqual(res['id'], helper['id']) +diff --git a/neutron/tests/unit/services/conntrack_helper/test_plugin.py b/neutron/tests/unit/services/conntrack_helper/test_plugin.py +index 99e0898..eac7283 100644 +--- a/neutron/tests/unit/services/conntrack_helper/test_plugin.py ++++ b/neutron/tests/unit/services/conntrack_helper/test_plugin.py +@@ -193,9 +193,10 @@ + cth_obj = mock.Mock() + cth_obj.helper = 'tftp' + cth_obj.protocol = 'udp' ++ cth_obj.router_id = 'fake-router' + mock_cth_get_object.return_value = cth_obj + self.cth_plugin.update_router_conntrack_helper( +- self.ctxt, 'cth_id', mock.ANY, **cth_input) ++ self.ctxt, 'cth_id', 'fake-router', **cth_input) + mock_cth_get_object.assert_called_once_with(self.ctxt, id='cth_id') + self.assertTrue(cth_obj.update_fields) + self.assertTrue(cth_obj.update) +@@ -216,12 +217,31 @@ + self.assertRaises( + cth_exc.ConntrackHelperNotFound, + self.cth_plugin.update_router_conntrack_helper, +- self.ctxt, 'cth_id', mock.ANY, **cth_input) ++ self.ctxt, 'cth_id', 'fake-router', **cth_input) ++ ++ @mock.patch.object(conntrack_helper.ConntrackHelper, 'get_object') ++ def test_negative_update_conntrack_helper_wrong_router( ++ self, mock_cth_get_object): ++ cth_input = { ++ 'conntrack_helper': { ++ 'conntrack_helper': { ++ 'protocol': 'udp', ++ 'port': 69, ++ 'helper': 'tftp'} ++ } ++ } ++ mock_cth_get_object.return_value = mock.Mock( ++ router_id='correct-router') ++ self.assertRaises( ++ cth_exc.ConntrackHelperNotFound, ++ self.cth_plugin.update_router_conntrack_helper, ++ self.ctxt, 'cth_id', 'other-router', **cth_input) + + @mock.patch.object(conntrack_helper.ConntrackHelper, 'get_object') + def test_get_conntrack_helper(self, get_object_mock): ++ get_object_mock.return_value = mock.Mock(router_id='fake-router') + self.cth_plugin.get_router_conntrack_helper( +- self.ctxt, 'cth_id', mock.ANY, fields=None) ++ self.ctxt, 'cth_id', 'fake-router', fields=None) + get_object_mock.assert_called_once_with(self.ctxt, id='cth_id') + + @mock.patch.object(conntrack_helper.ConntrackHelper, 'get_object') +@@ -230,7 +250,16 @@ + self.assertRaises( + cth_exc.ConntrackHelperNotFound, + self.cth_plugin.get_router_conntrack_helper, +- self.ctxt, 'cth_id', mock.ANY, fields=None) ++ self.ctxt, 'cth_id', 'fake-router', fields=None) ++ ++ @mock.patch.object(conntrack_helper.ConntrackHelper, 'get_object') ++ def test_negative_get_conntrack_helper_wrong_router(self, ++ get_object_mock): ++ get_object_mock.return_value = mock.Mock(router_id='victim-router') ++ self.assertRaises( ++ cth_exc.ConntrackHelperNotFound, ++ self.cth_plugin.get_router_conntrack_helper, ++ self.ctxt, 'cth_id', 'other-router', fields=None) + + @mock.patch.object(conntrack_helper.ConntrackHelper, 'get_objects') + def test_get_conntrack_helpers(self, get_objects_mock): +@@ -248,7 +277,7 @@ + helper='tftp') + get_object_mock.return_value = cth_obj + self.cth_plugin.delete_router_conntrack_helper(self.ctxt, 'cth_id', +- mock.ANY) ++ 'fake-router') + cth_obj.delete.assert_called() + mock_rpc_push.assert_called_once_with( + self.ctxt, mock.ANY, rpc_events.DELETED) +@@ -258,4 +287,12 @@ + get_object_mock.return_value = None + self.assertRaises(cth_exc.ConntrackHelperNotFound, + self.cth_plugin.delete_router_conntrack_helper, +- self.ctxt, 'cth_id', mock.ANY) ++ self.ctxt, 'cth_id', 'fake-router') ++ ++ @mock.patch.object(conntrack_helper.ConntrackHelper, 'get_object') ++ def test_negative_delete_conntrack_helper_wrong_router( ++ self, get_object_mock): ++ get_object_mock.return_value = mock.Mock(router_id='correct-router') ++ self.assertRaises(cth_exc.ConntrackHelperNotFound, ++ self.cth_plugin.delete_router_conntrack_helper, ++ self.ctxt, 'cth_id', 'other-router') +diff --git a/releasenotes/notes/fix-router-conntrack-helper-cross-project-access-ba1425c06abc45d5.yaml b/releasenotes/notes/fix-router-conntrack-helper-cross-project-access-ba1425c06abc45d5.yaml +new file mode 100644 +index 0000000..df142ec +--- /dev/null ++++ b/releasenotes/notes/fix-router-conntrack-helper-cross-project-access-ba1425c06abc45d5.yaml +@@ -0,0 +1,8 @@ ++--- ++security: ++ - | ++ Fixed an issue in the router conntrack helper API where a project ++ member authorized on one router could read, update, or delete another ++ project's conntrack helper by supplying an incorrect helper ID under their ++ own router ID in the request URL. Singleton operations now verify that ++ the conntrack helper belongs to the router specified in the URL. diff -Nru neutron-26.0.3/debian/patches/series neutron-26.0.3/debian/patches/series --- neutron-26.0.3/debian/patches/series 2026-06-05 09:00:14.000000000 +0000 +++ neutron-26.0.3/debian/patches/series 2026-08-03 07:47:00.000000000 +0000 @@ -1,3 +1,6 @@ fix-path-of-healthcheck_disable.patch OSSA-2026-016_Fix_plural_policy_names_in_tagging_controller_and_floatingip_policy.patch OSSA-2026-021_Fix_port_RBAC_policies_to_require_network_ownership.patch +CVE-2026-55707-stable-2025.1.patch +OSSN-0102-Fix_PF_GET_PUT_parent_floating_IP_validation.patch +OSSN-0102_Fix_cross-project_access_to_router_conntrack_helpers.patch