Bug#1144913: trixie-pu: package glance/2:30.0.0-3+deb13u2

Thomas Goirand <[email protected]>
Newsgroups gmane.linux.debian.devel.release
Message-ID <178722800379.69198.4849883365019281365.reportbug__48308.8070921198$1787228145$gmane$org@zbuz.infomaniak.ch>
Package: release.debian.org
Severity: normal
Tags: trixie
X-Debbugs-Cc: [email protected]
Control: affects -1 + src:glance
User: [email protected]
Usertags: pu

Hi,

[ Reason ]
I'd like to upload Glance u3 to Trixie to address:
https://wiki.openstack.org/wiki/OSSN/OSSN-0105
aka:
https://bugs.debian.org/1144212

The security team asked me to go through p-u.

[ Impact ]
As per upstream announce, Glance is otherwise affected by
"legacy Tasks import bypasses image import URI"

[ Tests ]
The patch includes new tests, ran at package build time.

[ Risks ]
Not much, the patch is small (if excluding tests).

[ Checklist ]
  [x] *all* changes are documented in the d/changelog
  [x] I reviewed all changes and I approve them
  [x] attach debdiff against the package in (old)stable
  [x] the issue is verified as fixed in unstable

Please allow me to upload glance/2:30.0.0-3+deb13u3 to
trixie-pu.

Cheers,

Thomas Goirand (zigo)
glance_30.0.0-3+deb13u3.debdiff (text/plain, 21.2 KB)
diff -Nru glance-30.0.0/debian/changelog glance-30.0.0/debian/changelog
--- glance-30.0.0/debian/changelog	2026-04-27 08:23:24.000000000 +0200
+++ glance-30.0.0/debian/changelog	2026-08-20 14:02:16.000000000 +0200
@@ -1,3 +1,11 @@
+glance (2:30.0.0-3+deb13u3) trixie; urgency=medium
+
+  * OSSN-0105: legacy Tasks import bypasses image import URI filtering.
+    Applied upstream patch: "Apply import URI filtering to legacy import
+    tasks" (Closes: #1144212).
+
+ -- Thomas Goirand <[email protected]>  Thu, 20 Aug 2026 14:02:16 +0200
+
 glance (2:30.0.0-3+deb13u2) trixie; urgency=medium
 
   * Add No_DNS_resolution_in_test.patch, otherwise Glance cannot be build in
