diff --git a/.grype.yaml b/.grype.yaml index c557c16..713bed0 100644 --- a/.grype.yaml +++ b/.grype.yaml @@ -1,6 +1,6 @@ -# The Dockerfile applies the upstream 3.10 XML hash-salt patch and tests both -# XML parsers. The version-only binary matcher cannot see the backport. -# https://github.com/python/cpython/commit/ea70712d1a8508e14e9677d44f838dab04dc0286 +# Temporary acceptance of CVE-2026-7210 in the unpatched upstream Python 3.10.21. +# Remove after adopting an official Python 3.10 image containing the XML hash-salt fix. +# Tracked in https://github.com/wodby/python/issues/10 ignore: - vulnerability: CVE-2026-7210 package: diff --git a/Dockerfile b/Dockerfile index 25ba3db..867226c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,32 +1,6 @@ ARG PYTHON_VER -FROM python:${PYTHON_VER}-alpine AS python-security - -ARG PYTHON_VER -COPY patches/python310-xml-hash-salt.patch /tmp/python310-xml-hash-salt.patch - -# Backport the upstream XML hash-flooding fix until Python 3.10.22 is released. -# Other Python versions keep the official interpreter unchanged. -RUN set -eux; \ - if [ "${PYTHON_VER}" = "3.10.21" ]; then \ - apk add --no-cache build-base bzip2-dev gdbm-dev libffi-dev \ - libnsl-dev libtirpc-dev linux-headers ncurses-dev openssl-dev \ - readline-dev sqlite-dev tcl-dev tk-dev util-linux-dev xz-dev zlib-dev; \ - wget -O /tmp/python.tar.xz "https://www.python.org/ftp/python/${PYTHON_VER}/Python-${PYTHON_VER}.tar.xz"; \ - echo 'a0da1e72132e950154eca0f6f47d5db828454700de20e5113667940d81e0db04 /tmp/python.tar.xz' | sha256sum -c -; \ - mkdir /tmp/python-src; \ - tar -xJf /tmp/python.tar.xz --strip-components=1 -C /tmp/python-src; \ - cd /tmp/python-src; \ - patch -p1 < /tmp/python310-xml-hash-salt.patch; \ - ./configure --enable-shared --enable-loadable-sqlite-extensions --with-ensurepip; \ - make -j2 EXTRA_CFLAGS='-DTHREAD_STACK_SIZE=0x100000'; \ - make install; \ - ./python -m test test_pyexpat test_xml_etree test_xml_etree_c; \ - rm -rf /usr/local/lib/python3.10/test /usr/local/lib/libpython*.a; \ - fi - FROM python:${PYTHON_VER}-alpine -COPY --from=python-security /usr/local/ /usr/local/ LABEL com.wodby.ci.cache="uv" diff --git a/patches/python310-xml-hash-salt.patch b/patches/python310-xml-hash-salt.patch deleted file mode 100644 index 41ffba5..0000000 --- a/patches/python310-xml-hash-salt.patch +++ /dev/null @@ -1,102 +0,0 @@ -diff --git a/Include/pyexpat.h b/Include/pyexpat.h -index f523f8bb273983a..a676e16a7a457ea 100644 ---- a/Include/pyexpat.h -+++ b/Include/pyexpat.h -@@ -62,6 +62,9 @@ struct PyExpat_CAPI - XML_Parser parser, unsigned long long activationThresholdBytes); - XML_Bool (*SetBillionLaughsAttackProtectionMaximumAmplification)( - XML_Parser parser, float maxAmplificationFactor); -+ /* might be NULL for expat < 2.8.0 */ -+ XML_Bool (*SetHashSalt16Bytes)( -+ XML_Parser parser, const uint8_t entropy[16]); - /* always add new stuff to the end! */ - }; - -diff --git a/Include/pyhash.h b/Include/pyhash.h -index a314ea907b7fe21..24a82c3b548d1b5 100644 ---- a/Include/pyhash.h -+++ b/Include/pyhash.h -@@ -39,14 +39,14 @@ PyAPI_FUNC(Py_hash_t) _Py_HashBytes(const void*, Py_ssize_t); - * pppppppp ssssssss ........ fnv -- two Py_hash_t - * k0k0k0k0 k1k1k1k1 ........ siphash -- two uint64_t - * ........ ........ ssssssss djbx33a -- 16 bytes padding + one Py_hash_t -- * ........ ........ eeeeeeee pyexpat XML hash salt -+ * eeeeeeee eeeeeeee eeeeeeee pyexpat XML hash salt - * - * memory layout on 32 bit systems - * cccccccc cccccccc cccccccc uc - * ppppssss ........ ........ fnv -- two Py_hash_t - * k0k0k0k0 k1k1k1k1 ........ siphash -- two uint64_t (*) - * ........ ........ ssss.... djbx33a -- 16 bytes padding + one Py_hash_t -- * ........ ........ eeee.... pyexpat XML hash salt -+ * eeeeeeee eeeeeeee eeee.... pyexpat XML hash salt - * - * (*) The siphash member may not be available on 32 bit platforms without - * an unsigned int64 data type. -@@ -71,7 +71,9 @@ typedef union { - Py_hash_t suffix; - } djbx33a; - struct { -- unsigned char padding[16]; -+ /* 16 bytes for XML_SetHashSalt16Bytes */ -+ uint8_t hashsalt16[16]; -+ /* 4/8 bytes for legacy XML_SetHashSalt */ - Py_hash_t hashsalt; - } expat; - } _Py_HashSecret_t; -diff --git a/Misc/NEWS.d/next/Security/2026-04-26-19-30-45.gh-issue-149018.a9SqWb.rst b/Misc/NEWS.d/next/Security/2026-04-26-19-30-45.gh-issue-149018.a9SqWb.rst -new file mode 100644 -index 000000000000000..d1b5b368684e6a5 ---- /dev/null -+++ b/Misc/NEWS.d/next/Security/2026-04-26-19-30-45.gh-issue-149018.a9SqWb.rst -@@ -0,0 +1,3 @@ -+Improved protection against XML hash-flooding attacks in -+:mod:`xml.parsers.expat` and :mod:`xml.etree.ElementTree` when Python is -+compiled with libExpat 2.8.0 or later. -diff --git a/Modules/_elementtree.c b/Modules/_elementtree.c -index 090d6fd58e10ee7..6d4c56c0580aaeb 100644 ---- a/Modules/_elementtree.c -+++ b/Modules/_elementtree.c -@@ -3663,8 +3663,12 @@ _elementtree_XMLParser___init___impl(XMLParserObject *self, PyObject *target, - PyErr_NoMemory(); - return -1; - } -- /* expat < 2.1.0 has no XML_SetHashSalt() */ -- if (EXPAT(SetHashSalt) != NULL) { -+ // Prefer 16-byte entropy, only expat >= 2.8.0. See gh-149018 -+ if (EXPAT(SetHashSalt16Bytes) != NULL) { -+ EXPAT(SetHashSalt16Bytes)(self->parser, -+ _Py_HashSecret.expat.hashsalt16); -+ } -+ else if (EXPAT(SetHashSalt) != NULL) { - EXPAT(SetHashSalt)(self->parser, - (unsigned long)_Py_HashSecret.expat.hashsalt); - } -diff --git a/Modules/pyexpat.c b/Modules/pyexpat.c -index 738a3f2d01c4e85..80ff3c02a55b0ac 100644 ---- a/Modules/pyexpat.c -+++ b/Modules/pyexpat.c -@@ -1453,7 +1453,10 @@ newxmlparseobject(pyexpat_state *state, const char *encoding, - Py_DECREF(self); - return NULL; - } --#if XML_COMBINED_VERSION >= 20100 -+#if XML_COMBINED_VERSION >= 20800 -+ /* This feature was added upstream in libexpat 2.8.0. */ -+ XML_SetHashSalt16Bytes(self->itself, _Py_HashSecret.expat.hashsalt16); -+#elif XML_COMBINED_VERSION >= 20100 - /* This feature was added upstream in libexpat 2.1.0. */ - XML_SetHashSalt(self->itself, - (unsigned long)_Py_HashSecret.expat.hashsalt); -@@ -2261,6 +2264,11 @@ pyexpat_exec(PyObject *mod) - #else - capi.SetHashSalt = NULL; - #endif -+#if XML_COMBINED_VERSION >= 20800 -+ capi.SetHashSalt16Bytes = XML_SetHashSalt16Bytes; -+#else -+ capi.SetHashSalt16Bytes = NULL; -+#endif - #if XML_COMBINED_VERSION >= 20600 - capi.SetReparseDeferralEnabled = XML_SetReparseDeferralEnabled; - #else