Your message dated Sat, 11 Jul 2026 10:42:30 +0000
with message-id <[email protected]>
and subject line Released in 12.15
has caused the Debian Bug report #1135910,
regarding bookworm-pu: package ironic/1:21.1.0-3 (CVE-2026-42510, 
CVE-2025-44021, CVE-2026-42997)
to be marked as done.

This means that you claim that the problem has been dealt with.
If this is not the case it is now your responsibility to reopen the
Bug report if necessary, and/or fix the problem forthwith.

(NB: If you are a system administrator and have no idea what this
message is talking about, this may indicate a serious mail system
misconfiguration somewhere. Please contact [email protected]
immediately.)


-- 
1135910: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1135910
Debian Bug Tracking System
Contact [email protected] with problems
--- Begin Message ---
Package: release.debian.org
Severity: normal
Tags: bookworm
X-Debbugs-Cc: [email protected]
Control: affects -1 + src:ironic
User: [email protected]
Usertags: pu

Hi,

[ Reason ]
I'd like to update Ironic in Bookworm to fix the 3 CVE in the
subject.

[ Impact ]
Serious security issue in public accessible clouds.

[ Tests ]
Upstream has a lot of functional tests, though I haven't run
them, the package contains unit tests that are ran at build time.

[ Risks ]
The proposed fix only contains bugfix, so it should be ok.

