From c8675c3214fb748ade5f93dea9550c9596344f57 Mon Sep 17 00:00:00 2001 From: Ihar Voitka Date: Thu, 23 Apr 2026 19:05:24 -0700 Subject: [PATCH 1/5] Add python3.14 package (side-by-side with python3) Ship Python 3.14.4 as a new SxS package under SPECS/python3.14/. The default python3 (3.12) package is not modified; python3.14 installs strictly under versioned paths (/usr/bin/python3.14, /usr/lib/python3.14/, libpython3.14.so.1.0) and removes unversioned symlinks/pkgconfig/man pages in %install so it cannot collide with python3. Follows the SPECS/nodejs24 precedent for side-by-side major-version packages. CVE patches triaged against the python3 (3.12) set: cgi3.patch dropped (cgi removed in 3.13, PEP 594); all CVE-2025-* patches dropped (fixed upstream in 3.14.4, GA 2026-04-07); CVE-2026-0672, CVE-2026-0865, CVE-2026-1299 and CVE-2026-4519 carried since their fixes post-date 3.14.4. Co-Authored-By: Claude Opus 4.7 (1M context) --- LICENSES-AND-NOTICES/SPECS/LICENSES-MAP.md | 2 +- LICENSES-AND-NOTICES/SPECS/data/licenses.json | 1 + SPECS/python3.14/CVE-2026-0672.patch | 189 +++++++++++++++ SPECS/python3.14/CVE-2026-0865.patch | 102 ++++++++ SPECS/python3.14/CVE-2026-1299.patch | 110 +++++++++ SPECS/python3.14/CVE-2026-4519.patch | 132 ++++++++++ SPECS/python3.14/python3.14.signatures.json | 6 + SPECS/python3.14/python3.14.spec | 226 ++++++++++++++++++ cgmanifest.json | 10 + 9 files changed, 777 insertions(+), 1 deletion(-) create mode 100644 SPECS/python3.14/CVE-2026-0672.patch create mode 100644 SPECS/python3.14/CVE-2026-0865.patch create mode 100644 SPECS/python3.14/CVE-2026-1299.patch create mode 100644 SPECS/python3.14/CVE-2026-4519.patch create mode 100644 SPECS/python3.14/python3.14.signatures.json create mode 100644 SPECS/python3.14/python3.14.spec diff --git a/LICENSES-AND-NOTICES/SPECS/LICENSES-MAP.md b/LICENSES-AND-NOTICES/SPECS/LICENSES-MAP.md index 921a9611b2e..0c4b9b8d901 100644 --- a/LICENSES-AND-NOTICES/SPECS/LICENSES-MAP.md +++ b/LICENSES-AND-NOTICES/SPECS/LICENSES-MAP.md @@ -19,5 +19,5 @@ The Azure Linux SPEC files originated from a variety of sources with varying lic | OpenSUSE | Following [openSUSE guidelines](https://en.opensuse.org/openSUSE:Specfile_guidelines#Specfile_Licensing) | ant
ant-junit
antlr
aopalliance
apache-commons-beanutils
apache-commons-cli
apache-commons-codec
apache-commons-collections
apache-commons-collections4
apache-commons-compress
apache-commons-daemon
apache-commons-dbcp
apache-commons-digester
apache-commons-httpclient
apache-commons-io
apache-commons-jexl
apache-commons-lang3
apache-commons-logging
apache-commons-net
apache-commons-pool
apache-commons-pool2
apache-commons-validator
apache-commons-vfs2
apache-parent
args4j
atinject
base64coder
bcel
bea-stax
beust-jcommander
bsf
byaccj
cal10n
cdparanoia
cglib
cni
containerized-data-importer
cpulimit
cri-o
ecj
ed25519-java
fillup
flux
gd
geronimo-specs
glassfish-annotation-api
gnu-getopt
gnu-regexp
golang-packaging
guava
hamcrest
hawtjni-runtime
httpcomponents-core
influx-cli
influxdb
jakarta-taglibs-standard
jansi
jarjar
java-cup
java-cup-bootstrap
javacc
javacc-bootstrap
javassist
jbcrypt
jboss-interceptors-1.2-api
jdepend
jflex
jflex-bootstrap
jlex
jline
jna
jsch
jsoup
jsr-305
jtidy
junit
junitperf
jzlib
kubevirt
kured
libcontainers-common
libtheora
libva
libvdpau
lynx
multus
objectweb-anttask
objectweb-asm
objenesis
oro
osgi-annotation
osgi-compendium
osgi-core
patterns-ceph-containers
plexus-classworlds
plexus-interpolation
plexus-utils
proj
psl-make-dafsa
publicsuffix
qdox
regexp
relaxngDatatype
rhino
ripgrep
servletapi4
servletapi5
shapelib
slf4j
trilead-ssh2
virtiofsd
xalan-j2
xbean
xcursor-themes
xerces-j2
xml-commons-apis
xml-commons-resolver
xmldb-api
xmlrpc-c
xmlunit
xz-java | | Photon | [Photon License](LICENSE-PHOTON.md) and [Photon Notice](NOTICE.APACHE2).
Also see [LICENSE-EXCEPTIONS.PHOTON](LICENSE-EXCEPTIONS.PHOTON). | acl
alsa-lib
alsa-utils
ansible
apparmor
apr
apr-util
asciidoc
atftp
audit
autoconf
autoconf-archive
autofs
autogen
automake
babel
bash
bc
bcc
bind
binutils
bison
blktrace
boost
btrfs-progs
bubblewrap
build-essential
bzip2
c-ares
cairo
cassandra
cassandra-driver
cdrkit
check
chkconfig
chrpath
cifs-utils
clang
cloud-init
cloud-utils-growpart
cmake
cni-plugins
core-packages
coreutils
cpio
cppunit
cqlsh
cracklib
crash
crash-gcore-command
createrepo_c
cri-tools
cronie
curl
cyrus-sasl
cyrus-sasl-bootstrap
dbus
dbus-glib
dejagnu
device-mapper-multipath
dialog
diffutils
dkms
dmidecode
dnsmasq
docbook-dtd-xml
docbook-style-xsl
dosfstools
dracut
dstat
e2fsprogs
ed
efibootmgr
efivar
elfutils
emacs
erlang
etcd
ethtool
expat
expect
fcgi
file
filesystem
findutils
flex
fontconfig
fping
freetype
fuse
gawk
gc
gcc
gdb
gdbm
gettext
git
git-lfs
glib
glib-networking
glibc
glibmm
gmp
gnome-common
gnupg2
gnuplot
gnutls
gobject-introspection
golang
golang-1.23
golang-1.24
golang-1.25
gperf
gperftools
gpgme
gptfdisk
grep
groff
grub2
gtest
gtk-doc
guile
gzip
haproxy
harfbuzz
haveged
hdparm
http-parser
httpd
i2c-tools
iana-etc
icu
initramfs
initscripts
inotify-tools
intltool
iotop
iperf3
iproute
ipset
iptables
iputils
ipvsadm
ipxe
irqbalance
itstool
jansson
jq
json-c
json-glib
kbd
keepalived
kernel
kernel-64k
kernel-headers
kernel-hwe
kernel-hwe-headers
kernel-ipe
kernel-lpg-innovate
kernel-mshv
kernel-rt
kernel-uvm
kernel-uvm-micro
keyutils
kmod
krb5
less
libaio
libarchive
libassuan
libatomic_ops
libcap
libcap-ng
libconfig
libdb
libdnet
libedit
libestr
libevent
libfastjson
libffi
libgcrypt
libgpg-error
libgssglue
libgudev
libjpeg-turbo
libksba
liblogging
libmbim
libmnl
libmodulemd
libmpc
libmspack
libndp
libnetfilter_conntrack
libnetfilter_cthelper
libnetfilter_cttimeout
libnetfilter_queue
libnfnetlink
libnftnl
libnl3
libnsl2
libpcap
libpipeline
libpng
libpsl
libqmi
librelp
librepo
librsync
libseccomp
libselinux
libsepol
libserf
libsigc++30
libsolv
libsoup
libssh2
libtalloc
libtar
libtasn1
libtiff
libtirpc
libtool
libunistring
libunwind
libusb
libvirt
libwebp
libxml2
libxslt
libyaml
linux-firmware
lldb
lldpad
llvm
lm-sensors
lmdb
log4cpp
logrotate
lshw
lsof
lsscsi
ltrace
lttng-tools
lttng-ust
lvm2
lz4
lzo
m2crypto
m4
make
man-db
man-pages
maven
mc
mercurial
meson
mlocate
ModemManager
mpfr
msr-tools
mysql
nano
nasm
ncurses
ndctl
net-snmp
net-tools
nettle
newt
nfs-utils
nghttp2
nginx
ninja-build
nodejs
nodejs24
npth
nspr
nss
nss-altfiles
ntp
numactl
nvme-cli
oniguruma
OpenIPMI
openldap
openscap
openssh
openvswitch
ostree
pam
pango
parted
patch
pciutils
perl-Canary-Stability
perl-CGI
perl-common-sense
perl-Crypt-SSLeay
perl-DBD-SQLite
perl-DBI
perl-DBIx-Simple
perl-Exporter-Tiny
perl-File-HomeDir
perl-File-Which
perl-IO-Socket-SSL
perl-JSON-Any
perl-JSON-XS
perl-libintl-perl
perl-List-MoreUtils
perl-Module-Build
perl-Module-Install
perl-Module-ScanDeps
perl-Net-SSLeay
perl-NetAddr-IP
perl-Object-Accessor
perl-Path-Class
perl-Try-Tiny
perl-Types-Serialiser
perl-WWW-Curl
perl-XML-Parser
perl-YAML
perl-YAML-Tiny
pgbouncer
pinentry
polkit
popt
postgresql
procps-ng
protobuf
protobuf-c
psmisc
pth
pyasn1-modules
pyOpenSSL
pyparsing
pytest
python-appdirs
python-asn1crypto
python-atomicwrites
python-attrs
python-bcrypt
python-certifi
python-cffi
python-chardet
python-configobj
python-constantly
python-coverage
python-cryptography
python-daemon
python-dateutil
python-defusedxml
python-distro
python-docopt
python-docutils
python-ecdsa
python-geomet
python-gevent
python-hyperlink
python-hypothesis
python-idna
python-imagesize
python-incremental
python-iniparse
python-ipaddr
python-jinja2
python-jmespath
python-jsonpatch
python-jsonpointer
python-jsonschema
python-lockfile
python-lxml
python-mako
python-markupsafe
python-mistune
python-msgpack
python-netaddr
python-netifaces
python-ntplib
python-oauthlib
python-packaging
python-pam
python-pbr
python-ply
python-prettytable
python-psutil
python-psycopg2
python-py
python-pyasn1
python-pycodestyle
python-pycparser
python-pycurl
python-pygments
python-pynacl
python-requests
python-setuptools_scm
python-simplejson
python-six
python-snowballstemmer
python-sphinx-theme-alabaster
python-twisted
python-urllib3
python-vcversioner
python-virtualenv
python-wcwidth
python-webob
python-websocket-client
python-werkzeug
python-zope-event
python-zope-interface
python3
pytz
PyYAML
rapidjson
readline
rng-tools
rpcbind
rpcsvc-proto
rpm
rpm-ostree
rrdtool
rsync
rsyslog
ruby
rust
rust-1.75
scons
sed
sg3_utils
shadow-utils
slang
snappy
socat
sqlite
sshpass
strace
subversion
sudo
swig
syslinux
syslog-ng
sysstat
systemd-bootstrap
systemtap
tar
tboot
tcl
tcpdump
tcsh
tdnf
telegraf
texinfo
tmux
tpm2-abrmd
tpm2-pkcs11
tpm2-pytss
tpm2-tools
tpm2-tss
traceroute
tree
tzdata
unbound
unixODBC
unzip
usbutils
userspace-rcu
utf8proc
util-linux
valgrind
vim
vsftpd
WALinuxAgent
which
wpa_supplicant
xfsprogs
xinetd
xmlsec1
xmlto
xz
zchunk
zeromq
zip
zlib
zsh | | RPM software management source | [GPLv2+ License](https://github.com/rpm-software-management/dnf5/blob/main/COPYING.md) | dnf5 | -| Source project | Same as the source project. | azure-vm-utils
bootengine
coreos-cloudinit
coreos-init
python-nocaselist
update-ssh-keys | +| Source project | Same as the source project. | azure-vm-utils
bootengine
coreos-cloudinit
coreos-init
python-nocaselist
python3.14
update-ssh-keys | | Sysbench source | [GPLv2+ License](https://github.com/akopytov/sysbench/blob/master/COPYING) | sysbench | diff --git a/LICENSES-AND-NOTICES/SPECS/data/licenses.json b/LICENSES-AND-NOTICES/SPECS/data/licenses.json index 180befe61c1..9d64db97c8d 100644 --- a/LICENSES-AND-NOTICES/SPECS/data/licenses.json +++ b/LICENSES-AND-NOTICES/SPECS/data/licenses.json @@ -3177,6 +3177,7 @@ "coreos-cloudinit", "coreos-init", "python-nocaselist", + "python3.14", "update-ssh-keys" ] }, diff --git a/SPECS/python3.14/CVE-2026-0672.patch b/SPECS/python3.14/CVE-2026-0672.patch new file mode 100644 index 00000000000..304e7d5dc92 --- /dev/null +++ b/SPECS/python3.14/CVE-2026-0672.patch @@ -0,0 +1,189 @@ +From 62498dced866fee86727379378acb20a541f3371 Mon Sep 17 00:00:00 2001 +From: Seth Michael Larson +Date: Tue, 20 Jan 2026 15:23:42 -0600 +Subject: [PATCH] gh-143919: Reject control characters in http cookies (cherry + picked from commit 95746b3a13a985787ef53b977129041971ed7f70) +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Co-authored-by: Seth Michael Larson +Co-authored-by: Bartosz Sławecki +Co-authored-by: sobolevn +Signed-off-by: Azure Linux Security Servicing Account +Upstream-reference: https://github.com/python/cpython/pull/144091.patch +--- + Doc/library/http.cookies.rst | 4 +- + Lib/http/cookies.py | 25 +++++++-- + Lib/test/test_http_cookies.py | 52 +++++++++++++++++-- + ...-01-16-11-13-15.gh-issue-143919.kchwZV.rst | 1 + + 4 files changed, 73 insertions(+), 9 deletions(-) + create mode 100644 Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst + +diff --git a/Doc/library/http.cookies.rst b/Doc/library/http.cookies.rst +index ad37a0f..317a71a 100644 +--- a/Doc/library/http.cookies.rst ++++ b/Doc/library/http.cookies.rst +@@ -272,9 +272,9 @@ The following example demonstrates how to use the :mod:`http.cookies` module. + Set-Cookie: chips=ahoy + Set-Cookie: vienna=finger + >>> C = cookies.SimpleCookie() +- >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=\\012;";') ++ >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=;";') + >>> print(C) +- Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=\012;" ++ Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=;" + >>> C = cookies.SimpleCookie() + >>> C["oreo"] = "doublestuff" + >>> C["oreo"]["path"] = "/" +diff --git a/Lib/http/cookies.py b/Lib/http/cookies.py +index 57791c6..d0a69cb 100644 +--- a/Lib/http/cookies.py ++++ b/Lib/http/cookies.py +@@ -87,9 +87,9 @@ within a string. Escaped quotation marks, nested semicolons, and other + such trickeries do not confuse it. + + >>> C = cookies.SimpleCookie() +- >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=\\012;";') ++ >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=;";') + >>> print(C) +- Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=\012;" ++ Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=;" + + Each element of the Cookie also supports all of the RFC 2109 + Cookie attributes. Here's an example which sets the Path +@@ -170,6 +170,15 @@ _Translator.update({ + }) + + _is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch ++_control_character_re = re.compile(r'[\x00-\x1F\x7F]') ++ ++ ++def _has_control_character(*val): ++ """Detects control characters within a value. ++ Supports any type, as header values can be any type. ++ """ ++ return any(_control_character_re.search(str(v)) for v in val) ++ + + def _quote(str): + r"""Quote a string for use in a cookie header. +@@ -292,12 +301,16 @@ class Morsel(dict): + K = K.lower() + if not K in self._reserved: + raise CookieError("Invalid attribute %r" % (K,)) ++ if _has_control_character(K, V): ++ raise CookieError(f"Control characters are not allowed in cookies {K!r} {V!r}") + dict.__setitem__(self, K, V) + + def setdefault(self, key, val=None): + key = key.lower() + if key not in self._reserved: + raise CookieError("Invalid attribute %r" % (key,)) ++ if _has_control_character(key, val): ++ raise CookieError("Control characters are not allowed in cookies %r %r" % (key, val,)) + return dict.setdefault(self, key, val) + + def __eq__(self, morsel): +@@ -333,6 +346,9 @@ class Morsel(dict): + raise CookieError('Attempt to set a reserved key %r' % (key,)) + if not _is_legal_key(key): + raise CookieError('Illegal key %r' % (key,)) ++ if _has_control_character(key, val, coded_val): ++ raise CookieError( ++ "Control characters are not allowed in cookies %r %r %r" % (key, val, coded_val,)) + + # It's a good key, so save it. + self._key = key +@@ -486,7 +502,10 @@ class BaseCookie(dict): + result = [] + items = sorted(self.items()) + for key, value in items: +- result.append(value.output(attrs, header)) ++ value_output = value.output(attrs, header) ++ if _has_control_character(value_output): ++ raise CookieError("Control characters are not allowed in cookies") ++ result.append(value_output) + return sep.join(result) + + __str__ = output +diff --git a/Lib/test/test_http_cookies.py b/Lib/test/test_http_cookies.py +index 7b3dc0f..f196bcc 100644 +--- a/Lib/test/test_http_cookies.py ++++ b/Lib/test/test_http_cookies.py +@@ -17,10 +17,10 @@ class CookieTests(unittest.TestCase): + 'repr': "", + 'output': 'Set-Cookie: chips=ahoy\nSet-Cookie: vienna=finger'}, + +- {'data': 'keebler="E=mc2; L=\\"Loves\\"; fudge=\\012;"', +- 'dict': {'keebler' : 'E=mc2; L="Loves"; fudge=\012;'}, +- 'repr': '''''', +- 'output': 'Set-Cookie: keebler="E=mc2; L=\\"Loves\\"; fudge=\\012;"'}, ++ {'data': 'keebler="E=mc2; L=\\"Loves\\"; fudge=;"', ++ 'dict': {'keebler' : 'E=mc2; L="Loves"; fudge=;'}, ++ 'repr': '''''', ++ 'output': 'Set-Cookie: keebler="E=mc2; L=\\"Loves\\"; fudge=;"'}, + + # Check illegal cookies that have an '=' char in an unquoted value + {'data': 'keebler=E=mc2', +@@ -563,6 +563,50 @@ class MorselTests(unittest.TestCase): + r'Set-Cookie: key=coded_val; ' + r'expires=\w+, \d+ \w+ \d+ \d+:\d+:\d+ \w+') + ++ def test_control_characters(self): ++ for c0 in support.control_characters_c0(): ++ morsel = cookies.Morsel() ++ ++ # .__setitem__() ++ with self.assertRaises(cookies.CookieError): ++ morsel[c0] = "val" ++ with self.assertRaises(cookies.CookieError): ++ morsel["path"] = c0 ++ ++ # .setdefault() ++ with self.assertRaises(cookies.CookieError): ++ morsel.setdefault("path", c0) ++ with self.assertRaises(cookies.CookieError): ++ morsel.setdefault(c0, "val") ++ ++ # .set() ++ with self.assertRaises(cookies.CookieError): ++ morsel.set(c0, "val", "coded-value") ++ with self.assertRaises(cookies.CookieError): ++ morsel.set("path", c0, "coded-value") ++ with self.assertRaises(cookies.CookieError): ++ morsel.set("path", "val", c0) ++ ++ def test_control_characters_output(self): ++ # Tests that even if the internals of Morsel are modified ++ # that a call to .output() has control character safeguards. ++ for c0 in support.control_characters_c0(): ++ morsel = cookies.Morsel() ++ morsel.set("key", "value", "coded-value") ++ morsel._key = c0 # Override private variable. ++ cookie = cookies.SimpleCookie() ++ cookie["cookie"] = morsel ++ with self.assertRaises(cookies.CookieError): ++ cookie.output() ++ ++ morsel = cookies.Morsel() ++ morsel.set("key", "value", "coded-value") ++ morsel._coded_value = c0 # Override private variable. ++ cookie = cookies.SimpleCookie() ++ cookie["cookie"] = morsel ++ with self.assertRaises(cookies.CookieError): ++ cookie.output() ++ + + def load_tests(loader, tests, pattern): + tests.addTest(doctest.DocTestSuite(cookies)) +diff --git a/Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst b/Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst +new file mode 100644 +index 0000000..788c3e4 +--- /dev/null ++++ b/Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst +@@ -0,0 +1 @@ ++Reject control characters in :class:`http.cookies.Morsel` fields and values. +-- +2.45.4 + diff --git a/SPECS/python3.14/CVE-2026-0865.patch b/SPECS/python3.14/CVE-2026-0865.patch new file mode 100644 index 00000000000..c19c80c2787 --- /dev/null +++ b/SPECS/python3.14/CVE-2026-0865.patch @@ -0,0 +1,102 @@ +From 3b17e0a6b6af91a844bd6b5725b7d6a806d36714 Mon Sep 17 00:00:00 2001 +From: "Gregory P. Smith" <68491+gpshead@users.noreply.github.com> +Date: Sat, 17 Jan 2026 10:23:57 -0800 +Subject: [PATCH] gh-143916: Reject control characters in + wsgiref.headers.Headers (GH-143917) (GH-143973) + +gh-143916: Reject control characters in wsgiref.headers.Headers (GH-143917) + +* Add 'test.support' fixture for C0 control characters +* gh-143916: Reject control characters in wsgiref.headers.Headers + +(cherry picked from commit f7fceed79ca1bceae8dbe5ba5bc8928564da7211) +(cherry picked from commit 22e4d55285cee52bc4dbe061324e5f30bd4dee58) + +Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com> +Co-authored-by: Seth Michael Larson +Signed-off-by: Azure Linux Security Servicing Account +Upstream-reference: https://github.com/python/cpython/pull/143974.patch +--- + Lib/test/support/__init__.py | 7 +++++++ + Lib/test/test_wsgiref.py | 12 +++++++++++- + Lib/wsgiref/headers.py | 3 +++ + .../2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst | 2 ++ + 4 files changed, 23 insertions(+), 1 deletion(-) + create mode 100644 Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst + +diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py +index ba57eb3..abd2fea 100644 +--- a/Lib/test/support/__init__.py ++++ b/Lib/test/support/__init__.py +@@ -2551,3 +2551,10 @@ class BrokenIter: + if self.iter_raises: + 1/0 + return self ++ ++ ++def control_characters_c0() -> list[str]: ++ """Returns a list of C0 control characters as strings. ++ C0 control characters defined as the byte range 0x00-0x1F, and 0x7F. ++ """ ++ return [chr(c) for c in range(0x00, 0x20)] + ["\x7F"] +diff --git a/Lib/test/test_wsgiref.py b/Lib/test/test_wsgiref.py +index 9316d0e..28e3656 100644 +--- a/Lib/test/test_wsgiref.py ++++ b/Lib/test/test_wsgiref.py +@@ -1,6 +1,6 @@ + from unittest import mock + from test import support +-from test.support import socket_helper ++from test.support import socket_helper, control_characters_c0 + from test.test_httpservers import NoLogRequestHandler + from unittest import TestCase + from wsgiref.util import setup_testing_defaults +@@ -503,6 +503,16 @@ class HeaderTests(TestCase): + '\r\n' + ) + ++ def testRaisesControlCharacters(self): ++ headers = Headers() ++ for c0 in control_characters_c0(): ++ self.assertRaises(ValueError, headers.__setitem__, f"key{c0}", "val") ++ self.assertRaises(ValueError, headers.__setitem__, "key", f"val{c0}") ++ self.assertRaises(ValueError, headers.add_header, f"key{c0}", "val", param="param") ++ self.assertRaises(ValueError, headers.add_header, "key", f"val{c0}", param="param") ++ self.assertRaises(ValueError, headers.add_header, "key", "val", param=f"param{c0}") ++ ++ + class ErrorHandler(BaseCGIHandler): + """Simple handler subclass for testing BaseHandler""" + +diff --git a/Lib/wsgiref/headers.py b/Lib/wsgiref/headers.py +index fab851c..fd98e85 100644 +--- a/Lib/wsgiref/headers.py ++++ b/Lib/wsgiref/headers.py +@@ -9,6 +9,7 @@ written by Barry Warsaw. + # existence of which force quoting of the parameter value. + import re + tspecials = re.compile(r'[ \(\)<>@,;:\\"/\[\]\?=]') ++_control_chars_re = re.compile(r'[\x00-\x1F\x7F]') + + def _formatparam(param, value=None, quote=1): + """Convenience function to format and return a key=value pair. +@@ -41,6 +42,8 @@ class Headers: + def _convert_string_type(self, value): + """Convert/check value type.""" + if type(value) is str: ++ if _control_chars_re.search(value): ++ raise ValueError("Control characters not allowed in headers") + return value + raise AssertionError("Header names/values must be" + " of type str (got {0})".format(repr(value))) +diff --git a/Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst b/Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst +new file mode 100644 +index 0000000..44bd0b2 +--- /dev/null ++++ b/Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst +@@ -0,0 +1,2 @@ ++Reject C0 control characters within wsgiref.headers.Headers fields, values, ++and parameters. +-- +2.45.4 + diff --git a/SPECS/python3.14/CVE-2026-1299.patch b/SPECS/python3.14/CVE-2026-1299.patch new file mode 100644 index 00000000000..2ee0d06e581 --- /dev/null +++ b/SPECS/python3.14/CVE-2026-1299.patch @@ -0,0 +1,110 @@ +From 8e35c877df664a0c424f4f7476d541d21a3b7288 Mon Sep 17 00:00:00 2001 +From: Seth Michael Larson +Date: Fri, 23 Jan 2026 08:59:35 -0600 +Subject: [PATCH] gh-144125: email: verify headers are sound in BytesGenerator + (cherry picked from commit 052e55e7d44718fe46cbba0ca995cb8fcc359413) + +Co-authored-by: Seth Michael Larson +Co-authored-by: Denis Ledoux +Co-authored-by: Denis Ledoux <5822488+beledouxdenis@users.noreply.github.com> +Co-authored-by: Petr Viktorin <302922+encukou@users.noreply.github.com> +Co-authored-by: Bas Bloemsaat <1586868+basbloemsaat@users.noreply.github.com> +Signed-off-by: Azure Linux Security Servicing Account +Upstream-reference: https://github.com/python/cpython/pull/144188.patch +--- + Lib/email/generator.py | 12 +++++++++++- + Lib/test/test_email/test_generator.py | 4 +++- + Lib/test/test_email/test_policy.py | 6 +++++- + .../2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst | 4 ++++ + 4 files changed, 23 insertions(+), 3 deletions(-) + create mode 100644 Misc/NEWS.d/next/Security/2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst + +diff --git a/Lib/email/generator.py b/Lib/email/generator.py +index 47b9df8..8cbc43e 100644 +--- a/Lib/email/generator.py ++++ b/Lib/email/generator.py +@@ -22,6 +22,7 @@ NL = '\n' # XXX: no longer used by the code below. + NLCRE = re.compile(r'\r\n|\r|\n') + fcre = re.compile(r'^From ', re.MULTILINE) + NEWLINE_WITHOUT_FWSP = re.compile(r'\r\n[^ \t]|\r[^ \n\t]|\n[^ \t]') ++NEWLINE_WITHOUT_FWSP_BYTES = re.compile(br'\r\n[^ \t]|\r[^ \n\t]|\n[^ \t]') + + + class Generator: +@@ -429,7 +430,16 @@ class BytesGenerator(Generator): + # This is almost the same as the string version, except for handling + # strings with 8bit bytes. + for h, v in msg.raw_items(): +- self._fp.write(self.policy.fold_binary(h, v)) ++ folded = self.policy.fold_binary(h, v) ++ if self.policy.verify_generated_headers: ++ linesep = self.policy.linesep.encode() ++ if not folded.endswith(linesep): ++ raise HeaderWriteError( ++ f'folded header does not end with {linesep!r}: {folded!r}') ++ if NEWLINE_WITHOUT_FWSP_BYTES.search(folded.removesuffix(linesep)): ++ raise HeaderWriteError( ++ f'folded header contains newline: {folded!r}') ++ self._fp.write(folded) + # A blank line always separates headers from body + self.write(self._NL) + +diff --git a/Lib/test/test_email/test_generator.py b/Lib/test/test_email/test_generator.py +index c75a842..3ca79ed 100644 +--- a/Lib/test/test_email/test_generator.py ++++ b/Lib/test/test_email/test_generator.py +@@ -313,7 +313,7 @@ class TestGenerator(TestGeneratorBase, TestEmailBase): + self.assertEqual(s.getvalue(), self.typ(expected)) + + def test_verify_generated_headers(self): +- """gh-121650: by default the generator prevents header injection""" ++ # gh-121650: by default the generator prevents header injection + class LiteralHeader(str): + name = 'Header' + def fold(self, **kwargs): +@@ -334,6 +334,8 @@ class TestGenerator(TestGeneratorBase, TestEmailBase): + + with self.assertRaises(email.errors.HeaderWriteError): + message.as_string() ++ with self.assertRaises(email.errors.HeaderWriteError): ++ message.as_bytes() + + + class TestBytesGenerator(TestGeneratorBase, TestEmailBase): +diff --git a/Lib/test/test_email/test_policy.py b/Lib/test/test_email/test_policy.py +index baa35fd..71ec0fe 100644 +--- a/Lib/test/test_email/test_policy.py ++++ b/Lib/test/test_email/test_policy.py +@@ -296,7 +296,7 @@ class PolicyAPITests(unittest.TestCase): + policy.fold("Subject", subject) + + def test_verify_generated_headers(self): +- """Turning protection off allows header injection""" ++ # Turning protection off allows header injection + policy = email.policy.default.clone(verify_generated_headers=False) + for text in ( + 'Header: Value\r\nBad: Injection\r\n', +@@ -319,6 +319,10 @@ class PolicyAPITests(unittest.TestCase): + message.as_string(), + f"{text}\nBody", + ) ++ self.assertEqual( ++ message.as_bytes(), ++ f"{text}\nBody".encode(), ++ ) + + # XXX: Need subclassing tests. + # For adding subclassed objects, make sure the usual rules apply (subclass +diff --git a/Misc/NEWS.d/next/Security/2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst b/Misc/NEWS.d/next/Security/2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst +new file mode 100644 +index 0000000..e6333e7 +--- /dev/null ++++ b/Misc/NEWS.d/next/Security/2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst +@@ -0,0 +1,4 @@ ++:mod:`~email.generator.BytesGenerator` will now refuse to serialize (write) headers ++that are unsafely folded or delimited; see ++:attr:`~email.policy.Policy.verify_generated_headers`. (Contributed by Bas ++Bloemsaat and Petr Viktorin in :gh:`121650`). +-- +2.45.4 + diff --git a/SPECS/python3.14/CVE-2026-4519.patch b/SPECS/python3.14/CVE-2026-4519.patch new file mode 100644 index 00000000000..9225b700d89 --- /dev/null +++ b/SPECS/python3.14/CVE-2026-4519.patch @@ -0,0 +1,132 @@ +From 64423d38b3b27b8f6cb479c554138b6a4d94830a Mon Sep 17 00:00:00 2001 +From: Seth Michael Larson +Date: Fri, 20 Mar 2026 09:47:13 -0500 +Subject: [PATCH 1/2] gh-143930: Reject leading dashes in webbrowser URLs + (cherry picked from commit 82a24a4442312bdcfc4c799885e8b3e00990f02b) + +--- + Lib/test/test_webbrowser.py | 5 +++++ + Lib/webbrowser.py | 12 ++++++++++++ + .../2026-01-16-12-04-49.gh-issue-143930.zYC5x3.rst | 1 + + 3 files changed, 18 insertions(+) + create mode 100644 Misc/NEWS.d/next/Security/2026-01-16-12-04-49.gh-issue-143930.zYC5x3.rst + +diff --git a/Lib/test/test_webbrowser.py b/Lib/test/test_webbrowser.py +index 2d695bc..60f094f 100644 +--- a/Lib/test/test_webbrowser.py ++++ b/Lib/test/test_webbrowser.py +@@ -59,6 +59,11 @@ class GenericBrowserCommandTest(CommandTestMixin, unittest.TestCase): + options=[], + arguments=[URL]) + ++ def test_reject_dash_prefixes(self): ++ browser = self.browser_class(name=CMD_NAME) ++ with self.assertRaises(ValueError): ++ browser.open(f"--key=val {URL}") ++ + + class BackgroundBrowserCommandTest(CommandTestMixin, unittest.TestCase): + +diff --git a/Lib/webbrowser.py b/Lib/webbrowser.py +index 13b9e85..ab66519 100755 +--- a/Lib/webbrowser.py ++++ b/Lib/webbrowser.py +@@ -158,6 +158,12 @@ class BaseBrowser(object): + def open_new_tab(self, url): + return self.open(url, 2) + ++ @staticmethod ++ def _check_url(url): ++ """Ensures that the URL is safe to pass to subprocesses as a parameter""" ++ if url and url.lstrip().startswith("-"): ++ raise ValueError(f"Invalid URL {url!r}: URLs must not start with '-' after leading whitespace") ++ + + class GenericBrowser(BaseBrowser): + """Class for all browsers started with a command +@@ -175,6 +181,7 @@ class GenericBrowser(BaseBrowser): + + def open(self, url, new=0, autoraise=True): + sys.audit("webbrowser.open", url) ++ self._check_url(url) + cmdline = [self.name] + [arg.replace("%s", url) + for arg in self.args] + try: +@@ -195,6 +202,7 @@ class BackgroundBrowser(GenericBrowser): + cmdline = [self.name] + [arg.replace("%s", url) + for arg in self.args] + sys.audit("webbrowser.open", url) ++ self._check_url(url) + try: + if sys.platform[:3] == 'win': + p = subprocess.Popen(cmdline) +@@ -260,6 +268,7 @@ class UnixBrowser(BaseBrowser): + + def open(self, url, new=0, autoraise=True): + sys.audit("webbrowser.open", url) ++ self._check_url(url) + if new == 0: + action = self.remote_action + elif new == 1: +@@ -350,6 +359,7 @@ class Konqueror(BaseBrowser): + + def open(self, url, new=0, autoraise=True): + sys.audit("webbrowser.open", url) ++ self._check_url(url) + # XXX Currently I know no way to prevent KFM from opening a new win. + if new == 2: + action = "newTab" +@@ -554,6 +564,7 @@ if sys.platform[:3] == "win": + class WindowsDefault(BaseBrowser): + def open(self, url, new=0, autoraise=True): + sys.audit("webbrowser.open", url) ++ self._check_url(url) + try: + os.startfile(url) + except OSError: +@@ -638,6 +649,7 @@ if sys.platform == 'darwin': + + def open(self, url, new=0, autoraise=True): + sys.audit("webbrowser.open", url) ++ self._check_url(url) + if self.name == 'default': + script = 'open location "%s"' % url.replace('"', '%22') # opens in default browser + else: +diff --git a/Misc/NEWS.d/next/Security/2026-01-16-12-04-49.gh-issue-143930.zYC5x3.rst b/Misc/NEWS.d/next/Security/2026-01-16-12-04-49.gh-issue-143930.zYC5x3.rst +new file mode 100644 +index 0000000..0f27eae +--- /dev/null ++++ b/Misc/NEWS.d/next/Security/2026-01-16-12-04-49.gh-issue-143930.zYC5x3.rst +@@ -0,0 +1 @@ ++Reject leading dashes in URLs passed to :func:`webbrowser.open` +-- +2.45.4 + + +From 83c608b5ece9ad2aa88866dee9b52f8895156671 Mon Sep 17 00:00:00 2001 +From: Pinky +Date: Wed, 25 Mar 2026 00:01:36 +0530 +Subject: [PATCH 2/2] Simplify error message for invalid URL- backport + +Signed-off-by: Azure Linux Security Servicing Account +Upstream-reference: https://github.com/python/cpython/pull/146360.patch +--- + Lib/webbrowser.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/Lib/webbrowser.py b/Lib/webbrowser.py +index ab66519..0bdb644 100755 +--- a/Lib/webbrowser.py ++++ b/Lib/webbrowser.py +@@ -162,7 +162,7 @@ class BaseBrowser(object): + def _check_url(url): + """Ensures that the URL is safe to pass to subprocesses as a parameter""" + if url and url.lstrip().startswith("-"): +- raise ValueError(f"Invalid URL {url!r}: URLs must not start with '-' after leading whitespace") ++ raise ValueError(f"Invalid URL: {url}") + + + class GenericBrowser(BaseBrowser): +-- +2.45.4 + diff --git a/SPECS/python3.14/python3.14.signatures.json b/SPECS/python3.14/python3.14.signatures.json new file mode 100644 index 00000000000..0cd00f62eed --- /dev/null +++ b/SPECS/python3.14/python3.14.signatures.json @@ -0,0 +1,6 @@ +{ + "Signatures": { + "pathfix.py": "7a2ff222346d3c95b08814e3372975823e099c17dddaa73a459a3d840e6e9c1b", + "Python-3.14.4.tar.xz": "d923c51303e38e249136fc1bdf3568d56ecb03214efdef48516176d3d7faaef8" + } +} diff --git a/SPECS/python3.14/python3.14.spec b/SPECS/python3.14/python3.14.spec new file mode 100644 index 00000000000..1e72ec50eb6 --- /dev/null +++ b/SPECS/python3.14/python3.14.spec @@ -0,0 +1,226 @@ +%global openssl_flags -DOPENSSL_NO_SSL3 -DOPENSSL_NO_SSL2 -DOPENSSL_NO_COMP +%global __brp_python_bytecompile %{nil} +%define majmin %(echo %{version} | cut -d. -f1-2) +%define majmin_nodots %(echo %{majmin} | tr -d .) + +Summary: A high-level scripting language (version 3.14) +Name: python3.14 +Version: 3.14.4 +Release: 1%{?dist} +License: PSF +Vendor: Microsoft Corporation +Distribution: Azure Linux +Group: System Environment/Programming +URL: https://www.python.org/ +Source0: https://www.python.org/ftp/python/%{version}/Python-%{version}.tar.xz +# pathfix.py was provided by the previous Python source bundle (Python-3.9.14.tar.xz) +# It has been removed in later source bundles, but as our packages still require it, we will still provide for now. +Source1: https://github.com/python/cpython/blob/3.9/Tools/scripts/pathfix.py +Patch0: CVE-2026-0672.patch +Patch1: CVE-2026-0865.patch +Patch2: CVE-2026-1299.patch +Patch3: CVE-2026-4519.patch + +BuildRequires: bzip2-devel +BuildRequires: expat-devel >= 2.1.0 +BuildRequires: libffi-devel >= 3.0.13 +BuildRequires: ncurses-devel +BuildRequires: openssl-devel +BuildRequires: pkg-config >= 0.28 +BuildRequires: readline-devel +BuildRequires: sqlite-devel +BuildRequires: xz-devel +Requires: ncurses +Requires: openssl +Requires: %{name}-libs = %{version}-%{release} +Requires: readline +Requires: xz +# Only versioned provides. The unversioned "python", "python3", "/bin/python", +# "/bin/python3" names remain owned by the default "python3" package so that +# python3.14 installs strictly side-by-side. +Provides: python(abi) = %{majmin} +Provides: %{name}-docs = %{version}-%{release} +Provides: python%{majmin_nodots} = %{version}-%{release} + +%if 0%{?with_check} +BuildRequires: iana-etc +BuildRequires: tzdata +%endif + +%description +Python 3.14 installed side-by-side with the system python3. The interpreter +is available as /usr/bin/python3.14 and the standard library lives under +/usr/lib/python3.14/. This package does not change the system default python3. + +%package libs +Summary: The libraries for python 3.14 runtime +Group: Applications/System +Requires: bzip2-libs +Requires: expat >= 2.1.0 +Requires: libffi >= 3.0.13 +Requires: ncurses +Requires: sqlite-libs +Provides: %{name}-xml = %{version}-%{release} +Provides: python%{majmin_nodots}-libs = %{version}-%{release} + +%description libs +The python interpreter can be embedded into applications wanting to +use python as an embedded scripting language. The python3.14-libs package +provides the libraries needed for python 3.14 applications. + +%package curses +Summary: Python 3.14 module interface for NCurses Library +Group: Applications/System +Requires: ncurses +Requires: %{name}-libs = %{version}-%{release} +Provides: python%{majmin_nodots}-curses = %{version}-%{release} + +%description curses +The python3.14-curses package provides interface for ncurses library. + +%package devel +Summary: The libraries and header files needed for Python 3.14 development. +Group: Development/Libraries +Requires: expat-devel >= 2.1.0 +Requires: %{name} = %{version}-%{release} +Provides: python%{majmin_nodots}-devel = %{version}-%{release} + +%description devel +The Python programming language's interpreter can be extended with +dynamically loaded extensions and can be embedded in other programs. +This package contains the header files and libraries needed to do +these types of tasks against Python 3.14. + +%package test +Summary: Regression tests package for Python 3.14. +Group: Development/Tools +Requires: %{name} = %{version}-%{release} +Provides: python%{majmin_nodots}-test = %{version}-%{release} + +%description test +The test package contains all regression tests for Python 3.14 as well as the +modules test.support and test.regrtest. test.support is used to enhance your +tests while test.regrtest drives the testing suite. + +%prep +%autosetup -p1 -n Python-%{version} + +%build +# Remove GCC specs and build environment linker scripts +# from the flags used when compiling outside of an RPM environment +# https://fedoraproject.org/wiki/Changes/Python_Extension_Flags +export CFLAGS="%{extension_cflags} %{openssl_flags}" +export CFLAGS_NODIST="%{build_cflags} %{openssl_flags}" +export CXXFLAGS="%{extension_cxxflags} %{openssl_flags}" +export LDFLAGS="%{extension_ldflags}" +export LDFLAGS_NODIST="%{build_ldflags}" +export OPT="%{extension_cflags} %{openssl_flags}" + +%configure \ + --enable-shared \ + --with-platlibdir=%{_lib} \ + --with-system-expat \ + --with-system-ffi \ + --with-dbmliborder=gdbm:ndbm \ + --with-ensurepip=no \ + --enable-optimizations +%make_build + +%install +%make_install +%{_fixperms} %{buildroot}/* + +# Windows executables get installed by pip - we don't need these. +find %{buildroot}%{_libdir}/python%{majmin}/site-packages -name '*.exe' -delete -print + +# Install pathfix.py to bindir under the versioned name only. +# The unversioned /usr/bin/pathfix.py is owned by the default python3 package. +cp -pv %{SOURCE1} %{buildroot}%{_bindir}/pathfix%{majmin}.py + +# Remove unversioned filesystem entries that belong to the default python3 +# (3.12) package. Python 3.14 installs strictly side-by-side under versioned +# paths, so these unversioned symlinks / stable-ABI files must not be shipped +# by this package. +rm -f %{buildroot}%{_bindir}/python3 +rm -f %{buildroot}%{_bindir}/python3-config +rm -f %{buildroot}%{_bindir}/pydoc3 +rm -f %{buildroot}%{_bindir}/idle3 +rm -f %{buildroot}%{_libdir}/libpython3.so +rm -f %{buildroot}%{_libdir}/pkgconfig/python3.pc +rm -f %{buildroot}%{_libdir}/pkgconfig/python3-embed.pc +rm -f %{buildroot}%{_mandir}/man1/python3.1* + +# Remove unused stuff +find %{buildroot}%{_libdir} -name '*.pyc' -delete +find %{buildroot}%{_libdir} -name '*.pyo' -delete +find %{buildroot}%{_libdir} -name '*.o' -delete +rm -rf %{buildroot}%{_bindir}/__pycache__ + +%check +# vsock_loopback module needed by `test_socket` is not loaded by default in AzureLinux. +%{buildroot}%{_bindir}/python%{majmin} -m test --exclude test_socket + +%ldconfig_scriptlets + +%files +%defattr(-, root, root) +%license LICENSE +%doc README.rst +%{_bindir}/pydoc%{majmin} +%{_bindir}/python%{majmin} +%{_mandir}/man1/python%{majmin}.1* + +%dir %{_libdir}/python%{majmin} +%dir %{_libdir}/python%{majmin}/site-packages + +%exclude %{_libdir}/python%{majmin}/test/test_ctypes +%exclude %{_libdir}/python%{majmin}/test/test_sqlite3 +%exclude %{_libdir}/python%{majmin}/idlelib/idle_test +%exclude %{_libdir}/python%{majmin}/test +%exclude %{_libdir}/python%{majmin}/lib-dynload/_ctypes_test.*.so + +%files libs +%defattr(-,root,root) +%license LICENSE +%doc README.rst +%{_libdir}/libpython%{majmin}.so.1.0 +%{_libdir}/python%{majmin} +%exclude %{_libdir}/python%{majmin}/site-packages/ +%exclude %{_libdir}/python%{majmin}/test/test_ctypes +%exclude %{_libdir}/python%{majmin}/test/test_sqlite3 +%exclude %{_libdir}/python%{majmin}/idlelib/idle_test +%exclude %{_libdir}/python%{majmin}/test +%exclude %{_libdir}/python%{majmin}/lib-dynload/_ctypes_test.*.so +%exclude %{_libdir}/python%{majmin}/curses +%exclude %{_libdir}/python%{majmin}/lib-dynload/_curses*.so + +%files curses +%{_libdir}/python%{majmin}/curses/* +%{_libdir}/python%{majmin}/lib-dynload/_curses*.so + +%files devel +%defattr(-,root,root) +%{_includedir}/* +%{_libdir}/pkgconfig/python-%{majmin}.pc +%{_libdir}/pkgconfig/python-%{majmin}-embed.pc +%{_libdir}/libpython%{majmin}.so +%{_bindir}/python%{majmin}-config +%{_bindir}/pathfix%{majmin}.py +%doc Misc/README.valgrind Misc/valgrind-python.supp +%exclude %{_bindir}/idle* + +%files test +%{_libdir}/python%{majmin}/test/* + +%changelog +* Thu Apr 23 2026 Ihar Voitka - 3.14.4-1 +- Original version for Azure Linux. +- License verified. +- Side-by-side with the default python3 (3.12). Ships only versioned paths + (/usr/bin/python3.14, /usr/lib/python3.14/, libpython3.14.so.1.0). +- Carries post-3.14.4 CVE patches CVE-2026-0672, CVE-2026-0865, CVE-2026-1299, + CVE-2026-4519. All prior CVE-2025-* patches on python3-3.12 are upstream in + 3.14.4; cgi3.patch is dropped because the cgi module was removed in 3.13 + (PEP 594). +- Drops the lib2to3-based tools subpackage since lib2to3 and /usr/bin/2to3 + were removed in Python 3.13. diff --git a/cgmanifest.json b/cgmanifest.json index e0572d462b7..01000574585 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -25848,6 +25848,16 @@ } } }, + { + "component": { + "type": "other", + "other": { + "name": "python3.14", + "version": "3.14.4", + "downloadUrl": "https://www.python.org/ftp/python/3.14.4/Python-3.14.4.tar.xz" + } + } + }, { "component": { "type": "other", From 15ccce43c82fdec6db47d717c391e0cdf7fb335f Mon Sep 17 00:00:00 2001 From: Ihar Voitka Date: Thu, 23 Apr 2026 19:42:21 -0700 Subject: [PATCH 2/5] Fix license attribution changelog wording for python3.14 Switch the %changelog %changelog boilerplate from "Original version for Azure Linux" (which spec_source_attributions classifies as the "Microsoft" origin) to "Initial Azure Linux import from the source project (license: same as \"License\" tag)." so it matches the "Source project" regex and aligns with the licenses.json / LICENSES-MAP.md entry added for python3.14. Reported by the "Spec License Map Check" CI job on PR #16856. Co-Authored-By: Claude Opus 4.7 (1M context) --- SPECS/python3.14/python3.14.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SPECS/python3.14/python3.14.spec b/SPECS/python3.14/python3.14.spec index 1e72ec50eb6..dd5fa59a03f 100644 --- a/SPECS/python3.14/python3.14.spec +++ b/SPECS/python3.14/python3.14.spec @@ -214,7 +214,7 @@ rm -rf %{buildroot}%{_bindir}/__pycache__ %changelog * Thu Apr 23 2026 Ihar Voitka - 3.14.4-1 -- Original version for Azure Linux. +- Initial Azure Linux import from the source project (license: same as "License" tag). - License verified. - Side-by-side with the default python3 (3.12). Ships only versioned paths (/usr/bin/python3.14, /usr/lib/python3.14/, libpython3.14.so.1.0). From 10ed2bc0ca141d2544b832b165b32a9162ed785b Mon Sep 17 00:00:00 2001 From: Ihar Voitka Date: Thu, 23 Apr 2026 19:49:43 -0700 Subject: [PATCH 3/5] =?UTF-8?q?Drop=20all=20carried=20CVE=20patches=20from?= =?UTF-8?q?=20python3.14=20=E2=80=94=20upstream=20in=203.14.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified against python/cpython that every CVE patch on the 3.12 fork has a 3.14-branch backport PR that merged before 3.14.4's release on 2026-04-07: CVE-2026-0672 → PR python/cpython#144089 (merged 2026-01-23) CVE-2026-0865 → PRs python/cpython#143972 + #144761 (merged 2026-01-17 / 02-21) CVE-2026-1299 → PR python/cpython#144182 (merged 2026-01-25) CVE-2026-4519 → PRs python/cpython#146214 + #148042 (merged 2026-03-23 / 04-03) The CVE-2025-* patches are even older and were already in 3.14.0 (GA 2025-10). Carrying patches we don't need adds hunk-maintenance cost, rebase risk, and auditor confusion with no upside. Co-Authored-By: Claude Opus 4.7 (1M context) --- SPECS/python3.14/CVE-2026-0672.patch | 189 --------------------------- SPECS/python3.14/CVE-2026-0865.patch | 102 --------------- SPECS/python3.14/CVE-2026-1299.patch | 110 ---------------- SPECS/python3.14/CVE-2026-4519.patch | 132 ------------------- SPECS/python3.14/python3.14.spec | 12 +- 5 files changed, 4 insertions(+), 541 deletions(-) delete mode 100644 SPECS/python3.14/CVE-2026-0672.patch delete mode 100644 SPECS/python3.14/CVE-2026-0865.patch delete mode 100644 SPECS/python3.14/CVE-2026-1299.patch delete mode 100644 SPECS/python3.14/CVE-2026-4519.patch diff --git a/SPECS/python3.14/CVE-2026-0672.patch b/SPECS/python3.14/CVE-2026-0672.patch deleted file mode 100644 index 304e7d5dc92..00000000000 --- a/SPECS/python3.14/CVE-2026-0672.patch +++ /dev/null @@ -1,189 +0,0 @@ -From 62498dced866fee86727379378acb20a541f3371 Mon Sep 17 00:00:00 2001 -From: Seth Michael Larson -Date: Tue, 20 Jan 2026 15:23:42 -0600 -Subject: [PATCH] gh-143919: Reject control characters in http cookies (cherry - picked from commit 95746b3a13a985787ef53b977129041971ed7f70) -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit - -Co-authored-by: Seth Michael Larson -Co-authored-by: Bartosz Sławecki -Co-authored-by: sobolevn -Signed-off-by: Azure Linux Security Servicing Account -Upstream-reference: https://github.com/python/cpython/pull/144091.patch ---- - Doc/library/http.cookies.rst | 4 +- - Lib/http/cookies.py | 25 +++++++-- - Lib/test/test_http_cookies.py | 52 +++++++++++++++++-- - ...-01-16-11-13-15.gh-issue-143919.kchwZV.rst | 1 + - 4 files changed, 73 insertions(+), 9 deletions(-) - create mode 100644 Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst - -diff --git a/Doc/library/http.cookies.rst b/Doc/library/http.cookies.rst -index ad37a0f..317a71a 100644 ---- a/Doc/library/http.cookies.rst -+++ b/Doc/library/http.cookies.rst -@@ -272,9 +272,9 @@ The following example demonstrates how to use the :mod:`http.cookies` module. - Set-Cookie: chips=ahoy - Set-Cookie: vienna=finger - >>> C = cookies.SimpleCookie() -- >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=\\012;";') -+ >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=;";') - >>> print(C) -- Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=\012;" -+ Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=;" - >>> C = cookies.SimpleCookie() - >>> C["oreo"] = "doublestuff" - >>> C["oreo"]["path"] = "/" -diff --git a/Lib/http/cookies.py b/Lib/http/cookies.py -index 57791c6..d0a69cb 100644 ---- a/Lib/http/cookies.py -+++ b/Lib/http/cookies.py -@@ -87,9 +87,9 @@ within a string. Escaped quotation marks, nested semicolons, and other - such trickeries do not confuse it. - - >>> C = cookies.SimpleCookie() -- >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=\\012;";') -+ >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=;";') - >>> print(C) -- Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=\012;" -+ Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=;" - - Each element of the Cookie also supports all of the RFC 2109 - Cookie attributes. Here's an example which sets the Path -@@ -170,6 +170,15 @@ _Translator.update({ - }) - - _is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch -+_control_character_re = re.compile(r'[\x00-\x1F\x7F]') -+ -+ -+def _has_control_character(*val): -+ """Detects control characters within a value. -+ Supports any type, as header values can be any type. -+ """ -+ return any(_control_character_re.search(str(v)) for v in val) -+ - - def _quote(str): - r"""Quote a string for use in a cookie header. -@@ -292,12 +301,16 @@ class Morsel(dict): - K = K.lower() - if not K in self._reserved: - raise CookieError("Invalid attribute %r" % (K,)) -+ if _has_control_character(K, V): -+ raise CookieError(f"Control characters are not allowed in cookies {K!r} {V!r}") - dict.__setitem__(self, K, V) - - def setdefault(self, key, val=None): - key = key.lower() - if key not in self._reserved: - raise CookieError("Invalid attribute %r" % (key,)) -+ if _has_control_character(key, val): -+ raise CookieError("Control characters are not allowed in cookies %r %r" % (key, val,)) - return dict.setdefault(self, key, val) - - def __eq__(self, morsel): -@@ -333,6 +346,9 @@ class Morsel(dict): - raise CookieError('Attempt to set a reserved key %r' % (key,)) - if not _is_legal_key(key): - raise CookieError('Illegal key %r' % (key,)) -+ if _has_control_character(key, val, coded_val): -+ raise CookieError( -+ "Control characters are not allowed in cookies %r %r %r" % (key, val, coded_val,)) - - # It's a good key, so save it. - self._key = key -@@ -486,7 +502,10 @@ class BaseCookie(dict): - result = [] - items = sorted(self.items()) - for key, value in items: -- result.append(value.output(attrs, header)) -+ value_output = value.output(attrs, header) -+ if _has_control_character(value_output): -+ raise CookieError("Control characters are not allowed in cookies") -+ result.append(value_output) - return sep.join(result) - - __str__ = output -diff --git a/Lib/test/test_http_cookies.py b/Lib/test/test_http_cookies.py -index 7b3dc0f..f196bcc 100644 ---- a/Lib/test/test_http_cookies.py -+++ b/Lib/test/test_http_cookies.py -@@ -17,10 +17,10 @@ class CookieTests(unittest.TestCase): - 'repr': "", - 'output': 'Set-Cookie: chips=ahoy\nSet-Cookie: vienna=finger'}, - -- {'data': 'keebler="E=mc2; L=\\"Loves\\"; fudge=\\012;"', -- 'dict': {'keebler' : 'E=mc2; L="Loves"; fudge=\012;'}, -- 'repr': '''''', -- 'output': 'Set-Cookie: keebler="E=mc2; L=\\"Loves\\"; fudge=\\012;"'}, -+ {'data': 'keebler="E=mc2; L=\\"Loves\\"; fudge=;"', -+ 'dict': {'keebler' : 'E=mc2; L="Loves"; fudge=;'}, -+ 'repr': '''''', -+ 'output': 'Set-Cookie: keebler="E=mc2; L=\\"Loves\\"; fudge=;"'}, - - # Check illegal cookies that have an '=' char in an unquoted value - {'data': 'keebler=E=mc2', -@@ -563,6 +563,50 @@ class MorselTests(unittest.TestCase): - r'Set-Cookie: key=coded_val; ' - r'expires=\w+, \d+ \w+ \d+ \d+:\d+:\d+ \w+') - -+ def test_control_characters(self): -+ for c0 in support.control_characters_c0(): -+ morsel = cookies.Morsel() -+ -+ # .__setitem__() -+ with self.assertRaises(cookies.CookieError): -+ morsel[c0] = "val" -+ with self.assertRaises(cookies.CookieError): -+ morsel["path"] = c0 -+ -+ # .setdefault() -+ with self.assertRaises(cookies.CookieError): -+ morsel.setdefault("path", c0) -+ with self.assertRaises(cookies.CookieError): -+ morsel.setdefault(c0, "val") -+ -+ # .set() -+ with self.assertRaises(cookies.CookieError): -+ morsel.set(c0, "val", "coded-value") -+ with self.assertRaises(cookies.CookieError): -+ morsel.set("path", c0, "coded-value") -+ with self.assertRaises(cookies.CookieError): -+ morsel.set("path", "val", c0) -+ -+ def test_control_characters_output(self): -+ # Tests that even if the internals of Morsel are modified -+ # that a call to .output() has control character safeguards. -+ for c0 in support.control_characters_c0(): -+ morsel = cookies.Morsel() -+ morsel.set("key", "value", "coded-value") -+ morsel._key = c0 # Override private variable. -+ cookie = cookies.SimpleCookie() -+ cookie["cookie"] = morsel -+ with self.assertRaises(cookies.CookieError): -+ cookie.output() -+ -+ morsel = cookies.Morsel() -+ morsel.set("key", "value", "coded-value") -+ morsel._coded_value = c0 # Override private variable. -+ cookie = cookies.SimpleCookie() -+ cookie["cookie"] = morsel -+ with self.assertRaises(cookies.CookieError): -+ cookie.output() -+ - - def load_tests(loader, tests, pattern): - tests.addTest(doctest.DocTestSuite(cookies)) -diff --git a/Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst b/Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst -new file mode 100644 -index 0000000..788c3e4 ---- /dev/null -+++ b/Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst -@@ -0,0 +1 @@ -+Reject control characters in :class:`http.cookies.Morsel` fields and values. --- -2.45.4 - diff --git a/SPECS/python3.14/CVE-2026-0865.patch b/SPECS/python3.14/CVE-2026-0865.patch deleted file mode 100644 index c19c80c2787..00000000000 --- a/SPECS/python3.14/CVE-2026-0865.patch +++ /dev/null @@ -1,102 +0,0 @@ -From 3b17e0a6b6af91a844bd6b5725b7d6a806d36714 Mon Sep 17 00:00:00 2001 -From: "Gregory P. Smith" <68491+gpshead@users.noreply.github.com> -Date: Sat, 17 Jan 2026 10:23:57 -0800 -Subject: [PATCH] gh-143916: Reject control characters in - wsgiref.headers.Headers (GH-143917) (GH-143973) - -gh-143916: Reject control characters in wsgiref.headers.Headers (GH-143917) - -* Add 'test.support' fixture for C0 control characters -* gh-143916: Reject control characters in wsgiref.headers.Headers - -(cherry picked from commit f7fceed79ca1bceae8dbe5ba5bc8928564da7211) -(cherry picked from commit 22e4d55285cee52bc4dbe061324e5f30bd4dee58) - -Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com> -Co-authored-by: Seth Michael Larson -Signed-off-by: Azure Linux Security Servicing Account -Upstream-reference: https://github.com/python/cpython/pull/143974.patch ---- - Lib/test/support/__init__.py | 7 +++++++ - Lib/test/test_wsgiref.py | 12 +++++++++++- - Lib/wsgiref/headers.py | 3 +++ - .../2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst | 2 ++ - 4 files changed, 23 insertions(+), 1 deletion(-) - create mode 100644 Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst - -diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py -index ba57eb3..abd2fea 100644 ---- a/Lib/test/support/__init__.py -+++ b/Lib/test/support/__init__.py -@@ -2551,3 +2551,10 @@ class BrokenIter: - if self.iter_raises: - 1/0 - return self -+ -+ -+def control_characters_c0() -> list[str]: -+ """Returns a list of C0 control characters as strings. -+ C0 control characters defined as the byte range 0x00-0x1F, and 0x7F. -+ """ -+ return [chr(c) for c in range(0x00, 0x20)] + ["\x7F"] -diff --git a/Lib/test/test_wsgiref.py b/Lib/test/test_wsgiref.py -index 9316d0e..28e3656 100644 ---- a/Lib/test/test_wsgiref.py -+++ b/Lib/test/test_wsgiref.py -@@ -1,6 +1,6 @@ - from unittest import mock - from test import support --from test.support import socket_helper -+from test.support import socket_helper, control_characters_c0 - from test.test_httpservers import NoLogRequestHandler - from unittest import TestCase - from wsgiref.util import setup_testing_defaults -@@ -503,6 +503,16 @@ class HeaderTests(TestCase): - '\r\n' - ) - -+ def testRaisesControlCharacters(self): -+ headers = Headers() -+ for c0 in control_characters_c0(): -+ self.assertRaises(ValueError, headers.__setitem__, f"key{c0}", "val") -+ self.assertRaises(ValueError, headers.__setitem__, "key", f"val{c0}") -+ self.assertRaises(ValueError, headers.add_header, f"key{c0}", "val", param="param") -+ self.assertRaises(ValueError, headers.add_header, "key", f"val{c0}", param="param") -+ self.assertRaises(ValueError, headers.add_header, "key", "val", param=f"param{c0}") -+ -+ - class ErrorHandler(BaseCGIHandler): - """Simple handler subclass for testing BaseHandler""" - -diff --git a/Lib/wsgiref/headers.py b/Lib/wsgiref/headers.py -index fab851c..fd98e85 100644 ---- a/Lib/wsgiref/headers.py -+++ b/Lib/wsgiref/headers.py -@@ -9,6 +9,7 @@ written by Barry Warsaw. - # existence of which force quoting of the parameter value. - import re - tspecials = re.compile(r'[ \(\)<>@,;:\\"/\[\]\?=]') -+_control_chars_re = re.compile(r'[\x00-\x1F\x7F]') - - def _formatparam(param, value=None, quote=1): - """Convenience function to format and return a key=value pair. -@@ -41,6 +42,8 @@ class Headers: - def _convert_string_type(self, value): - """Convert/check value type.""" - if type(value) is str: -+ if _control_chars_re.search(value): -+ raise ValueError("Control characters not allowed in headers") - return value - raise AssertionError("Header names/values must be" - " of type str (got {0})".format(repr(value))) -diff --git a/Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst b/Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst -new file mode 100644 -index 0000000..44bd0b2 ---- /dev/null -+++ b/Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst -@@ -0,0 +1,2 @@ -+Reject C0 control characters within wsgiref.headers.Headers fields, values, -+and parameters. --- -2.45.4 - diff --git a/SPECS/python3.14/CVE-2026-1299.patch b/SPECS/python3.14/CVE-2026-1299.patch deleted file mode 100644 index 2ee0d06e581..00000000000 --- a/SPECS/python3.14/CVE-2026-1299.patch +++ /dev/null @@ -1,110 +0,0 @@ -From 8e35c877df664a0c424f4f7476d541d21a3b7288 Mon Sep 17 00:00:00 2001 -From: Seth Michael Larson -Date: Fri, 23 Jan 2026 08:59:35 -0600 -Subject: [PATCH] gh-144125: email: verify headers are sound in BytesGenerator - (cherry picked from commit 052e55e7d44718fe46cbba0ca995cb8fcc359413) - -Co-authored-by: Seth Michael Larson -Co-authored-by: Denis Ledoux -Co-authored-by: Denis Ledoux <5822488+beledouxdenis@users.noreply.github.com> -Co-authored-by: Petr Viktorin <302922+encukou@users.noreply.github.com> -Co-authored-by: Bas Bloemsaat <1586868+basbloemsaat@users.noreply.github.com> -Signed-off-by: Azure Linux Security Servicing Account -Upstream-reference: https://github.com/python/cpython/pull/144188.patch ---- - Lib/email/generator.py | 12 +++++++++++- - Lib/test/test_email/test_generator.py | 4 +++- - Lib/test/test_email/test_policy.py | 6 +++++- - .../2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst | 4 ++++ - 4 files changed, 23 insertions(+), 3 deletions(-) - create mode 100644 Misc/NEWS.d/next/Security/2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst - -diff --git a/Lib/email/generator.py b/Lib/email/generator.py -index 47b9df8..8cbc43e 100644 ---- a/Lib/email/generator.py -+++ b/Lib/email/generator.py -@@ -22,6 +22,7 @@ NL = '\n' # XXX: no longer used by the code below. - NLCRE = re.compile(r'\r\n|\r|\n') - fcre = re.compile(r'^From ', re.MULTILINE) - NEWLINE_WITHOUT_FWSP = re.compile(r'\r\n[^ \t]|\r[^ \n\t]|\n[^ \t]') -+NEWLINE_WITHOUT_FWSP_BYTES = re.compile(br'\r\n[^ \t]|\r[^ \n\t]|\n[^ \t]') - - - class Generator: -@@ -429,7 +430,16 @@ class BytesGenerator(Generator): - # This is almost the same as the string version, except for handling - # strings with 8bit bytes. - for h, v in msg.raw_items(): -- self._fp.write(self.policy.fold_binary(h, v)) -+ folded = self.policy.fold_binary(h, v) -+ if self.policy.verify_generated_headers: -+ linesep = self.policy.linesep.encode() -+ if not folded.endswith(linesep): -+ raise HeaderWriteError( -+ f'folded header does not end with {linesep!r}: {folded!r}') -+ if NEWLINE_WITHOUT_FWSP_BYTES.search(folded.removesuffix(linesep)): -+ raise HeaderWriteError( -+ f'folded header contains newline: {folded!r}') -+ self._fp.write(folded) - # A blank line always separates headers from body - self.write(self._NL) - -diff --git a/Lib/test/test_email/test_generator.py b/Lib/test/test_email/test_generator.py -index c75a842..3ca79ed 100644 ---- a/Lib/test/test_email/test_generator.py -+++ b/Lib/test/test_email/test_generator.py -@@ -313,7 +313,7 @@ class TestGenerator(TestGeneratorBase, TestEmailBase): - self.assertEqual(s.getvalue(), self.typ(expected)) - - def test_verify_generated_headers(self): -- """gh-121650: by default the generator prevents header injection""" -+ # gh-121650: by default the generator prevents header injection - class LiteralHeader(str): - name = 'Header' - def fold(self, **kwargs): -@@ -334,6 +334,8 @@ class TestGenerator(TestGeneratorBase, TestEmailBase): - - with self.assertRaises(email.errors.HeaderWriteError): - message.as_string() -+ with self.assertRaises(email.errors.HeaderWriteError): -+ message.as_bytes() - - - class TestBytesGenerator(TestGeneratorBase, TestEmailBase): -diff --git a/Lib/test/test_email/test_policy.py b/Lib/test/test_email/test_policy.py -index baa35fd..71ec0fe 100644 ---- a/Lib/test/test_email/test_policy.py -+++ b/Lib/test/test_email/test_policy.py -@@ -296,7 +296,7 @@ class PolicyAPITests(unittest.TestCase): - policy.fold("Subject", subject) - - def test_verify_generated_headers(self): -- """Turning protection off allows header injection""" -+ # Turning protection off allows header injection - policy = email.policy.default.clone(verify_generated_headers=False) - for text in ( - 'Header: Value\r\nBad: Injection\r\n', -@@ -319,6 +319,10 @@ class PolicyAPITests(unittest.TestCase): - message.as_string(), - f"{text}\nBody", - ) -+ self.assertEqual( -+ message.as_bytes(), -+ f"{text}\nBody".encode(), -+ ) - - # XXX: Need subclassing tests. - # For adding subclassed objects, make sure the usual rules apply (subclass -diff --git a/Misc/NEWS.d/next/Security/2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst b/Misc/NEWS.d/next/Security/2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst -new file mode 100644 -index 0000000..e6333e7 ---- /dev/null -+++ b/Misc/NEWS.d/next/Security/2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst -@@ -0,0 +1,4 @@ -+:mod:`~email.generator.BytesGenerator` will now refuse to serialize (write) headers -+that are unsafely folded or delimited; see -+:attr:`~email.policy.Policy.verify_generated_headers`. (Contributed by Bas -+Bloemsaat and Petr Viktorin in :gh:`121650`). --- -2.45.4 - diff --git a/SPECS/python3.14/CVE-2026-4519.patch b/SPECS/python3.14/CVE-2026-4519.patch deleted file mode 100644 index 9225b700d89..00000000000 --- a/SPECS/python3.14/CVE-2026-4519.patch +++ /dev/null @@ -1,132 +0,0 @@ -From 64423d38b3b27b8f6cb479c554138b6a4d94830a Mon Sep 17 00:00:00 2001 -From: Seth Michael Larson -Date: Fri, 20 Mar 2026 09:47:13 -0500 -Subject: [PATCH 1/2] gh-143930: Reject leading dashes in webbrowser URLs - (cherry picked from commit 82a24a4442312bdcfc4c799885e8b3e00990f02b) - ---- - Lib/test/test_webbrowser.py | 5 +++++ - Lib/webbrowser.py | 12 ++++++++++++ - .../2026-01-16-12-04-49.gh-issue-143930.zYC5x3.rst | 1 + - 3 files changed, 18 insertions(+) - create mode 100644 Misc/NEWS.d/next/Security/2026-01-16-12-04-49.gh-issue-143930.zYC5x3.rst - -diff --git a/Lib/test/test_webbrowser.py b/Lib/test/test_webbrowser.py -index 2d695bc..60f094f 100644 ---- a/Lib/test/test_webbrowser.py -+++ b/Lib/test/test_webbrowser.py -@@ -59,6 +59,11 @@ class GenericBrowserCommandTest(CommandTestMixin, unittest.TestCase): - options=[], - arguments=[URL]) - -+ def test_reject_dash_prefixes(self): -+ browser = self.browser_class(name=CMD_NAME) -+ with self.assertRaises(ValueError): -+ browser.open(f"--key=val {URL}") -+ - - class BackgroundBrowserCommandTest(CommandTestMixin, unittest.TestCase): - -diff --git a/Lib/webbrowser.py b/Lib/webbrowser.py -index 13b9e85..ab66519 100755 ---- a/Lib/webbrowser.py -+++ b/Lib/webbrowser.py -@@ -158,6 +158,12 @@ class BaseBrowser(object): - def open_new_tab(self, url): - return self.open(url, 2) - -+ @staticmethod -+ def _check_url(url): -+ """Ensures that the URL is safe to pass to subprocesses as a parameter""" -+ if url and url.lstrip().startswith("-"): -+ raise ValueError(f"Invalid URL {url!r}: URLs must not start with '-' after leading whitespace") -+ - - class GenericBrowser(BaseBrowser): - """Class for all browsers started with a command -@@ -175,6 +181,7 @@ class GenericBrowser(BaseBrowser): - - def open(self, url, new=0, autoraise=True): - sys.audit("webbrowser.open", url) -+ self._check_url(url) - cmdline = [self.name] + [arg.replace("%s", url) - for arg in self.args] - try: -@@ -195,6 +202,7 @@ class BackgroundBrowser(GenericBrowser): - cmdline = [self.name] + [arg.replace("%s", url) - for arg in self.args] - sys.audit("webbrowser.open", url) -+ self._check_url(url) - try: - if sys.platform[:3] == 'win': - p = subprocess.Popen(cmdline) -@@ -260,6 +268,7 @@ class UnixBrowser(BaseBrowser): - - def open(self, url, new=0, autoraise=True): - sys.audit("webbrowser.open", url) -+ self._check_url(url) - if new == 0: - action = self.remote_action - elif new == 1: -@@ -350,6 +359,7 @@ class Konqueror(BaseBrowser): - - def open(self, url, new=0, autoraise=True): - sys.audit("webbrowser.open", url) -+ self._check_url(url) - # XXX Currently I know no way to prevent KFM from opening a new win. - if new == 2: - action = "newTab" -@@ -554,6 +564,7 @@ if sys.platform[:3] == "win": - class WindowsDefault(BaseBrowser): - def open(self, url, new=0, autoraise=True): - sys.audit("webbrowser.open", url) -+ self._check_url(url) - try: - os.startfile(url) - except OSError: -@@ -638,6 +649,7 @@ if sys.platform == 'darwin': - - def open(self, url, new=0, autoraise=True): - sys.audit("webbrowser.open", url) -+ self._check_url(url) - if self.name == 'default': - script = 'open location "%s"' % url.replace('"', '%22') # opens in default browser - else: -diff --git a/Misc/NEWS.d/next/Security/2026-01-16-12-04-49.gh-issue-143930.zYC5x3.rst b/Misc/NEWS.d/next/Security/2026-01-16-12-04-49.gh-issue-143930.zYC5x3.rst -new file mode 100644 -index 0000000..0f27eae ---- /dev/null -+++ b/Misc/NEWS.d/next/Security/2026-01-16-12-04-49.gh-issue-143930.zYC5x3.rst -@@ -0,0 +1 @@ -+Reject leading dashes in URLs passed to :func:`webbrowser.open` --- -2.45.4 - - -From 83c608b5ece9ad2aa88866dee9b52f8895156671 Mon Sep 17 00:00:00 2001 -From: Pinky -Date: Wed, 25 Mar 2026 00:01:36 +0530 -Subject: [PATCH 2/2] Simplify error message for invalid URL- backport - -Signed-off-by: Azure Linux Security Servicing Account -Upstream-reference: https://github.com/python/cpython/pull/146360.patch ---- - Lib/webbrowser.py | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/Lib/webbrowser.py b/Lib/webbrowser.py -index ab66519..0bdb644 100755 ---- a/Lib/webbrowser.py -+++ b/Lib/webbrowser.py -@@ -162,7 +162,7 @@ class BaseBrowser(object): - def _check_url(url): - """Ensures that the URL is safe to pass to subprocesses as a parameter""" - if url and url.lstrip().startswith("-"): -- raise ValueError(f"Invalid URL {url!r}: URLs must not start with '-' after leading whitespace") -+ raise ValueError(f"Invalid URL: {url}") - - - class GenericBrowser(BaseBrowser): --- -2.45.4 - diff --git a/SPECS/python3.14/python3.14.spec b/SPECS/python3.14/python3.14.spec index dd5fa59a03f..46b3576e05a 100644 --- a/SPECS/python3.14/python3.14.spec +++ b/SPECS/python3.14/python3.14.spec @@ -16,10 +16,6 @@ Source0: https://www.python.org/ftp/python/%{version}/Python-%{version}.t # pathfix.py was provided by the previous Python source bundle (Python-3.9.14.tar.xz) # It has been removed in later source bundles, but as our packages still require it, we will still provide for now. Source1: https://github.com/python/cpython/blob/3.9/Tools/scripts/pathfix.py -Patch0: CVE-2026-0672.patch -Patch1: CVE-2026-0865.patch -Patch2: CVE-2026-1299.patch -Patch3: CVE-2026-4519.patch BuildRequires: bzip2-devel BuildRequires: expat-devel >= 2.1.0 @@ -218,9 +214,9 @@ rm -rf %{buildroot}%{_bindir}/__pycache__ - License verified. - Side-by-side with the default python3 (3.12). Ships only versioned paths (/usr/bin/python3.14, /usr/lib/python3.14/, libpython3.14.so.1.0). -- Carries post-3.14.4 CVE patches CVE-2026-0672, CVE-2026-0865, CVE-2026-1299, - CVE-2026-4519. All prior CVE-2025-* patches on python3-3.12 are upstream in - 3.14.4; cgi3.patch is dropped because the cgi module was removed in 3.13 - (PEP 594). +- No CVE patches carried. All CVE patches on python3-3.12 (CVE-2025-4516, + -4517, -6069, -6075, -8194, -8291, -11468, -12084, -13836, -13837, + CVE-2026-0672, -0865, -1299, -4519) are present upstream in 3.14.4; + cgi3.patch is dropped because the cgi module was removed in 3.13 (PEP 594). - Drops the lib2to3-based tools subpackage since lib2to3 and /usr/bin/2to3 were removed in Python 3.13. From 8a5c0107739334fe6de323d90da1c808f48ee8ac Mon Sep 17 00:00:00 2001 From: Ihar Voitka Date: Thu, 23 Apr 2026 19:51:28 -0700 Subject: [PATCH 4/5] Ship pathfix.py locally in SPECS/python3.14/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pathfix.py Source1 is committed directly under SPECS/python3/ — the srpmpacker resolves sources from the local spec dir before reaching to azurelinuxsrcstorage. Missing the local copy was the reason the "Source Signature Check (SPECS)" CI was 404'ing on pathfix.py; the file is byte-identical to SPECS/python3/pathfix.py, so the existing signatures.json sha is already correct. Co-Authored-By: Claude Opus 4.7 (1M context) --- SPECS/python3.14/pathfix.py | 226 ++++++++++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100755 SPECS/python3.14/pathfix.py diff --git a/SPECS/python3.14/pathfix.py b/SPECS/python3.14/pathfix.py new file mode 100755 index 00000000000..d252321a21a --- /dev/null +++ b/SPECS/python3.14/pathfix.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 + +# Change the #! line (shebang) occurring in Python scripts. The new interpreter +# pathname must be given with a -i option. +# +# Command line arguments are files or directories to be processed. +# Directories are searched recursively for files whose name looks +# like a python module. +# Symbolic links are always ignored (except as explicit directory +# arguments). +# The original file is kept as a back-up (with a "~" attached to its name), +# -n flag can be used to disable this. + +# Sometimes you may find shebangs with flags such as `#! /usr/bin/env python -si`. +# Normally, pathfix overwrites the entire line, including the flags. +# To change interpreter and keep flags from the original shebang line, use -k. +# If you want to keep flags and add to them one single literal flag, use option -a. + + +# Undoubtedly you can do this using find and sed or perl, but this is +# a nice example of Python code that recurses down a directory tree +# and uses regular expressions. Also note several subtleties like +# preserving the file's mode and avoiding to even write a temp file +# when no changes are needed for a file. +# +# NB: by changing only the function fixfile() you can turn this +# into a program for a different change to Python programs... + +import sys +import re +import os +from stat import * +import getopt + +err = sys.stderr.write +dbg = err +rep = sys.stdout.write + +new_interpreter = None +preserve_timestamps = False +create_backup = True +keep_flags = False +add_flags = b'' + + +def main(): + global new_interpreter + global preserve_timestamps + global create_backup + global keep_flags + global add_flags + + usage = ('usage: %s -i /interpreter -p -n -k -a file-or-directory ...\n' % + sys.argv[0]) + try: + opts, args = getopt.getopt(sys.argv[1:], 'i:a:kpn') + except getopt.error as msg: + err(str(msg) + '\n') + err(usage) + sys.exit(2) + for o, a in opts: + if o == '-i': + new_interpreter = a.encode() + if o == '-p': + preserve_timestamps = True + if o == '-n': + create_backup = False + if o == '-k': + keep_flags = True + if o == '-a': + add_flags = a.encode() + if b' ' in add_flags: + err("-a option doesn't support whitespaces") + sys.exit(2) + if not new_interpreter or not new_interpreter.startswith(b'/') or \ + not args: + err('-i option or file-or-directory missing\n') + err(usage) + sys.exit(2) + bad = 0 + for arg in args: + if os.path.isdir(arg): + if recursedown(arg): bad = 1 + elif os.path.islink(arg): + err(arg + ': will not process symbolic links\n') + bad = 1 + else: + if fix(arg): bad = 1 + sys.exit(bad) + + +def ispython(name): + return name.endswith('.py') + + +def recursedown(dirname): + dbg('recursedown(%r)\n' % (dirname,)) + bad = 0 + try: + names = os.listdir(dirname) + except OSError as msg: + err('%s: cannot list directory: %r\n' % (dirname, msg)) + return 1 + names.sort() + subdirs = [] + for name in names: + if name in (os.curdir, os.pardir): continue + fullname = os.path.join(dirname, name) + if os.path.islink(fullname): pass + elif os.path.isdir(fullname): + subdirs.append(fullname) + elif ispython(name): + if fix(fullname): bad = 1 + for fullname in subdirs: + if recursedown(fullname): bad = 1 + return bad + + +def fix(filename): +## dbg('fix(%r)\n' % (filename,)) + try: + f = open(filename, 'rb') + except IOError as msg: + err('%s: cannot open: %r\n' % (filename, msg)) + return 1 + with f: + line = f.readline() + fixed = fixline(line) + if line == fixed: + rep(filename+': no change\n') + return + head, tail = os.path.split(filename) + tempname = os.path.join(head, '@' + tail) + try: + g = open(tempname, 'wb') + except IOError as msg: + err('%s: cannot create: %r\n' % (tempname, msg)) + return 1 + with g: + rep(filename + ': updating\n') + g.write(fixed) + BUFSIZE = 8*1024 + while 1: + buf = f.read(BUFSIZE) + if not buf: break + g.write(buf) + + # Finishing touch -- move files + + mtime = None + atime = None + # First copy the file's mode to the temp file + try: + statbuf = os.stat(filename) + mtime = statbuf.st_mtime + atime = statbuf.st_atime + os.chmod(tempname, statbuf[ST_MODE] & 0o7777) + except OSError as msg: + err('%s: warning: chmod failed (%r)\n' % (tempname, msg)) + # Then make a backup of the original file as filename~ + if create_backup: + try: + os.rename(filename, filename + '~') + except OSError as msg: + err('%s: warning: backup failed (%r)\n' % (filename, msg)) + else: + try: + os.remove(filename) + except OSError as msg: + err('%s: warning: removing failed (%r)\n' % (filename, msg)) + # Now move the temp file to the original file + try: + os.rename(tempname, filename) + except OSError as msg: + err('%s: rename failed (%r)\n' % (filename, msg)) + return 1 + if preserve_timestamps: + if atime and mtime: + try: + os.utime(filename, (atime, mtime)) + except OSError as msg: + err('%s: reset of timestamp failed (%r)\n' % (filename, msg)) + return 1 + # Return success + return 0 + + +def parse_shebang(shebangline): + shebangline = shebangline.rstrip(b'\n') + start = shebangline.find(b' -') + if start == -1: + return b'' + return shebangline[start:] + + +def populate_flags(shebangline): + old_flags = b'' + if keep_flags: + old_flags = parse_shebang(shebangline) + if old_flags: + old_flags = old_flags[2:] + if not (old_flags or add_flags): + return b'' + # On Linux, the entire string following the interpreter name + # is passed as a single argument to the interpreter. + # e.g. "#! /usr/bin/python3 -W Error -s" runs "/usr/bin/python3 "-W Error -s" + # so shebang should have single '-' where flags are given and + # flag might need argument for that reasons adding new flags is + # between '-' and original flags + # e.g. #! /usr/bin/python3 -sW Error + return b' -' + add_flags + old_flags + + +def fixline(line): + if not line.startswith(b'#!'): + return line + + if b"python" not in line: + return line + + flags = populate_flags(line) + return b'#! ' + new_interpreter + flags + b'\n' + + +if __name__ == '__main__': + main() From 06cf8ea6172f71f2a5f17a0767c1db8ac8c9141d Mon Sep 17 00:00:00 2001 From: Ihar Voitka Date: Thu, 23 Apr 2026 19:55:03 -0700 Subject: [PATCH 5/5] Drop pathfix.py from python3.14 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nothing in Azure Linux's build pipeline invokes a versioned pathfixN.N.py — the file is a vestige from the era when CPython shipped pathfix.py in its own source tarball. Carrying it for 3.14 adds a build-time source fetch and a devel-subpackage file with no downstream consumer. Removes Source1, the %install cp from %{SOURCE1}, the %files devel entry, the local pathfix.py payload and its signatures.json hash. Co-Authored-By: Claude Opus 4.7 (1M context) --- SPECS/python3.14/pathfix.py | 226 -------------------- SPECS/python3.14/python3.14.signatures.json | 1 - SPECS/python3.14/python3.14.spec | 8 - 3 files changed, 235 deletions(-) delete mode 100755 SPECS/python3.14/pathfix.py diff --git a/SPECS/python3.14/pathfix.py b/SPECS/python3.14/pathfix.py deleted file mode 100755 index d252321a21a..00000000000 --- a/SPECS/python3.14/pathfix.py +++ /dev/null @@ -1,226 +0,0 @@ -#!/usr/bin/env python3 - -# Change the #! line (shebang) occurring in Python scripts. The new interpreter -# pathname must be given with a -i option. -# -# Command line arguments are files or directories to be processed. -# Directories are searched recursively for files whose name looks -# like a python module. -# Symbolic links are always ignored (except as explicit directory -# arguments). -# The original file is kept as a back-up (with a "~" attached to its name), -# -n flag can be used to disable this. - -# Sometimes you may find shebangs with flags such as `#! /usr/bin/env python -si`. -# Normally, pathfix overwrites the entire line, including the flags. -# To change interpreter and keep flags from the original shebang line, use -k. -# If you want to keep flags and add to them one single literal flag, use option -a. - - -# Undoubtedly you can do this using find and sed or perl, but this is -# a nice example of Python code that recurses down a directory tree -# and uses regular expressions. Also note several subtleties like -# preserving the file's mode and avoiding to even write a temp file -# when no changes are needed for a file. -# -# NB: by changing only the function fixfile() you can turn this -# into a program for a different change to Python programs... - -import sys -import re -import os -from stat import * -import getopt - -err = sys.stderr.write -dbg = err -rep = sys.stdout.write - -new_interpreter = None -preserve_timestamps = False -create_backup = True -keep_flags = False -add_flags = b'' - - -def main(): - global new_interpreter - global preserve_timestamps - global create_backup - global keep_flags - global add_flags - - usage = ('usage: %s -i /interpreter -p -n -k -a file-or-directory ...\n' % - sys.argv[0]) - try: - opts, args = getopt.getopt(sys.argv[1:], 'i:a:kpn') - except getopt.error as msg: - err(str(msg) + '\n') - err(usage) - sys.exit(2) - for o, a in opts: - if o == '-i': - new_interpreter = a.encode() - if o == '-p': - preserve_timestamps = True - if o == '-n': - create_backup = False - if o == '-k': - keep_flags = True - if o == '-a': - add_flags = a.encode() - if b' ' in add_flags: - err("-a option doesn't support whitespaces") - sys.exit(2) - if not new_interpreter or not new_interpreter.startswith(b'/') or \ - not args: - err('-i option or file-or-directory missing\n') - err(usage) - sys.exit(2) - bad = 0 - for arg in args: - if os.path.isdir(arg): - if recursedown(arg): bad = 1 - elif os.path.islink(arg): - err(arg + ': will not process symbolic links\n') - bad = 1 - else: - if fix(arg): bad = 1 - sys.exit(bad) - - -def ispython(name): - return name.endswith('.py') - - -def recursedown(dirname): - dbg('recursedown(%r)\n' % (dirname,)) - bad = 0 - try: - names = os.listdir(dirname) - except OSError as msg: - err('%s: cannot list directory: %r\n' % (dirname, msg)) - return 1 - names.sort() - subdirs = [] - for name in names: - if name in (os.curdir, os.pardir): continue - fullname = os.path.join(dirname, name) - if os.path.islink(fullname): pass - elif os.path.isdir(fullname): - subdirs.append(fullname) - elif ispython(name): - if fix(fullname): bad = 1 - for fullname in subdirs: - if recursedown(fullname): bad = 1 - return bad - - -def fix(filename): -## dbg('fix(%r)\n' % (filename,)) - try: - f = open(filename, 'rb') - except IOError as msg: - err('%s: cannot open: %r\n' % (filename, msg)) - return 1 - with f: - line = f.readline() - fixed = fixline(line) - if line == fixed: - rep(filename+': no change\n') - return - head, tail = os.path.split(filename) - tempname = os.path.join(head, '@' + tail) - try: - g = open(tempname, 'wb') - except IOError as msg: - err('%s: cannot create: %r\n' % (tempname, msg)) - return 1 - with g: - rep(filename + ': updating\n') - g.write(fixed) - BUFSIZE = 8*1024 - while 1: - buf = f.read(BUFSIZE) - if not buf: break - g.write(buf) - - # Finishing touch -- move files - - mtime = None - atime = None - # First copy the file's mode to the temp file - try: - statbuf = os.stat(filename) - mtime = statbuf.st_mtime - atime = statbuf.st_atime - os.chmod(tempname, statbuf[ST_MODE] & 0o7777) - except OSError as msg: - err('%s: warning: chmod failed (%r)\n' % (tempname, msg)) - # Then make a backup of the original file as filename~ - if create_backup: - try: - os.rename(filename, filename + '~') - except OSError as msg: - err('%s: warning: backup failed (%r)\n' % (filename, msg)) - else: - try: - os.remove(filename) - except OSError as msg: - err('%s: warning: removing failed (%r)\n' % (filename, msg)) - # Now move the temp file to the original file - try: - os.rename(tempname, filename) - except OSError as msg: - err('%s: rename failed (%r)\n' % (filename, msg)) - return 1 - if preserve_timestamps: - if atime and mtime: - try: - os.utime(filename, (atime, mtime)) - except OSError as msg: - err('%s: reset of timestamp failed (%r)\n' % (filename, msg)) - return 1 - # Return success - return 0 - - -def parse_shebang(shebangline): - shebangline = shebangline.rstrip(b'\n') - start = shebangline.find(b' -') - if start == -1: - return b'' - return shebangline[start:] - - -def populate_flags(shebangline): - old_flags = b'' - if keep_flags: - old_flags = parse_shebang(shebangline) - if old_flags: - old_flags = old_flags[2:] - if not (old_flags or add_flags): - return b'' - # On Linux, the entire string following the interpreter name - # is passed as a single argument to the interpreter. - # e.g. "#! /usr/bin/python3 -W Error -s" runs "/usr/bin/python3 "-W Error -s" - # so shebang should have single '-' where flags are given and - # flag might need argument for that reasons adding new flags is - # between '-' and original flags - # e.g. #! /usr/bin/python3 -sW Error - return b' -' + add_flags + old_flags - - -def fixline(line): - if not line.startswith(b'#!'): - return line - - if b"python" not in line: - return line - - flags = populate_flags(line) - return b'#! ' + new_interpreter + flags + b'\n' - - -if __name__ == '__main__': - main() diff --git a/SPECS/python3.14/python3.14.signatures.json b/SPECS/python3.14/python3.14.signatures.json index 0cd00f62eed..208f0ae1b59 100644 --- a/SPECS/python3.14/python3.14.signatures.json +++ b/SPECS/python3.14/python3.14.signatures.json @@ -1,6 +1,5 @@ { "Signatures": { - "pathfix.py": "7a2ff222346d3c95b08814e3372975823e099c17dddaa73a459a3d840e6e9c1b", "Python-3.14.4.tar.xz": "d923c51303e38e249136fc1bdf3568d56ecb03214efdef48516176d3d7faaef8" } } diff --git a/SPECS/python3.14/python3.14.spec b/SPECS/python3.14/python3.14.spec index 46b3576e05a..ab93f44e11e 100644 --- a/SPECS/python3.14/python3.14.spec +++ b/SPECS/python3.14/python3.14.spec @@ -13,9 +13,6 @@ Distribution: Azure Linux Group: System Environment/Programming URL: https://www.python.org/ Source0: https://www.python.org/ftp/python/%{version}/Python-%{version}.tar.xz -# pathfix.py was provided by the previous Python source bundle (Python-3.9.14.tar.xz) -# It has been removed in later source bundles, but as our packages still require it, we will still provide for now. -Source1: https://github.com/python/cpython/blob/3.9/Tools/scripts/pathfix.py BuildRequires: bzip2-devel BuildRequires: expat-devel >= 2.1.0 @@ -129,10 +126,6 @@ export OPT="%{extension_cflags} %{openssl_flags}" # Windows executables get installed by pip - we don't need these. find %{buildroot}%{_libdir}/python%{majmin}/site-packages -name '*.exe' -delete -print -# Install pathfix.py to bindir under the versioned name only. -# The unversioned /usr/bin/pathfix.py is owned by the default python3 package. -cp -pv %{SOURCE1} %{buildroot}%{_bindir}/pathfix%{majmin}.py - # Remove unversioned filesystem entries that belong to the default python3 # (3.12) package. Python 3.14 installs strictly side-by-side under versioned # paths, so these unversioned symlinks / stable-ABI files must not be shipped @@ -201,7 +194,6 @@ rm -rf %{buildroot}%{_bindir}/__pycache__ %{_libdir}/pkgconfig/python-%{majmin}-embed.pc %{_libdir}/libpython%{majmin}.so %{_bindir}/python%{majmin}-config -%{_bindir}/pathfix%{majmin}.py %doc Misc/README.valgrind Misc/valgrind-python.supp %exclude %{_bindir}/idle*