Commit 09c3e734b16 for php

commit 09c3e734b1601e6f8d4fcdf5a564dee556532b03
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Thu Jul 23 14:37:07 2026 -0400

    ext/standard: Fix convert.quoted-printable-decode of lowercase hex

    The stream filter's nibble decoder accepts lowercase a-f via isxdigit()
    but decodes them as (*ps - 0x37), correct only for uppercase A-F. Bytes
    whose hex spelling uses a lowercase digit are silently corrupted (=cd
    decodes to 0xed, =0a to 0x2a). Decode lowercase with (*ps - 0x57).

    Closes GH-22870

diff --git a/NEWS b/NEWS
index 52dda8a9daa..d0748093db9 100644
--- a/NEWS
+++ b/NEWS
@@ -223,6 +223,8 @@ PHP                                                                        NEWS
   . Fix persistent stream context lifetime during shutdown (Levi Morrison)
   . Fix wordwrap() ignoring an existing break at the end of the string
     (Nicolas Grekas)
+  . Fixed convert.quoted-printable-decode decoding lowercase hex digits to
+    the wrong byte. (Ilia Alshanetsky)

 - Tidy:
   . Fixed a use-after-free when a tidyNode is used after its document is
diff --git a/ext/standard/filters.c b/ext/standard/filters.c
index 8f2d348f2e5..3c6cfd84af0 100644
--- a/ext/standard/filters.c
+++ b/ext/standard/filters.c
@@ -953,7 +953,7 @@ static php_conv_err_t php_conv_qprint_decode_convert(php_conv_qprint_decode *ins
 					err = PHP_CONV_ERR_INVALID_SEQ;
 					goto out;
 				}
-				next_char = (next_char << 4) | (*ps >= 'A' ? *ps - 0x37 : *ps - 0x30);
+				next_char = (next_char << 4) | (*ps >= 'a' ? *ps - 0x57 : (*ps >= 'A' ? *ps - 0x37 : *ps - 0x30));
 				scan_stat++;
 				ps++, icnt--;
 				if (scan_stat != 3) {
diff --git a/ext/standard/tests/filters/qp_decode_lowercase.phpt b/ext/standard/tests/filters/qp_decode_lowercase.phpt
new file mode 100644
index 00000000000..50a6c454d37
--- /dev/null
+++ b/ext/standard/tests/filters/qp_decode_lowercase.phpt
@@ -0,0 +1,20 @@
+--TEST--
+convert.quoted-printable-decode: lowercase hex digits decode correctly
+--FILE--
+<?php
+foreach (['=cd', '=0a', '=da', '=CD', '=AB', '=ab'] as $in) {
+    $fp = fopen('php://temp', 'r+');
+    fwrite($fp, $in);
+    rewind($fp);
+    stream_filter_append($fp, 'convert.quoted-printable-decode', STREAM_FILTER_READ);
+    echo $in, ' => ', bin2hex(stream_get_contents($fp)), "\n";
+    fclose($fp);
+}
+?>
+--EXPECT--
+=cd => cd
+=0a => 0a
+=da => da
+=CD => cd
+=AB => ab
+=ab => ab