[ 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

[ Changes ]
Adds CVE 3 patches, plus a build dependency.

Please allow me to upload ironic/21.1.0-3+deb12u1 to bookworm.

Cheers,

Thomas Goirand (zigo)
diff -Nru ironic-21.1.0/debian/changelog ironic-21.1.0/debian/changelog
--- ironic-21.1.0/debian/changelog      2023-04-14 13:48:42.000000000 +0200
+++ ironic-21.1.0/debian/changelog      2026-04-30 10:41:21.000000000 +0200
@@ -1,3 +1,19 @@
+ironic (1:21.1.0-3+deb12u1) bookworm; urgency=medium
+
+  * CVE-2026-42510 / OSSA-2026-008: Command Injection in Ironic IPMI Console
+    Implementations. Applied upstream patch: "Shell-quote console command
+    passed to socat" (Closes: #1135255).
+  * CVE-2025-44021: Ironic fails to restrict paths used for file:// image URLs.
+    Add upstream patch: OSSA-2025-001_Disallow+unsafe_image_file_paths.patch.
+    (Closes: #1104964).
+  * Add qemu-utils as build-depends because of tests from CVE-2025-44021 fix.
+  * CVE-2026-42997 / OSSA-2026-010: Credential Forwarding to Arbitrary
+    Endpoints via Ironic’s idrac Configuration molds Feature. Add upstream
+    patch validate_molds_url_against_swift_in_keystone_catalog.patch.
+    (Closes: #1135898).
+
+ -- Thomas Goirand <[email protected]>  Thu, 30 Apr 2026 10:41:21 +0200
+
 ironic (1:21.1.0-3) unstable; urgency=medium
 
   * Build-depends on openstack-pkg-tools (>= 123~).
diff -Nru ironic-21.1.0/debian/control ironic-21.1.0/debian/control
--- ironic-21.1.0/debian/control        2023-04-14 13:48:42.000000000 +0200
+++ ironic-21.1.0/debian/control        2026-04-30 10:41:21.000000000 +0200
@@ -87,6 +87,7 @@
  python3-tz,
  python3-webob,
  python3-webtest,
+ qemu-utils,
 Build-Conflicts:
  python-pysqlite2,
 Standards-Version: 4.4.1
diff -Nru 
ironic-21.1.0/debian/patches/CVE-2025-44021_OSSA-2025-001_Disallow_unsafe_image_file_paths.patch
 
ironic-21.1.0/debian/patches/CVE-2025-44021_OSSA-2025-001_Disallow_unsafe_image_file_paths.patch
--- 
ironic-21.1.0/debian/patches/CVE-2025-44021_OSSA-2025-001_Disallow_unsafe_image_file_paths.patch
    1970-01-01 01:00:00.000000000 +0100
+++ 
ironic-21.1.0/debian/patches/CVE-2025-44021_OSSA-2025-001_Disallow_unsafe_image_file_paths.patch
    2026-04-30 10:41:21.000000000 +0200
@@ -0,0 +1,393 @@
+Description: CVE-2025-44021 / OSSA-2025-001: Disallow unsafe image file:// 
paths
+ Before this change, Ironic did not filter file:// paths when used as an
+ image source except to ensure they were a file (and not, e.g. a
+ character device). This is problematic from a security perspective
+ because you could end up with config files from well-known paths being
+ written to disk on a node.
+ .
+ The allowlist default list is huge, but it includes all known usages of
+ file:// URLs across Bifrost, Ironic, Metal3, and OpenShift in both CI
+ and default configuration.
+ .
+ For the backportable version of this patch for stable branches, we have
+ omitted the unconditional block of system paths in order to permit
+ operators using those branches to fully disable the new security
+ functionality.
+Author: Jay Faulkner <[email protected]>
+Date: Mon, 21 Apr 2025 15:57:37 -0700
+Generated-by: Jetbrains Junie
+Bug: https://launchpad.net/bugs/2107847
+Bug-Debian: https://bugs.debian.org/1104964
+Change-Id: I2fa995439ee500f9dd82ec8ccfa1a25ee8e1179c
+Origin: upstream, https://review.opendev.org/c/openstack/ironic/+/949176
+Last-Update: 2025-03-12
+
+Index: ironic/doc/source/install/standalone/enrollment.rst
+===================================================================
+--- ironic.orig/doc/source/install/standalone/enrollment.rst
++++ ironic/doc/source/install/standalone/enrollment.rst
+@@ -32,13 +32,20 @@ There are however some limitations for d
+    $ sha256sum image.qcow2
+    9f6c942ad81690a9926ff530629fb69a82db8b8ab267e2cbd59df417c1a28060  
image.qcow2
+ 
+-* :ref:`direct-deploy` started supporting ``file://`` images in the Victoria
+-  release cycle, before that only HTTP(s) had been supported.
++* If you're using :ref:`direct-deploy` with ``file://`` URLs, you have to
++  ensure the images meet all requirements:
++
++  * File images must be accessible to every conductor
++  * File images must be located in a path listed in
++    :oslo_config:option:`conductor.file_url_allowed_paths`
++  * File images must not be located in ``/dev``, ``/sys``, ``/proc``,
++    ``/etc``, ``/boot``, ``/run`` or other system paths starting with the
++    Ironic 2025.2 release.
+ 
+   .. warning::
+-     File images must be accessible to every conductor! Use a shared file
+-     system if you have more than one conductor. The ironic CLI tool will not
+-     transfer the file from a local machine to the conductor(s).
++    The Ironic CLI tool will not transfer the file from a local machine to the
++    conductor(s). Operators should use shared file systems or configuration
++    management to ensure consistent availability of images.
+ 
+ .. note::
+    The Bare Metal service tracks content changes for non-Glance images by
+Index: ironic/ironic/common/image_service.py
+===================================================================
+--- ironic.orig/ironic/common/image_service.py
++++ ironic/ironic/common/image_service.py
+@@ -242,14 +242,32 @@ class FileImageService(BaseImageService)
+ 
+         :param image_href: Image reference.
+         :raises: exception.ImageRefValidationFailed if source image file
+-            doesn't exist.
+-        :returns: Path to image file if it exists.
++            doesn't exist, is in a blocked path, or is not in an allowed path.
++        :returns: Path to image file if it exists and is allowed.
+         """
+         image_path = urlparse.urlparse(image_href).path
++
++        # Check if the path is in the blocklist
++        rpath = os.path.abspath(image_path)
++
++        # Check if the path is in the allowlist
++        for allowed in CONF.conductor.file_url_allowed_paths:
++            if rpath == allowed or rpath.startswith(allowed + os.sep):
++                break
++        else:
++            raise exception.ImageRefValidationFailed(
++                image_href=image_href,
++                reason=_(
++                    "Security: Path %s is not allowed for image source "
++                    "file URLs" % image_path)
++            )
++
++        # Check if the file exists
+         if not os.path.isfile(image_path):
+             raise exception.ImageRefValidationFailed(
+                 image_href=image_href,
+                 reason=_("Specified image file not found."))
++
+         return image_path
+ 
+     def download(self, image_href, image_file):
+Index: ironic/ironic/conf/conductor.py
+===================================================================
+--- ironic.orig/ironic/conf/conductor.py
++++ ironic/ironic/conf/conductor.py
+@@ -19,6 +19,7 @@ from oslo_config import cfg
+ from oslo_config import types
+ 
+ from ironic.common.i18n import _
++from ironic.conf import types as ir_types
+ 
+ opts = [
+     cfg.IntOpt('workers_pool_size',
+@@ -384,6 +385,20 @@ opts = [
+                       'is a global setting applying to all requests this '
+                       'conductor receives, regardless of access rights. '
+                       'The concurrent clean limit cannot be disabled.')),
++    cfg.ListOpt('file_url_allowed_paths',
++                default=['/var/lib/ironic', '/shared/html', '/templates',
++                         '/opt/cache/files', '/vagrant'],
++                item_type=ir_types.ExplicitAbsolutePath(),
++                help=_(
++                    'List of paths that are allowed to be used as file:// '
++                    'URLs. Files in /boot, /dev, /etc, /proc, /sys and other'
++                    'system paths are always disallowed for security reasons. 
'
++                    'Any files in this path readable by ironic may be used as 
'
++                    'an image source when deploying. Setting this value to '
++                    '"" (empty) disables file:// URL support. Paths listed '
++                    'here are validated as absolute paths and will be 
rejected'
++                    'if they contain path traversal mechanisms, such as "..".'
++                )),
+ ]
+ 
+ 
+Index: ironic/ironic/conf/types.py
+===================================================================
+--- /dev/null
++++ ironic/ironic/conf/types.py
+@@ -0,0 +1,55 @@
++# Licensed under the Apache License, Version 2.0 (the "License"); you may not
++# use this file except in compliance with the License. You may obtain a copy
++# of the License at
++#
++# http://www.apache.org/licenses/LICENSE-2.0
++#
++# Unless required by applicable law or agreed to in writing, software
++# distributed under the License is distributed on an "AS IS" BASIS,
++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++# See the License for the specific language governing permissions and
++# limitations under the License.
++import os
++
++from oslo_config import types
++
++
++class ExplicitAbsolutePath(types.ConfigType):
++    """Explicit Absolute path type.
++
++    Absolute path values do not get transformed and are returned as
++    strings. They are validated to ensure they are absolute paths and are 
equal
++    to os.path.abspath(value) -- protecting from path traversal issues.
++
++    Python path libraries generally define "absolute path" as anything
++    starting with a /, so tools like path.PurePath(str).is_absolute() is not
++    useful, because it will happily return that /tmp/../etc/resolv.conf is
++    absolute. This type is to be used in cases where we require the path to
++    be explicitly absolute.
++
++    :param type_name: Type name to be used in the sample config file.
++    """
++
++    def __init__(self, type_name='explicit absolute path'):
++        super().__init__(type_name=type_name)
++
++    def __call__(self, value):
++        value = str(value)
++
++        # NOTE(JayF): This removes trailing / if provided, since
++        # os.path.abspath will not return a trailing slash.
++        if len(value) > 1:
++            value = value.rstrip('/')
++        absvalue = os.path.abspath(value)
++        if value != absvalue:
++            raise ValueError('Value must be an absolute path '
++                             'containing no path traversal mechanisms. Config'
++                             f'item was: {value}, but resolved to {absvalue}')
++
++        return value
++
++    def __repr__(self):
++        return 'explicit absolute path'
++
++    def _formatter(self, value):
++        return self.quote_trailing_and_leading_space(value)
+Index: ironic/ironic/tests/unit/common/test_image_service.py
+===================================================================
+--- ironic.orig/ironic/tests/unit/common/test_image_service.py
++++ ironic/ironic/tests/unit/common/test_image_service.py
+@@ -452,20 +452,58 @@ class FileImageServiceTestCase(base.Test
+     def setUp(self):
+         super(FileImageServiceTestCase, self).setUp()
+         self.service = image_service.FileImageService()
+-        self.href = 'file:///home/user/image.qcow2'
+-        self.href_path = '/home/user/image.qcow2'
++        self.href = 'file:///var/lib/ironic/images/image.qcow2'
++        self.href_path = '/var/lib/ironic/images/image.qcow2'
+ 
+     @mock.patch.object(os.path, 'isfile', return_value=True, autospec=True)
+     def test_validate_href(self, path_exists_mock):
+         self.service.validate_href(self.href)
+         path_exists_mock.assert_called_once_with(self.href_path)
+ 
+-    @mock.patch.object(os.path, 'isfile', return_value=False, autospec=True)
++    @mock.patch.object(os.path, 'isfile', return_value=False,
++                       autospec=True)
+     def test_validate_href_path_not_found_or_not_file(self, path_exists_mock):
+         self.assertRaises(exception.ImageRefValidationFailed,
+                           self.service.validate_href, self.href)
+         path_exists_mock.assert_called_once_with(self.href_path)
+ 
++    @mock.patch.object(os.path, 'abspath', autospec=True)
++    def test_validate_href_empty_allowlist(self, abspath_mock):
++        abspath_mock.return_value = self.href_path
++        cfg.CONF.set_override('file_url_allowed_paths', [], 'conductor')
++        self.assertRaisesRegex(exception.ImageRefValidationFailed,
++                               "is not allowed for image source file URLs",
++                               self.service.validate_href, self.href)
++
++    @mock.patch.object(os.path, 'abspath', autospec=True)
++    def test_validate_href_not_in_allowlist(self, abspath_mock):
++        href = "file:///var/is/allowed/not/this/path/image.qcow2"
++        href_path = "/var/is/allowed/not/this/path/image.qcow2"
++        abspath_mock.side_effect = ['/var/lib/ironic', href_path]
++        cfg.CONF.set_override('file_url_allowed_paths', ['/var/lib/ironic'],
++                              'conductor')
++        self.assertRaisesRegex(exception.ImageRefValidationFailed,
++                               "is not allowed for image source file URLs",
++                               self.service.validate_href, href)
++
++    @mock.patch.object(os.path, 'abspath', autospec=True)
++    @mock.patch.object(os.path, 'isfile',
++                       return_value=True, autospec=True)
++    def test_validate_href_in_allowlist(self,
++                                        path_exists_mock,
++                                        abspath_mock):
++        href_dir = '/var/lib'  # self.href_path is in /var/lib/ironic/images/
++        # First call is ironic.conf.types.ExplicitAbsolutePath
++        # Second call is in validate_href()
++        abspath_mock.side_effect = [href_dir, self.href_path]
++        cfg.CONF.set_override('file_url_allowed_paths', [href_dir],
++                              'conductor')
++        result = self.service.validate_href(self.href)
++        self.assertEqual(self.href_path, result)
++        path_exists_mock.assert_called_once_with(self.href_path)
++        abspath_mock.assert_has_calls(
++            [mock.call(href_dir), mock.call(self.href_path)])
++
+     @mock.patch.object(os.path, 'getmtime', return_value=1431087909.1641912,
+                        autospec=True)
+     @mock.patch.object(os.path, 'getsize', return_value=42, autospec=True)
+Index: ironic/ironic/tests/unit/conf/test_conductor.py
+===================================================================
+--- /dev/null
++++ ironic/ironic/tests/unit/conf/test_conductor.py
+@@ -0,0 +1,34 @@
++#    Licensed under the Apache License, Version 2.0 (the "License"); you may
++#    not use this file except in compliance with the License. You may obtain
++#    a copy of the License at
++#
++#         http://www.apache.org/licenses/LICENSE-2.0
++#
++#    Unless required by applicable law or agreed to in writing, software
++#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
++#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
++#    License for the specific language governing permissions and limitations
++#    under the License.
++
++from ironic.conf import CONF
++from ironic.tests.base import TestCase
++
++
++class ValidateConductorAllowedPaths(TestCase):
++    def test_abspath_validation_bad_path_raises(self):
++        """Verifies setting a relative path raises an error via 
oslo.config."""
++        self.assertRaises(
++            ValueError,
++            CONF.set_override,
++            'file_url_allowed_paths',
++            ['bad/path'],
++            'conductor'
++        )
++
++    def test_abspath_validation_good_paths(self):
++        """Verifies setting an absolute path works via oslo.config."""
++        CONF.set_override('file_url_allowed_paths', ['/var'], 'conductor')
++
++    def test_abspath_validation_good_paths_trailing_slash(self):
++        """Verifies setting an absolute path works via oslo.config."""
++        CONF.set_override('file_url_allowed_paths', ['/var/'], 'conductor')
+Index: ironic/ironic/tests/unit/conf/test_types.py
+===================================================================
+--- /dev/null
++++ ironic/ironic/tests/unit/conf/test_types.py
+@@ -0,0 +1,63 @@
++#    Licensed under the Apache License, Version 2.0 (the "License"); you may
++#    not use this file except in compliance with the License. You may obtain
++#    a copy of the License at
++#
++#         http://www.apache.org/licenses/LICENSE-2.0
++#
++#    Unless required by applicable law or agreed to in writing, software
++#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
++#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
++#    License for the specific language governing permissions and limitations
++#    under the License.
++
++from ironic.conf import types
++from ironic.tests.base import TestCase
++
++
++class ExplicitAbsolutePath(TestCase):
++    def test_explicit_absolute_path(self):
++        """Verifies the Opt subclass used to validate absolute paths."""
++        good_paths = [
++            '/etc/passwd',  # Valid
++            '/usr/bin/python',  # Valid
++            '/home/user/file.txt',  # Valid - dot in filename allowed
++            '/var/lib/ironic/.secretdir',  # Valid - hidden directory allowed
++            '/var/lib/ironic/oslo.config',  # Valid - dots in filename allowed
++            '/tmp/',  # Valid
++            '/',  # Valid (root directory)
++            '/.hidden_root_file',  # Valid
++            '/path/including/a/numb3r',  # Valid
++            '/a/path/with/a/trailing/slash/'  # Valid
++        ]
++        bad_paths = [
++            'relative/path',  # Invalid - no leading slash
++            './file.txt',  # Invalid - relative path
++            '../file.txt',  # Invalid - relative path
++            'file.txt',  # Invalid - no leading slash
++            '',  # Invalid - empty string
++            '/var/lib/ironic/../../../etc/passwd',  # Invalid - path traversal
++            '/etc/../etc/passwd',  # Invalid - path traversal
++            '/home/user/./config',  # Invalid - contains current dir reference
++            '/home/user/../user/config',  # Invalid - path traversal
++            '/../etc/passwd',  # Invalid - path traversal at beginning
++            '/.',  # Invalid - just current directory
++            '/..'  # Invalid - just parent directory
++        ]
++
++        eap = types.ExplicitAbsolutePath()
++
++        def _trypath(tpath):
++            try:
++                eap(tpath)
++            except ValueError:
++                return False
++            else:
++                return True
++
++        for path in good_paths:
++            self.assertTrue(_trypath(path),
++                            msg=f"Improperly disallowed path: {path}")
++
++        for path in bad_paths:
++            self.assertFalse(_trypath(path),
++                             msg=f"Improperly allowed path: {path}")
+Index: 
ironic/releasenotes/notes/ossa-2025-001-disallow-unsafe-image-paths-670fdcfe3e4647d4.yaml
+===================================================================
+--- /dev/null
++++ 
ironic/releasenotes/notes/ossa-2025-001-disallow-unsafe-image-paths-670fdcfe3e4647d4.yaml
+@@ -0,0 +1,29 @@
++---
++security:
++  - |
++    Fixes OSSA-2025-001, where Ironic did not properly filter file:// paths
++    when used as image sources. This would permit any file accessible by the
++    conductor to be used as an image to attempt deployment.
++
++    Adds ``CONF.conductor.file_url_allowed_paths``, an allowlist configuration
++    defaulting to ``/var/lib/ironic``, ``/shared/html``,
++    ``/opt/cache/files``, ``/vagrant``, and  ``/templates``,
++    permits operators to further restrict where the conductor will fetch
++    images for when provided a file:// URL. This default value was chosen
++    based on known usage by projects downstream of Ironic, including Metal3,
++    Bifrost, and OpenShift. These defaults may change to be more restrictive
++    at a later date. Operators using file:// URLs are encouraged to explicitly
++    set this value even if the current default is sufficient. Operators 
wishing
++    to fully disable the ability to deploy with a file:// URL should set this
++    configuration to "" (empty).
++
++    Operators wishing to restore the original insecure behavior should set
++    ``CONF.conductor.file_url_allowed_paths`` to ``/``. Take note that in the
++    2025.2 release and later, ``/dev``, ``/sys``, ``/proc``, ``/run``, and
++    ``/etc`` will be unconditionally blocked as a security measure.
++
++    This issue only poses a significant security risk when Ironic's
++    automated cleaning process is disabled and the service is configured in
++    such a way that permits direct deployment by an untrusted API user, such 
as
++    standalone Ironic installations or environments granting ownership of 
nodes
++    to projects.
diff -Nru 
ironic-21.1.0/debian/patches/CVE-2026-42510_Shell-quote_console_command_passed_to_socat.patch
 
ironic-21.1.0/debian/patches/CVE-2026-42510_Shell-quote_console_command_passed_to_socat.patch
--- 
ironic-21.1.0/debian/patches/CVE-2026-42510_Shell-quote_console_command_passed_to_socat.patch
       1970-01-01 01:00:00.000000000 +0100
+++ 
ironic-21.1.0/debian/patches/CVE-2026-42510_Shell-quote_console_command_passed_to_socat.patch
       2026-04-30 10:41:21.000000000 +0200
@@ -0,0 +1,77 @@
+Author: Afonne-CID <[email protected]>
+Date: Wed, 22 Apr 2026 22:51:20 +0100
+Description: CVE-2026-42510: Shell-quote console command passed to `socat`
+ Applies shell quoting to console command passed to `socat`'s EXEC:.
+Bug: https://launchpad.net/bugs/2148331
+Bug-Debian: https://bugs.debian.org/1135255
+Change-Id: Ib27f8ee08184a061beef28192bce39f690c9eaa3
+Signed-off-by: Afonne-CID <[email protected]>
+Origin: upstream, https://review.opendev.org/c/openstack/ironic/+/986418
+
+diff --git a/ironic/drivers/modules/console_utils.py 
b/ironic/drivers/modules/console_utils.py
+index c5e9e85..3019866 100644
+--- a/ironic/drivers/modules/console_utils.py
++++ b/ironic/drivers/modules/console_utils.py
+@@ -23,6 +23,7 @@
+ import fcntl
+ import ipaddress
+ import os
++import shlex
+ import signal
+ import socket
+ import subprocess
+@@ -424,7 +425,8 @@
+     args.append(arg % {'host': console_host,
+                        'port': port})
+ 
+-    args.append('EXEC:"%s",pty,stderr' % console_cmd)
++    quoted_cmd = shlex.quote(console_cmd)
++    args.append('EXEC:"%s",pty,stderr' % quoted_cmd)
+ 
+     # run the command as a subprocess
+     try:
+diff --git a/ironic/tests/unit/drivers/modules/test_console_utils.py 
b/ironic/tests/unit/drivers/modules/test_console_utils.py
+index 4ce3ae4..c6d8168 100644
+--- a/ironic/tests/unit/drivers/modules/test_console_utils.py
++++ b/ironic/tests/unit/drivers/modules/test_console_utils.py
+@@ -534,13 +534,14 @@
+                        autospec=True)
+     def _test_start_socat_console_check_arg(self, mock_timer_start,
+                                             mock_stop, mock_dir_exists,
+-                                            mock_get_pid, mock_popen):
++                                            mock_get_pid, mock_popen,
++                                            console_cmd='ls&'):
+         mock_timer_start.return_value = mock.Mock()
+         mock_get_pid.return_value = '/tmp/%s.pid' % self.info['uuid']
+ 
+         console_utils.start_socat_console(self.info['uuid'],
+                                           self.info['port'],
+-                                          'ls&')
++                                          console_cmd)
+ 
+         mock_stop.assert_called_once_with(self.info['uuid'])
+         mock_dir_exists.assert_called_once_with()
+@@ -549,6 +550,12 @@
+         mock_popen.assert_called_once_with(mock.ANY, stderr=subprocess.PIPE)
+         return mock_popen.call_args[0][0]
+ 
++    def test_escape_start_socat_console_command(self):
++        command = ";cat /etc/passwd; && echo it\'s tricky"
++        quoted_command = ';cat /etc/passwd; && echo it\'"\'"\'s tricky'
++        args = self._test_start_socat_console_check_arg(console_cmd=command)
++        self.assertIn(quoted_command, args[-1])
++
+     def test_start_socat_console_check_arg_default_timeout(self):
+         args = self._test_start_socat_console_check_arg()
+         self.assertIn('-T600', args)
+diff --git 
a/releasenotes/notes/escape-socat-console-string-arguments-555388ab8dcb8cc3.yaml
 
b/releasenotes/notes/escape-socat-console-string-arguments-555388ab8dcb8cc3.yaml
+new file mode 100644
+index 0000000..cbd63db
+--- /dev/null
++++ 
b/releasenotes/notes/escape-socat-console-string-arguments-555388ab8dcb8cc3.yaml
+@@ -0,0 +1,5 @@
++---
++fixes:
++  - |
++    Fixes an issue where console command passed to `socat`'s `EXEC:` was not
++    quoted which could have security implications.
diff -Nru 
ironic-21.1.0/debian/patches/CVE-2026-42997_OSSN-2026-010_validate_molds_url_against_swift_in_keystone_catalog.patch
 
ironic-21.1.0/debian/patches/CVE-2026-42997_OSSN-2026-010_validate_molds_url_against_swift_in_keystone_catalog.patch
--- 
ironic-21.1.0/debian/patches/CVE-2026-42997_OSSN-2026-010_validate_molds_url_against_swift_in_keystone_catalog.patch
        1970-01-01 01:00:00.000000000 +0100
+++ 
ironic-21.1.0/debian/patches/CVE-2026-42997_OSSN-2026-010_validate_molds_url_against_swift_in_keystone_catalog.patch
        2026-04-30 10:41:21.000000000 +0200
@@ -0,0 +1,230 @@
+Author: Julia Kreger <[email protected]>
+Date: Tue, 28 Apr 2026 14:05:28 -0700
+Description: CVE-2026-42997 / OSSN-2026-010: security: validate molds url 
against swift in keystone catalog
+ Adds a security check to ensure the URL "netloc", i.e. hostname
+ and port, matches the user supplied URL if the configuration
+ molds feature for Dell hardware goes down the path of attempting
+ to get or set the URL. This works by sending the url to the
+ authorization code and handling checking the URL there as a
+ safety check prior to proceeding.
+Bug: https://launchpad.net/bugs/2148317
+Bug-Debian: https://bugs.debian.org/1135898
+Change-Id: I22a476dd3734ae4c1940c595f537be1c1a94acf5
+Signed-off-by: Julia Kreger <[email protected]>
+Origin: upstream, https://review.opendev.org/c/openstack/ironic/+/986817
+Last-Update: 2026-05-07
+
+diff --git a/ironic/common/molds.py b/ironic/common/molds.py
+index 234fcc6..ec61116 100644
+--- a/ironic/common/molds.py
++++ b/ironic/common/molds.py
+@@ -13,6 +13,7 @@
+ # under the License.
+ 
+ import json
++from urllib.parse import urlparse
+ 
+ from oslo_config import cfg
+ from oslo_log import log as logging
+@@ -22,6 +23,7 @@
+ 
+ from ironic.common import exception
+ from ironic.common.i18n import _
++from ironic.common import keystone
+ from ironic.common import swift
+ 
+ LOG = logging.getLogger(__name__)
+@@ -51,7 +53,7 @@
+         return requests.put(
+             url, data=json.dumps(data, indent=2), headers=auth_header)
+ 
+-    auth_header = _get_auth_header(task)
++    auth_header = _get_auth_header(task, url)
+     response = _request(url, data, auth_header)
+     response.raise_for_status()
+ 
+@@ -78,7 +80,7 @@
+     def _request(url, auth_header):
+         return requests.get(url, headers=auth_header)
+ 
+-    auth_header = _get_auth_header(task)
++    auth_header = _get_auth_header(task, url)
+     response = _request(url, auth_header)
+     if response.status_code == requests.codes.ok:
+         if not response.content:
+@@ -96,7 +98,7 @@
+     response.raise_for_status()
+ 
+ 
+-def _get_auth_header(task):
++def _get_auth_header(task, url):
+     """Based on setup of configuration mold storage gets authentication header
+ 
+     :param task: A TaskManager instance.
+@@ -105,8 +107,21 @@
+     """
+     auth_header = None
+     if CONF.molds.storage == 'swift':
++        swift_session = swift.get_swift_session()
++        # NOTE(TheJulia): First validate the URL.
++        if url:
++            endpoint = keystone.get_endpoint('swift',
++                                             session=swift_session)
++            if (not endpoint
++                    or urlparse(endpoint).netloc != urlparse(url).netloc):
++                # Raise an InvalidParameterValue should the value not
++                # match swift configuration.
++                raise exception.InvalidParameterValue(
++                    _('Supplied URL for a mold target does not match the '
++                      'swift configuration.'))
++
+         # TODO(ajya) Need to update to use Swift client and context session
+-        auth_token = swift.get_swift_session().get_token()
++        auth_token = swift_session.get_token()
+         if auth_token:
+             auth_header = {'X-Auth-Token': auth_token}
+         else:
+diff --git a/ironic/tests/unit/common/test_molds.py 
b/ironic/tests/unit/common/test_molds.py
+index 810dd61..9c57979 100644
+--- a/ironic/tests/unit/common/test_molds.py
++++ b/ironic/tests/unit/common/test_molds.py
+@@ -19,6 +19,7 @@
+ import requests
+ 
+ from ironic.common import exception
++from ironic.common import keystone
+ from ironic.common import molds
+ from ironic.common import swift
+ from ironic.conductor import task_manager
+@@ -32,15 +33,18 @@
+         super(ConfigurationMoldTestCase, self).setUp()
+         self.node = obj_utils.create_test_node(self.context)
+ 
++    @mock.patch.object(keystone, 'get_endpoint', autospec=True)
+     @mock.patch.object(swift, 'get_swift_session', autospec=True)
+     @mock.patch.object(requests, 'put', autospec=True)
+-    def test_save_configuration_swift(self, mock_put, mock_swift):
++    def test_save_configuration_swift(self, mock_put, mock_swift,
++                                      mock_endpoint):
+         mock_session = mock.Mock()
+         mock_session.get_token.return_value = 'token'
+         mock_swift.return_value = mock_session
+         cfg.CONF.set_override('storage', 'swift', 'molds')
+         url = 'https://example.com/file1'
+         data = {'key': 'value'}
++        mock_endpoint.return_value = 'https://example.com/v1'
+ 
+         with task_manager.acquire(self.context, self.node.uuid) as task:
+             molds.save_configuration(task, url, data)
+@@ -48,15 +52,18 @@
+         mock_put.assert_called_once_with(url, '{\n  "key": "value"\n}',
+                                          headers={'X-Auth-Token': 'token'})
+ 
++    @mock.patch.object(keystone, 'get_endpoint', autospec=True)
+     @mock.patch.object(swift, 'get_swift_session', autospec=True)
+     @mock.patch.object(requests, 'put', autospec=True)
+-    def test_save_configuration_swift_noauth(self, mock_put, mock_swift):
++    def test_save_configuration_swift_noauth(self, mock_put, mock_swift,
++                                             mock_endpoint):
+         mock_session = mock.Mock()
+         mock_session.get_token.return_value = None
+         mock_swift.return_value = mock_session
+         cfg.CONF.set_override('storage', 'swift', 'molds')
+         url = 'https://example.com/file1'
+         data = {'key': 'value'}
++        mock_endpoint.return_value = 'https://example.com/v1'
+ 
+         with task_manager.acquire(self.context, self.node.uuid) as task:
+             self.assertRaises(
+@@ -158,9 +165,11 @@
+                 headers={'Authorization': 'Basic dXNlcjpwYXNzd29yZA=='})
+             self.assertEqual(mock_put.call_count, 2)
+ 
++    @mock.patch.object(keystone, 'get_endpoint', autospec=True)
+     @mock.patch.object(swift, 'get_swift_session', autospec=True)
+     @mock.patch.object(requests, 'get', autospec=True)
+-    def test_get_configuration_swift(self, mock_get, mock_swift):
++    def test_get_configuration_swift(self, mock_get, mock_swift,
++                                     mock_endpoint):
+         mock_session = mock.Mock()
+         mock_session.get_token.return_value = 'token'
+         mock_swift.return_value = mock_session
+@@ -171,6 +180,7 @@
+         response.json.return_value = {'key': 'value'}
+         mock_get.return_value = response
+         url = 'https://example.com/file1'
++        mock_endpoint.return_value = 'https://example.com/v1'
+ 
+         with task_manager.acquire(self.context, self.node.uuid) as task:
+             result = molds.get_configuration(task, url)
+@@ -178,15 +188,44 @@
+             mock_get.assert_called_once_with(
+                 url, headers={'X-Auth-Token': 'token'})
+             self.assertJsonEqual({'key': 'value'}, result)
++        mock_endpoint.assert_called_once_with('swift', session=mock_session)
+ 
++    @mock.patch.object(keystone, 'get_endpoint', autospec=True)
+     @mock.patch.object(swift, 'get_swift_session', autospec=True)
+     @mock.patch.object(requests, 'get', autospec=True)
+-    def test_get_configuration_swift_noauth(self, mock_get, mock_swift):
++    def test_get_configuration_swift_url_mismatch(
++            self, mock_get, mock_swift, mock_endpoint):
++        mock_session = mock.Mock()
++        mock_session.get_token.return_value = 'token'
++        mock_swift.return_value = mock_session
++        cfg.CONF.set_override('storage', 'swift', 'molds')
++        response = mock.MagicMock()
++        response.status_code = 200
++        response.content = "{'key': 'value'}"
++        response.json.return_value = {'key': 'value'}
++        mock_get.return_value = response
++        url = 'https://example.com/file1'
++        mock_endpoint.return_value = 'https://cloud.foo/v1'
++
++        with task_manager.acquire(self.context, self.node.uuid) as task:
++            self.assertRaises(exception.InvalidParameterValue,
++                              molds.get_configuration,
++                              task, url)
++
++        mock_endpoint.assert_called_once_with('swift', session=mock_session)
++        mock_get.assert_not_called()
++
++    @mock.patch.object(keystone, 'get_endpoint', autospec=True)
++    @mock.patch.object(swift, 'get_swift_session', autospec=True)
++    @mock.patch.object(requests, 'get', autospec=True)
++    def test_get_configuration_swift_noauth(self, mock_get, mock_swift,
++                                            mock_endpoint):
+         mock_session = mock.Mock()
+         mock_session.get_token.return_value = None
+         mock_swift.return_value = mock_session
+         cfg.CONF.set_override('storage', 'swift', 'molds')
+         url = 'https://example.com/file1'
++        mock_endpoint.return_value = 'https://example.com/v1'
+ 
+         with task_manager.acquire(self.context, self.node.uuid) as task:
+             self.assertRaises(
+diff --git a/releasenotes/notes/fix-bug-2148317-471adcfac69791dc.yaml 
b/releasenotes/notes/fix-bug-2148317-471adcfac69791dc.yaml
+new file mode 100644
+index 0000000..dafc3bb
+--- /dev/null
++++ b/releasenotes/notes/fix-bug-2148317-471adcfac69791dc.yaml
+@@ -0,0 +1,19 @@
++---
++fixes:
++  - |
++    Fixes a security issue where the deprecated configuration molds feature
++    would allow an user invoking molds to request authorization
++    to be sent to a remote endpoint. This user supplied URL could be a
++    ``swift`` or ``http`` url. While when used with ``http``, the feature
++    was explicitly designed around a concept of just publishing to a file
++    in a limited context with authentication details provided by the
++    conductor, where as with ``swift`` the impact is greater because the
++    time limited session token for Ironic's access of swift resources could
++    be leaked, captured, and used.
++
++    The configuration molds feature now explicitly checks the swift endpoint
++    URL and raises an exception when the URL does not match the user supplied
++    the configured Swift endpoint.
++
++    More information can be found in
++    `bug 2148317 <https://bugs.launchpad.net/ironic/+bug/2148317>`_.
diff -Nru ironic-21.1.0/debian/patches/series 
ironic-21.1.0/debian/patches/series
--- ironic-21.1.0/debian/patches/series 2023-04-14 13:48:42.000000000 +0200
+++ ironic-21.1.0/debian/patches/series 2026-04-30 10:41:21.000000000 +0200
@@ -1,2 +1,5 @@
 adds-alembic.ini-in-MANIFEST.in.patch
 py3.11_fix_unit_tests.patch
+CVE-2025-44021_OSSA-2025-001_Disallow_unsafe_image_file_paths.patch
+CVE-2026-42510_Shell-quote_console_command_passed_to_socat.patch
+CVE-2026-42997_OSSN-2026-010_validate_molds_url_against_swift_in_keystone_catalog.patch

--- End Message ---
--- Begin Message ---
Version: 12.15

This update was released as part of 12.15.

--- End Message ---

Reply via email to