From f9f1585d9ed3823a0c3e58530a1cbf519f5583ca Mon Sep 17 00:00:00 2001 From: Khairajani Date: Wed, 19 Aug 2026 14:10:52 +0530 Subject: [PATCH] fix(security): pull expat 2.8.3-1~deb13u1 into the ingestion-base image The image reports expat 2.8.2 for CVE-2026-72522 (DSA-6446-1: out-of-bounds read and resultant infinite loop in libexpat's *_toUtf16 surrogate handling); trixie-security has 2.8.3-1~deb13u1. expat is not in the python:3.12-slim-trixie base at all -- it comes in transitively from the top-of-file apt install -- and that layer is exactly why the image is stuck at 2.8.2: its cache key never changes, so it keeps resolving against the Debian index that was current when it was first built. Adding expat there would freeze the fix the same way. It goes in the late root layer instead, below the COPY and pip layers, where the index is re-read on every build -- the same reasoning that put util-linux there. expat rides in the existing dpkg source-package query rather than a second RUN, so one apt call covers both source packages and the next OS CVE is a one-line edit. The package set stays computed from dpkg rather than hand-listed for the reason already documented there: scanners report each binary of a source package separately, so a hand-written list silently leaves one behind. util-linux keeps its non-empty assert and expat deliberately does not get one. util-linux is Essential, so an empty query there means dpkg-query misbehaved and the build must stop; expat is transitive, so an empty query is a legitimate image with nothing to patch and failing on it would break the build for no reason. Asserting one of the two still covers the hazard the check exists for: `apt-get install --only-upgrade` with no package arguments exits 0, so an all-empty query would give a green build that shipped the vulnerable packages. Verified in a real build layer on python:3.12-slim-trixie: libexpat1 2.7.1-2 -> 2.8.3-1~deb13u1 and all nine util-linux binaries -> 2.41.5-0+deb13u1, exit 0. The expat-absent path was exercised on the bare base and also exits 0, upgrading util-linux alone. ingestion/Dockerfile gets no equivalent change because there is nothing to upgrade to, not because it is clean: apache/airflow:3.3.0-python3.12 is bookworm, Debian still marks bookworm and bookworm-security vulnerable, and the image already carries 2.5.0-1+deb12u2 -- the newest build either suite offers. It needs revisiting when Debian ships a bookworm fix. --- ingestion/operators/docker/Dockerfile | 33 +++++++++++++++--------- ingestion/operators/docker/Dockerfile.ci | 33 +++++++++++++++--------- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/ingestion/operators/docker/Dockerfile b/ingestion/operators/docker/Dockerfile index 8cb65b4560ee..5d44c8173594 100644 --- a/ingestion/operators/docker/Dockerfile +++ b/ingestion/operators/docker/Dockerfile @@ -197,29 +197,38 @@ RUN pip install psycopg2 mysqlclient==2.1.1 # apt-mark manual pins them before autoremove runs so the cleanup doesn't take # them out too -- verified end-to-end by the `import MySQLdb` gate check. # -# The util-linux upgrade rides along in the same root layer. trixie's base ships -# 2.41-5, which carries CVE-2025-14104, CVE-2026-13595 and CVE-2026-27456; -# trixie-security has 2.41.5-0+deb13u1. -# The package set is computed from dpkg rather than hand-listed. One source +# The OS security upgrades ride along in the same root layer. trixie's base ships +# util-linux 2.41-5, which carries CVE-2025-14104, CVE-2026-13595 and +# CVE-2026-27456; trixie-security has 2.41.5-0+deb13u1. +# expat is the same story one release later. It is not in the trixie base at all +# -- it arrives transitively from the top-of-file apt layer -- and that layer's +# frozen index is exactly why the image keeps shipping 2.8.2 and being reported +# for CVE-2026-72522 while trixie-security has 2.8.3-1~deb13u1. +# The package sets are computed from dpkg rather than hand-listed. One source # package produces many binaries -- here util-linux, bsdutils, login, mount, # liblastlog2-2, libblkid1, libmount1, libsmartcols1 and libuuid1 -- and scanners # report each separately, so a hand-written list silently leaves behind whichever # binary it forgot, and liblastlog2-2 is exactly the one that is easy to forget. -# Asking dpkg which installed packages came from the util-linux source cannot -# miss one, and stays correct if Debian splits the source differently later. -# The empty check is not defensive noise: `apt-get install --only-upgrade` with -# no package arguments exits 0, so a query that silently returned nothing would -# give a green build that shipped the vulnerable packages anyway. Fail closed. +# Asking dpkg which installed packages came from the source cannot miss one, and +# stays correct if Debian splits the source differently later. +# util-linux is asserted non-empty and expat deliberately is not: util-linux is +# Essential, so an empty query there means dpkg-query misbehaved and the build +# must not continue, whereas expat is transitive and an empty query is a +# legitimate image with nothing to patch. Asserting one of the two is what the +# check is for -- `apt-get install --only-upgrade` with no package arguments +# exits 0, so an all-empty query would give a green build that shipped the +# vulnerable packages anyway. Fail closed. # This deliberately does NOT go in the top-of-file apt RUN: that layer's cache # key never changes, so an upgrade placed there freezes its Debian index with it # and the image keeps shipping whatever was current when the layer was first # built. Below the COPY and pip layers, the index is re-read on every build. USER root RUN set -eu; \ - pkgs="$(dpkg-query -W -f='${source:Package} ${Package}\n' | awk '$1=="util-linux"{print $2}')"; \ - [ -n "$pkgs" ] || { echo "no src:util-linux packages found; refusing to skip the CVE patch" >&2; exit 1; }; \ + ul="$(dpkg-query -W -f='${source:Package} ${Package}\n' | awk '$1=="util-linux"{print $2}')"; \ + [ -n "$ul" ] || { echo "no src:util-linux packages found; refusing to skip the CVE patch" >&2; exit 1; }; \ + ex="$(dpkg-query -W -f='${source:Package} ${Package}\n' | awk '$1=="expat"{print $2}')"; \ apt-get -qq update; \ - apt-get -qq install -y --only-upgrade $pkgs; \ + apt-get -qq install -y --only-upgrade $ul $ex; \ apt-get -qq purge -y libmariadb-dev libmariadb-dev-compat libunbound8; \ apt-mark manual libmariadb3 mariadb-common; \ apt-get -qq autoremove -y --purge; \ diff --git a/ingestion/operators/docker/Dockerfile.ci b/ingestion/operators/docker/Dockerfile.ci index c4ad1cbd0769..8d991f539625 100644 --- a/ingestion/operators/docker/Dockerfile.ci +++ b/ingestion/operators/docker/Dockerfile.ci @@ -205,29 +205,38 @@ RUN pip install psycopg2 mysqlclient==2.1.1 # apt-mark manual pins them before autoremove runs so the cleanup doesn't take # them out too -- verified end-to-end by the `import MySQLdb` gate check. # -# The util-linux upgrade rides along in the same root layer. trixie's base ships -# 2.41-5, which carries CVE-2025-14104, CVE-2026-13595 and CVE-2026-27456; -# trixie-security has 2.41.5-0+deb13u1. -# The package set is computed from dpkg rather than hand-listed. One source +# The OS security upgrades ride along in the same root layer. trixie's base ships +# util-linux 2.41-5, which carries CVE-2025-14104, CVE-2026-13595 and +# CVE-2026-27456; trixie-security has 2.41.5-0+deb13u1. +# expat is the same story one release later. It is not in the trixie base at all +# -- it arrives transitively from the top-of-file apt layer -- and that layer's +# frozen index is exactly why the image keeps shipping 2.8.2 and being reported +# for CVE-2026-72522 while trixie-security has 2.8.3-1~deb13u1. +# The package sets are computed from dpkg rather than hand-listed. One source # package produces many binaries -- here util-linux, bsdutils, login, mount, # liblastlog2-2, libblkid1, libmount1, libsmartcols1 and libuuid1 -- and scanners # report each separately, so a hand-written list silently leaves behind whichever # binary it forgot, and liblastlog2-2 is exactly the one that is easy to forget. -# Asking dpkg which installed packages came from the util-linux source cannot -# miss one, and stays correct if Debian splits the source differently later. -# The empty check is not defensive noise: `apt-get install --only-upgrade` with -# no package arguments exits 0, so a query that silently returned nothing would -# give a green build that shipped the vulnerable packages anyway. Fail closed. +# Asking dpkg which installed packages came from the source cannot miss one, and +# stays correct if Debian splits the source differently later. +# util-linux is asserted non-empty and expat deliberately is not: util-linux is +# Essential, so an empty query there means dpkg-query misbehaved and the build +# must not continue, whereas expat is transitive and an empty query is a +# legitimate image with nothing to patch. Asserting one of the two is what the +# check is for -- `apt-get install --only-upgrade` with no package arguments +# exits 0, so an all-empty query would give a green build that shipped the +# vulnerable packages anyway. Fail closed. # This deliberately does NOT go in the top-of-file apt RUN: that layer's cache # key never changes, so an upgrade placed there freezes its Debian index with it # and the image keeps shipping whatever was current when the layer was first # built. Below the COPY and pip layers, the index is re-read on every build. USER root RUN set -eu; \ - pkgs="$(dpkg-query -W -f='${source:Package} ${Package}\n' | awk '$1=="util-linux"{print $2}')"; \ - [ -n "$pkgs" ] || { echo "no src:util-linux packages found; refusing to skip the CVE patch" >&2; exit 1; }; \ + ul="$(dpkg-query -W -f='${source:Package} ${Package}\n' | awk '$1=="util-linux"{print $2}')"; \ + [ -n "$ul" ] || { echo "no src:util-linux packages found; refusing to skip the CVE patch" >&2; exit 1; }; \ + ex="$(dpkg-query -W -f='${source:Package} ${Package}\n' | awk '$1=="expat"{print $2}')"; \ apt-get -qq update; \ - apt-get -qq install -y --only-upgrade $pkgs; \ + apt-get -qq install -y --only-upgrade $ul $ex; \ apt-get -qq purge -y libmariadb-dev libmariadb-dev-compat libunbound8; \ apt-mark manual libmariadb3 mariadb-common; \ apt-get -qq autoremove -y --purge; \