diff -Nru glance-30.0.0/debian/patches/OSSN-0105_Apply_import_URI_filtering_to_legacy_import_tasks.patch glance-30.0.0/debian/patches/OSSN-0105_Apply_import_URI_filtering_to_legacy_import_tasks.patch
--- glance-30.0.0/debian/patches/OSSN-0105_Apply_import_URI_filtering_to_legacy_import_tasks.patch	1970-01-01 01:00:00.000000000 +0100
+++ glance-30.0.0/debian/patches/OSSN-0105_Apply_import_URI_filtering_to_legacy_import_tasks.patch	2026-08-20 14:02:16.000000000 +0200
@@ -0,0 +1,436 @@
+Description: Apply import URI filtering to legacy import tasks
+ Legacy type=import tasks only ran validate_location_uri, so
+ import_from could skip the same host/port/path checks used for
+ modern image import. Route import_from through a small helper that
+ reuses validate_location_uri then validate_import_uri, and
+ reject bad URIs early when creating import tasks.
+Author: Abhishek Kekane <[email protected]>
+Date: Tue, 12 May 2026 06:53:53 +0000
+Depends-On: https://review.opendev.org/c/openstack/glance/+/996220
+Bug: https://launchpad.net/bugs/2152110
+Bug-Debian: https://bugs.debian.org/1144212
+Assisted-By: Cursor (claude-4.5-sonnet) for tests
+Change-Id: I6fbb8a91a49e3df6fa9bb185f7fcd8b1816ad74e
+Signed-off-by: Abhishek Kekane <[email protected]>
+Origin: upstream, https://review.opendev.org/c/openstack/glance/+/1000061
+Last-Update: 2026-08-20
+
+diff --git a/glance/api/v2/tasks.py b/glance/api/v2/tasks.py
+index ad12586..61cd0a9 100644
+--- a/glance/api/v2/tasks.py
++++ b/glance/api/v2/tasks.py
+@@ -16,6 +16,7 @@
+ 
+ import copy
+ import http.client as http
++import urllib.error
+ import urllib.parse as urlparse
+ 
+ import debtcollector
+@@ -31,6 +32,7 @@
+ from glance.api import policy
+ from glance.api.v2 import policy as api_policy
+ from glance.common import exception
++from glance.common.scripts import utils as script_utils
+ from glance.common import timeutils
+ from glance.common import wsgi
+ import glance.db
+@@ -74,6 +76,10 @@
+         executor_factory = self.gateway.get_task_executor_factory(ctxt)
+         task_repo = self.gateway.get_task_repo(ctxt)
+         try:
++            if task.get('type') == 'import':
++                task_input = task.get('input') or {}
++                script_utils.validate_legacy_import_from_uri(
++                    task_input.get('import_from'))
+             new_task = task_factory.new_task(
+                 task_type=task['type'],
+                 owner=ctxt.owner,
+@@ -90,6 +96,12 @@
+                    % {'reason': encodeutils.exception_to_unicode(e)})
+             LOG.warning(msg)
+             raise webob.exc.HTTPForbidden(explanation=e.msg)
++        except exception.BadStoreUri as e:
++            raise webob.exc.HTTPBadRequest(explanation=e.msg)
++        except exception.Invalid as e:
++            raise webob.exc.HTTPBadRequest(explanation=e.msg)
++        except urllib.error.URLError as e:
++            raise webob.exc.HTTPBadRequest(explanation=str(e.reason))
+         return new_task
+ 
+     @debtcollector.removals.remove(message=_DEPRECATION_MESSAGE)
+diff --git a/glance/async_/taskflow_executor.py b/glance/async_/taskflow_executor.py
+index b648639..87a3568 100644
+--- a/glance/async_/taskflow_executor.py
++++ b/glance/async_/taskflow_executor.py
+@@ -125,7 +125,7 @@
+                 kwds['admin_repo'] = self.admin_repo
+ 
+             if task.type == "import":
+-                uri = script_utils.validate_location_uri(
++                uri = script_utils.validate_legacy_import_from_uri(
+                     task_input.get('import_from'))
+                 kwds['uri'] = uri
+             if task.type == 'api_image_import':
+diff --git a/glance/common/scripts/image_import/main.py b/glance/common/scripts/image_import/main.py
+index cde2bf3..4cfdb64 100644
+--- a/glance/common/scripts/image_import/main.py
++++ b/glance/common/scripts/image_import/main.py
+@@ -53,7 +53,8 @@
+     try:
+         task_input = script_utils.unpack_task_input(task)
+ 
+-        uri = script_utils.validate_location_uri(task_input.get('import_from'))
++        uri = script_utils.validate_legacy_import_from_uri(
++            task_input.get('import_from'))
+         image_id = import_image(image_repo, image_factory, task_input, t_id,
+                                 uri)
+ 
+diff --git a/glance/common/scripts/utils.py b/glance/common/scripts/utils.py
+index aa6354d..74b088c 100644
+--- a/glance/common/scripts/utils.py
++++ b/glance/common/scripts/utils.py
+@@ -18,6 +18,7 @@
+     'unpack_task_input',
+     'set_base_image_properties',
+     'validate_location_uri',
++    'validate_legacy_import_from_uri',
+     'get_image_data_iter',
+     'SafeRedirectHandler',
+ ]
+@@ -129,6 +130,16 @@
+         raise urllib.error.URLError(msg)
+ 
+ 
++def validate_legacy_import_from_uri(location):
++    """Validate legacy ``import_from`` URI (scheme + import filter)."""
++    uri = validate_location_uri(location)
++    if not common_utils.validate_import_uri(uri):
++        msg = (_("URI for legacy import task does not pass filtering: %s") %
++               uri)
++        raise exception.Invalid(msg)
++    return uri
++
++
+ class SafeRedirectHandler(urllib.request.HTTPRedirectHandler):
+     """HTTP redirect handler that validates redirect destinations."""
+     def redirect_request(self, req, fp, code, msg, headers, newurl):
+diff --git a/glance/tests/unit/async_/flows/plugins/test_image_conversion.py b/glance/tests/unit/async_/flows/plugins/test_image_conversion.py
+index ada82b5..bd047ca 100644
+--- a/glance/tests/unit/async_/flows/plugins/test_image_conversion.py
++++ b/glance/tests/unit/async_/flows/plugins/test_image_conversion.py
+@@ -70,7 +70,7 @@
+                                                 container_format='bare')
+ 
+         task_input = {
+-            "import_from": "http://cloud.foo/image.raw",
++            "import_from": "http://198.51.100.1/image.raw",
+             "import_from_format": "raw",
+             "image_properties": {'disk_format': 'raw',
+                                  'container_format': 'bare'}
+diff --git a/glance/tests/unit/async_/flows/plugins/test_inject_image_metadata.py b/glance/tests/unit/async_/flows/plugins/test_inject_image_metadata.py
+index 92a18a5..c62025b 100644
+--- a/glance/tests/unit/async_/flows/plugins/test_inject_image_metadata.py
++++ b/glance/tests/unit/async_/flows/plugins/test_inject_image_metadata.py
+@@ -65,7 +65,7 @@
+         self.img_repo.get.return_value = self.image
+ 
+         task_input = {
+-            "import_from": "http://cloud.foo/image.qcow2",
++            "import_from": "http://198.51.100.1/image.qcow2",
+             "import_from_format": "qcow2",
+             "image_properties": {'disk_format': 'qcow2',
+                                  'container_format': 'bare'}
+diff --git a/glance/tests/unit/async_/flows/test_convert.py b/glance/tests/unit/async_/flows/test_convert.py
+index aa65ab7..4393021 100644
+--- a/glance/tests/unit/async_/flows/test_convert.py
++++ b/glance/tests/unit/async_/flows/test_convert.py
+@@ -56,7 +56,7 @@
+                                                 container_format='bare')
+ 
+         task_input = {
+-            "import_from": "http://cloud.foo/image.raw",
++            "import_from": "http://198.51.100.1/image.raw",
+             "import_from_format": "raw",
+             "image_properties": {'disk_format': 'qcow2',
+                                  'container_format': 'bare'}
+diff --git a/glance/tests/unit/async_/flows/test_import.py b/glance/tests/unit/async_/flows/test_import.py
+index 5774e3e..621ccdd 100644
+--- a/glance/tests/unit/async_/flows/test_import.py
++++ b/glance/tests/unit/async_/flows/test_import.py
+@@ -77,7 +77,7 @@
+                                                 container_format='bare')
+ 
+         task_input = {
+-            "import_from": "http://cloud.foo/image.qcow2",
++            "import_from": "http://198.51.100.1/image.qcow2",
+             "import_from_format": "qcow2",
+             "image_properties": {'disk_format': 'qcow2',
+                                  'container_format': 'bare'}
+diff --git a/glance/tests/unit/async_/flows/test_introspect.py b/glance/tests/unit/async_/flows/test_introspect.py
+index f7c6542..b2f68dd 100644
+--- a/glance/tests/unit/async_/flows/test_introspect.py
++++ b/glance/tests/unit/async_/flows/test_introspect.py
+@@ -37,7 +37,7 @@
+         super(TestImportTask, self).setUp()
+         self.task_factory = domain.TaskFactory()
+         task_input = {
+-            "import_from": "http://cloud.foo/image.qcow2",
++            "import_from": "http://198.51.100.1/image.qcow2",
+             "import_from_format": "qcow2",
+             "image_properties": mock.sentinel.image_properties
+         }
+diff --git a/glance/tests/unit/async_/test_async.py b/glance/tests/unit/async_/test_async.py
+index 96c6e6e..20ad9a9 100644
+--- a/glance/tests/unit/async_/test_async.py
++++ b/glance/tests/unit/async_/test_async.py
+@@ -131,7 +131,7 @@
+         import_req = {
+             'method': {
+                 'name': 'web-download',
+-                'uri': 'http://cloud.foo/image.qcow2'
++                'uri': 'http://198.51.100.1/image.qcow2'
+             }
+         }
+ 
+diff --git a/glance/tests/unit/async_/test_taskflow_executor.py b/glance/tests/unit/async_/test_taskflow_executor.py
+index 397fac9..ce10f79 100644
+--- a/glance/tests/unit/async_/test_taskflow_executor.py
++++ b/glance/tests/unit/async_/test_taskflow_executor.py
+@@ -55,7 +55,7 @@
+         self.image_factory = mock.Mock()
+ 
+         task_input = {
+-            "import_from": "http://cloud.foo/image.qcow2",
++            "import_from": "http://198.51.100.1/image.qcow2",
+             "import_from_format": "qcow2",
+             "image_properties": {'disk_format': 'qcow2',
+                                  'container_format': 'bare'}
+@@ -79,6 +79,12 @@
+             self.image_repo,
+             self.image_factory)
+ 
++        self._addrinfo_patcher = mock.patch(
++            'glance.common.utils.socket.getaddrinfo',
++            return_value=[(None, None, None, None, ('203.0.113.1', 80))])
++        self._addrinfo_patcher.start()
++        self.addCleanup(self._addrinfo_patcher.stop)
++
+     def test_fetch_an_executor_parallel(self):
+         self.config(engine_mode='parallel', group='taskflow_executor')
+         pool = self.executor._fetch_an_executor()
+@@ -142,7 +148,7 @@
+                          'image_factory': self.image_factory,
+                          'backend': None,
+                          'admin_repo': admin_repo,
+-                         'uri': 'http://cloud.foo/image.qcow2'})
++                         'uri': 'http://198.51.100.1/image.qcow2'})
+ 
+     @mock.patch('stevedore.driver.DriverManager')
+     @mock.patch.object(taskflow_executor, 'LOG')
+diff --git a/glance/tests/unit/common/scripts/test_scripts_utils.py b/glance/tests/unit/common/scripts/test_scripts_utils.py
+index 38c9a40..914e81c 100644
+--- a/glance/tests/unit/common/scripts/test_scripts_utils.py
++++ b/glance/tests/unit/common/scripts/test_scripts_utils.py
+@@ -153,6 +153,26 @@
+         self.assertRaises(urllib.error.URLError,
+                           script_utils.validate_location_uri, location)
+ 
++    @mock.patch('glance.common.utils.socket.getaddrinfo')
++    def test_validate_legacy_import_from_uri_ok(self, mock_getaddrinfo):
++        mock_getaddrinfo.return_value = [
++            (None, None, None, None, ('203.0.113.1', 80))]
++        uri = 'http://example.com/img'
++        self.assertEqual(
++            uri, script_utils.validate_legacy_import_from_uri(uri))
++
++    @mock.patch('glance.common.utils.socket.getaddrinfo')
++    def test_validate_legacy_import_from_uri_filtered(self, mock_getaddrinfo):
++        mock_getaddrinfo.return_value = [
++            (None, None, None, None, ('127.0.0.1', 80))]
++        self.config(disallowed_hosts=['127.0.0.1'],
++                    group='import_filtering_opts')
++        self.config(allowed_ports=[80],
++                    group='import_filtering_opts')
++        self.assertRaises(exception.Invalid,
++                          script_utils.validate_legacy_import_from_uri,
++                          'http://127.0.0.1:80/x')
++
+ 
+ class TestCallbackIterator(test_utils.BaseTestCase):
+     def test_iterator_iterates(self):
+diff --git a/glance/tests/unit/v2/test_tasks_resource.py b/glance/tests/unit/v2/test_tasks_resource.py
+index 9961b52..4a4c10a 100644
+--- a/glance/tests/unit/v2/test_tasks_resource.py
++++ b/glance/tests/unit/v2/test_tasks_resource.py
+@@ -297,18 +297,22 @@
+         self.assertRaises(webob.exc.HTTPNotFound,
+                           self.controller.get, request, UUID4)
+ 
++    @mock.patch('glance.common.utils.socket.getaddrinfo')
+     @mock.patch('glance.api.common.get_thread_pool')
+     @mock.patch.object(glance.gateway.Gateway, 'get_task_factory')
+     @mock.patch.object(glance.gateway.Gateway, 'get_task_executor_factory')
+     @mock.patch.object(glance.gateway.Gateway, 'get_task_repo')
+     def test_create(self, mock_get_task_repo, mock_get_task_executor_factory,
+-                    mock_get_task_factory, mock_get_thread_pool):
++                    mock_get_task_factory, mock_get_thread_pool,
++                    mock_getaddrinfo):
++        mock_getaddrinfo.return_value = [
++            (None, None, None, None, ('203.0.113.1', 80))]
+         # setup
+         request = unit_test_utils.get_fake_request()
+         task = {
+             "type": "import",
+             "input": {
+-                "import_from": "swift://cloud.foo/myaccount/mycontainer/path",
++                "import_from": "http://example.com/myaccount/mycontainer/path",
+                 "import_from_format": "qcow2",
+                 "image_properties": {}
+             }
+@@ -345,8 +349,8 @@
+             get_task_executor_factory.new_task_executor.return_value)
+ 
+     @mock.patch('glance.common.scripts.utils.get_image_data_iter')
+-    @mock.patch('glance.common.scripts.utils.validate_location_uri')
+-    def test_create_with_live_time(self, mock_validate_location_uri,
++    @mock.patch('glance.common.scripts.utils.validate_legacy_import_from_uri')
++    def test_create_with_live_time(self, mock_validate_legacy_import_from_uri,
+                                    mock_get_image_data_iter):
+         self.skipTest("Something wrong, this test touches registry")
+         request = unit_test_utils.get_fake_request()
+@@ -387,10 +391,6 @@
+             "file:///path",
+             "cinder://volume-id"
+         ]
+-        executor_factory = self.gateway.get_task_executor_factory(
+-            request.context)
+-        task_repo = self.gateway.get_task_repo(request.context)
+-
+         for import_from in wrong_import_from:
+             task = {
+                 "type": "import",
+@@ -404,12 +404,8 @@
+                     }
+                 }
+             }
+-            new_task = self.controller.create(request, task=task)
+-            task_executor = executor_factory.new_task_executor(request.context)
+-            task_executor.begin_processing(new_task.task_id)
+-            final_task = task_repo.get(new_task.task_id)
+-
+-            self.assertEqual('failure', final_task.status)
++            exc = self.assertRaises(webob.exc.HTTPBadRequest,
++                                    self.controller.create, request, task=task)
+             if import_from.startswith("file:///"):
+                 msg = ("File based imports are not allowed. Please use a "
+                        "non-local source of image data.")
+@@ -418,7 +414,33 @@
+                 msg = ("The given uri is not valid. Please specify a "
+                        "valid uri from the following list of supported uri "
+                        "%(supported)s") % {'supported': supported}
+-            self.assertEqual(msg, final_task.message)
++            self.assertEqual(msg, exc.explanation)
++
++    @mock.patch('glance.common.utils.socket.getaddrinfo')
++    def test_create_legacy_import_rejects_filtered_http_uri(
++            self, mock_getaddrinfo):
++        mock_getaddrinfo.return_value = [
++            (None, None, None, None, ('127.0.0.1', 80))]
++        self.config(disallowed_hosts=['127.0.0.1'],
++                    group='import_filtering_opts')
++        self.config(allowed_ports=[80],
++                    group='import_filtering_opts')
++        request = unit_test_utils.get_fake_request()
++        task = {
++            "type": "import",
++            "input": {
++                "import_from": "http://127.0.0.1:80/internal",
++                "import_from_format": "qcow2",
++                "image_properties": {
++                    "disk_format": "qcow2",
++                    "container_format": "bare",
++                    "name": "test-task"
++                }
++            }
++        }
++        exc = self.assertRaises(webob.exc.HTTPBadRequest,
++                                self.controller.create, request, task=task)
++        self.assertIn('does not pass filtering', exc.explanation)
+ 
+     def test_create_with_properties_missed(self):
+         request = unit_test_utils.get_fake_request()
+@@ -426,14 +448,17 @@
+             request.context)
+         task_repo = self.gateway.get_task_repo(request.context)
+ 
+-        task = {
+-            "type": "import",
+-            "input": {
+-                "import_from": "swift://cloud.foo/myaccount/mycontainer/path",
+-                "import_from_format": "qcow2",
++        with mock.patch('glance.common.utils.socket.getaddrinfo',
++                        return_value=[(None, None, None, None,
++                                       ('203.0.113.1', 80))]):
++            task = {
++                "type": "import",
++                "input": {
++                    "import_from": "http://example.com/myaccount/path",
++                    "import_from_format": "qcow2",
++                }
+             }
+-        }
+-        new_task = self.controller.create(request, task=task)
++            new_task = self.controller.create(request, task=task)
+         task_executor = executor_factory.new_task_executor(request.context)
+         task_executor.begin_processing(new_task.task_id)
+         final_task = task_repo.get(new_task.task_id)
+@@ -442,8 +467,12 @@
+         msg = "Input does not contain 'image_properties' field"
+         self.assertEqual(msg, final_task.message)
+ 
++    @mock.patch('glance.common.utils.socket.getaddrinfo')
+     @mock.patch.object(glance.gateway.Gateway, 'get_task_factory')
+-    def test_notifications_on_create(self, mock_get_task_factory):
++    def test_notifications_on_create(self, mock_get_task_factory,
++                                     mock_getaddrinfo):
++        mock_getaddrinfo.return_value = [
++            (None, None, None, None, ('203.0.113.1', 80))]
+         request = unit_test_utils.get_fake_request()
+ 
+         new_task = mock.MagicMock(type='import')
+diff --git a/releasenotes/notes/bug-2152110-8c4e91a2b3d0567f.yaml b/releasenotes/notes/bug-2152110-8c4e91a2b3d0567f.yaml
+new file mode 100644
+index 0000000..5434db6
+--- /dev/null
++++ b/releasenotes/notes/bug-2152110-8c4e91a2b3d0567f.yaml
+@@ -0,0 +1,29 @@
++---
++security:
++  - |
++    Fixed insufficient validation of ``import_from`` URIs for legacy
++    ``type=import`` tasks created through the deprecated Task API
++    (``POST /v2/tasks``). Those requests were not held to the same URI
++    rules as modern image import, so Server-Side Request Forgery (SSRF)
++    protections could be weaker on that path.
++
++    Impact:
++
++    - Severity: High (SSRF-style exposure through attacker-controlled
++      ``import_from`` URIs on the legacy import task path)
++    - Affected versions: All versions prior to this fix that still
++      expose the Task API for ``type=import`` tasks
++
++    Mitigation without upgrading (especially for unmaintained
++    releases):
++
++    The Task API has been deprecated for a long time; the simplest
++    mitigation is to block it in your deployment, for example set
++    ``tasks_api_access`` to a check that never matches (such as ``!``
++    in a policy YAML file) so ``/v2/tasks`` is denied for all callers.
++
++fixes:
++  - |
++    `Bug 2152110 <https://bugs.launchpad.net/glance/+bug/2152110>`_:
++    Align legacy ``type=import`` task URI checks with modern image
++    import so ``import_from`` cannot bypass the same restrictions.
diff -Nru glance-30.0.0/debian/patches/series glance-30.0.0/debian/patches/series
--- glance-30.0.0/debian/patches/series	2026-04-27 08:23:24.000000000 +0200
+++ glance-30.0.0/debian/patches/series	2026-08-20 14:02:16.000000000 +0200
@@ -2,3 +2,4 @@
 missing-files.patch
 CVE-2026-34881_OSSA-2026-004_Fix_SSRF_vulnerabilities_in_image_import_API.patch
 No_DNS_resolution_in_test.patch
+OSSN-0105_Apply_import_URI_filtering_to_legacy_import_tasks.patch
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.