From 36f4122c78c596b0d35f9d178ce55dd346cd8871 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Tue, 10 Nov 2026 09:30:44 -0500 Subject: [PATCH] External Libraries: Update getID3 to version 1.9.28. This update includes the following fixes: - Decompressing a compressed ID3v2 frame is now capped at a quarter of the memory limit, or 32 MB when the limit is unknown (GHSA-c2xw-vp6w-gpph). - The QuickTime `stsd`, `dref`, `elst` and `keys` atom parsers no longer trust entry counts larger than the atom's data can hold, which prevents memory exhaustion from crafted files (GHSA-4pp3-6hmx-mfmg). - On PHP < 8.0, `getid3_lib::XML2array()` refuses to parse XML if external entity loading cannot be disabled (GHSA-j649-xr34-mmmh). - Embedded pictures are no longer passed through character set conversion, which corrupted them. - The ID3v2 involved people (IPL, IPLS and TIPL) and musician credits (TMCL) frames are now parsed consistently into role and person pairs. - `replaygain_album_peak` is copied from TXXX frames to the main replay gain data. None of the changes are needed for PHP 8.6 compatibility. As in previous updates, only the audio and video subset of the library that WordPress bundles is updated. All 18 bundled files remain identical to the upstream release. References: * [https://github.com/JamesHeinrich/getID3/releases/tag/v1.9.27 getID3 1.9.27 release notes] * [https://github.com/JamesHeinrich/getID3/releases/tag/v1.9.28 getID3 1.9.28 release notes] * [https://github.com/JamesHeinrich/getID3/compare/v1.9.26...v1.9.28 Full list of changes in getID3 1.9.27 and 1.9.28] Follow-up to r47601, r48278, r52254, r54376, r56975, r61253, r61889, r62087, r63520. Props desrosj. See #00000. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011Y86gx1T9qpKZuJWesSGiZ --- src/wp-includes/ID3/getid3.lib.php | 8 +- src/wp-includes/ID3/getid3.php | 25 ++- .../ID3/module.audio-video.quicktime.php | 24 +++ src/wp-includes/ID3/module.tag.id3v2.php | 172 +++++++++--------- 4 files changed, 136 insertions(+), 93 deletions(-) diff --git a/src/wp-includes/ID3/getid3.lib.php b/src/wp-includes/ID3/getid3.lib.php index 911876eed6ad4..c0aa1ee3b1676 100644 --- a/src/wp-includes/ID3/getid3.lib.php +++ b/src/wp-includes/ID3/getid3.lib.php @@ -746,10 +746,14 @@ public static function XML2array($XMLstring) { // https://core.trac.wordpress.org/changeset/29378 // This function has been deprecated in PHP 8.0 because in libxml 2.9.0, external entity loading is // disabled by default, but is still needed when LIBXML_NOENT is used. - $loader = @libxml_disable_entity_loader(true); + $loader = libxml_disable_entity_loader(true); + if ($loader === false && !libxml_disable_entity_loader(true)) { + // Failed to disable external entity loading — refuse to parse to prevent XXE + return false; + } $XMLobject = simplexml_load_string($XMLstring, 'SimpleXMLElement', GETID3_LIBXML_OPTIONS); $return = self::SimpleXMLelement2array($XMLobject); - @libxml_disable_entity_loader($loader); + libxml_disable_entity_loader($loader); return $return; } } else { diff --git a/src/wp-includes/ID3/getid3.php b/src/wp-includes/ID3/getid3.php index 4e7ccf3356831..a2cdc481aa66c 100644 --- a/src/wp-includes/ID3/getid3.php +++ b/src/wp-includes/ID3/getid3.php @@ -387,7 +387,7 @@ class getID3 */ protected $startup_warning = ''; - const VERSION = '1.9.26-202609042051'; + const VERSION = '1.9.28-202610301738'; const FREAD_BUFFER_SIZE = 32768; const ATTACHMENTS_NONE = false; @@ -1008,7 +1008,7 @@ public function GetFileFormatArray() { 'mime_type' => 'audio/dsd', ), - // DTS - audio - Dolby Theatre System + // DTS - audio - Digital Theater Systems 'dts' => array( 'pattern' => '^\\x7F\\xFE\\x80\\x01', 'group' => 'audio', @@ -1606,7 +1606,7 @@ public function GetFileFormat(&$filedata, $filename='') { // and verify there's at least one instance of "TRACK xx AUDIO" in the file $GetFileFormatArray = $this->GetFileFormatArray(); $info = $GetFileFormatArray['cue']; - $info['include'] = 'module.'.$info['group'].'.'.$info['module'].'.php'; + $info['include'] = 'module.'.$info['group'].'.'.$info['module'].'.php'; return $info; } @@ -1629,14 +1629,21 @@ public function CharConvert(&$array, $encoding) { // loop thru array foreach ($array as $key => $value) { - // go recursive - if (is_array($value)) { + if ($key == 'picture') { // https://github.com/JamesHeinrich/getID3/issues/508 + + // pictures are binary data and should not be altered + continue; + + } elseif (is_array($value)) { + + // convert arrays recursively $this->CharConvert($array[$key], $encoding); - } - // convert string - elseif (is_string($value)) { + } elseif (is_string($value)) { + + // actually convert only strings $array[$key] = trim(getid3_lib::iconv_fallback($encoding, $this->encoding, $value)); + } } } @@ -1690,7 +1697,7 @@ public function HandleAllTags() { if (is_string($value)) { $value = trim($value, " \r\n\t"); // do not trim nulls from $value!! Unicode characters will get mangled if trailing nulls are removed! } - if (isset($value) && $value !== "") { + if (isset($value) && ($value !== '')) { if (!is_numeric($key)) { $this->info['tags'][trim($tag_name)][trim($tag_key)][$key] = $value; } else { diff --git a/src/wp-includes/ID3/module.audio-video.quicktime.php b/src/wp-includes/ID3/module.audio-video.quicktime.php index 161702ba186b6..d298024e11ea6 100644 --- a/src/wp-includes/ID3/module.audio-video.quicktime.php +++ b/src/wp-includes/ID3/module.audio-video.quicktime.php @@ -827,6 +827,12 @@ public function QuicktimeParseAtom($atomname, $atomsize, $atom_data, $baseoffset } } + // prevent memory exhaustion from crafted files: each "stsd" entry is at least 16 bytes (4 size + 4 data_format + 6 reserved + 2 reference_index) + $max_entries = (int) floor(max(0, strlen($atom_data) - 8) / 16); + if ($atom_structure['number_entries'] > $max_entries) { + $this->warning('QuickTime atom "stsd" claims '.$atom_structure['number_entries'].' entries but only has data for '.$max_entries.', truncating'); + $atom_structure['number_entries'] = $max_entries; + } $stsdEntriesDataOffset = 8; for ($i = 0; $i < (int) $atom_structure['number_entries']; $i++) { $atom_structure['sample_description_table'][$i]['size'] = getid3_lib::BigEndian2Int(substr($atom_data, $stsdEntriesDataOffset, 4)); @@ -1180,6 +1186,12 @@ public function QuicktimeParseAtom($atomname, $atomsize, $atom_data, $baseoffset $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 $atom_structure['number_entries'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); + // prevent memory exhaustion from crafted files: each "dref" entry is at least 12 bytes (4 size + 4 type + 1 version + 3 flags) + $max_entries = (int) floor(max(0, strlen($atom_data) - 8) / 12); + if ($atom_structure['number_entries'] > $max_entries) { + $this->warning('QuickTime atom "dref" claims '.$atom_structure['number_entries'].' entries but only has data for '.$max_entries.', truncating'); + $atom_structure['number_entries'] = $max_entries; + } $drefDataOffset = 8; for ($i = 0; $i < $atom_structure['number_entries']; $i++) { $atom_structure['data_references'][$i]['size'] = getid3_lib::BigEndian2Int(substr($atom_data, $drefDataOffset, 4)); @@ -1318,6 +1330,12 @@ public function QuicktimeParseAtom($atomname, $atomsize, $atom_data, $baseoffset $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 $atom_structure['number_entries'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); + // prevent memory exhaustion from crafted files: each "elst" entry is at least 12 bytes (4 track_duration + 4 media_time + 4 media_rate) + $max_entries = (int) floor(max(0, strlen($atom_data) - 8) / 12); + if ($atom_structure['number_entries'] > $max_entries) { + $this->warning('QuickTime atom "elst" claims '.$atom_structure['number_entries'].' entries but only has data for '.$max_entries.', truncating'); + $atom_structure['number_entries'] = $max_entries; + } for ($i = 0; $i < $atom_structure['number_entries']; $i++ ) { $atom_structure['edit_list'][$i]['track_duration'] = getid3_lib::BigEndian2Int(substr($atom_data, 8 + ($i * 12) + 0, 4)); $atom_structure['edit_list'][$i]['media_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 8 + ($i * 12) + 4, 4)); @@ -1800,6 +1818,12 @@ public function QuicktimeParseAtom($atomname, $atomsize, $atom_data, $baseoffset $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); $atom_structure['entry_count'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); + // prevent memory exhaustion from crafted files: each "keys" entry is at least 8 bytes (4 key_size + 4 key_namespace) + $max_entries = (int) floor(max(0, strlen($atom_data) - 8) / 8); + if ($atom_structure['entry_count'] > $max_entries) { + $this->warning('QuickTime atom "keys" claims '.$atom_structure['entry_count'].' entries but only has data for '.$max_entries.', truncating'); + $atom_structure['entry_count'] = $max_entries; + } $keys_atom_offset = 8; $keys_index_base = (!empty($info['quicktime']['temp_meta_key_names']) ? count($info['quicktime']['temp_meta_key_names']) : 0); // file may contain multiple "keys" entries, starting index should be culmulative not reset to 1 on each set; https://github.com/JamesHeinrich/getID3/issues/452 diff --git a/src/wp-includes/ID3/module.tag.id3v2.php b/src/wp-includes/ID3/module.tag.id3v2.php index 51a6b54104ede..7a5a8fcdaf2d5 100644 --- a/src/wp-includes/ID3/module.tag.id3v2.php +++ b/src/wp-includes/ID3/module.tag.id3v2.php @@ -495,6 +495,11 @@ public function Analyze() { $info['replay_gain']['album']['adjustment'] = floatval(trim(str_replace('dB', '', $txxx_array['data']))); } break; + case 'replaygain_album_peak': + if (empty($info['replay_gain']['album']['peak']) && !empty($txxx_array['data'])) { + $info['replay_gain']['album']['peak'] = floatval($txxx_array['data']); + } + break; } } } @@ -622,8 +627,10 @@ public function ParseID3v2Frame(&$parsedFrame) { if (!function_exists('gzuncompress')) { $this->warning('gzuncompress() support required to decompress ID3v2 frame "'.$parsedFrame['frame_name'].'"'); } else { - if ($decompresseddata = @gzuncompress(substr($parsedFrame['data'], 4))) { - //if ($decompresseddata = @gzuncompress($parsedFrame['data'])) { + // https://github.com/JamesHeinrich/getID3/security/advisories/GHSA-c2xw-vp6w-gpph + // Set a limit of 1/4 of PHP memory limit (if known), or default hard cap of 32MB if unknown. + $decompression_limit = ($this->getid3->memory_limit > 0) ? (int) round($this->getid3->memory_limit / 4) : 32*1024*1024; + if ($decompresseddata = @gzuncompress(substr($parsedFrame['data'], 4), $decompression_limit)) { $parsedFrame['data'] = $decompresseddata; unset($decompresseddata); } else { @@ -702,6 +709,87 @@ public function ParseID3v2Frame(&$parsedFrame) { //unset($parsedFrame['data']); do not unset, may be needed elsewhere, e.g. for replaygain + } elseif ((($id3v2_majorversion == 3) && ($parsedFrame['frame_name'] == 'IPLS')) || // 4.4 IPLS Involved people list (ID3v2.3 only) + (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'IPL')) || // 4.4 IPL Involved people list (ID3v2.2 only) + (($id3v2_majorversion >= 4) && ($parsedFrame['frame_name'] == 'TIPL')) || // 4.2.2 TIPL Involved people list (ID3v2.4 only) + (($id3v2_majorversion >= 4) && ($parsedFrame['frame_name'] == 'TMCL'))) { // 4.2.2 TMCL Musician credits list (ID3v2.4 only) + // http://id3.org/id3v2.3.0#sec4.4 + // There may only be one 'IPL' frame in each tag + //
+ // Text encoding $xx + // People list strings + + $frame_offset = 0; + $frame_textencoding = ord(substr($parsedFrame['data'], $frame_offset++, 1)); + if ((($id3v2_majorversion <= 3) && ($frame_textencoding > 1)) || (($id3v2_majorversion == 4) && ($frame_textencoding > 3))) { + $this->warning('Invalid text encoding byte ('.$frame_textencoding.') in frame "'.$parsedFrame['frame_name'].'" - defaulting to ISO-8859-1 encoding'); + } + $parsedFrame['encodingid'] = $frame_textencoding; + $parsedFrame['encoding'] = $this->TextEncodingNameLookup($parsedFrame['encodingid']); + $parsedFrame['data_raw'] = (string) substr($parsedFrame['data'], $frame_offset); + + // https://www.getid3.org/phpBB3/viewtopic.php?t=1369 + // "this tag typically contains null terminated strings, which are associated in pairs" + // "there are users that use the tag incorrectly" + $IPLS_parts_unsorted = array(); + if (((strlen($parsedFrame['data_raw']) % 2) == 0) && ((substr($parsedFrame['data_raw'], 0, 2) == "\xFF\xFE") || (substr($parsedFrame['data_raw'], 0, 2) == "\xFE\xFF"))) { + // UTF-16, be careful looking for null bytes since most 2-byte characters may contain one; you need to find twin null bytes, and on even padding + $thisILPS = ''; + for ($i = 0; $i < strlen($parsedFrame['data_raw']); $i += 2) { + $twobytes = substr($parsedFrame['data_raw'], $i, 2); + if ($twobytes === "\x00\x00") { + $IPLS_parts_unsorted[] = getid3_lib::iconv_fallback($parsedFrame['encoding'], $info['id3v2']['encoding'], $thisILPS); + $thisILPS = ''; + } else { + $thisILPS .= $twobytes; + } + } + if (strlen($thisILPS) > 0) { // extract the last part if any, even if it's only a BOM with no text following + $IPLS_parts_unsorted[] = getid3_lib::iconv_fallback($parsedFrame['encoding'], $info['id3v2']['encoding'], $thisILPS); + } + } else { + // ISO-8859-1 or UTF-8 or other single-byte-null character set + $IPLS_parts_unsorted = explode("\x00", $parsedFrame['data_raw']); + if (end($IPLS_parts_unsorted) === '') { + // there was a terminating null after the last part and explode appended an empty string; drop it + array_pop($IPLS_parts_unsorted); + } + } + + $IPLS_parts = array(); + if (count($IPLS_parts_unsorted) == 1) { + // Just a list of names, e.g. "Dino Baptiste, Jimmy Copley, John Gordon, Bernie Marsden, Sharon Watson". + // Use the normal output format but with empty roles. + $IPLS_parts_sorted = preg_split('#[;,\\r\\n\\t]#', $IPLS_parts_unsorted[0]); + foreach ($IPLS_parts_sorted as $person) { + $IPLS_parts[] = array( + 'position' => '', + 'person' => $person + ); + } + } + else { + while (\count($IPLS_parts_unsorted) >= 2) { + $IPLS_parts[] = array( + 'position' => array_shift($IPLS_parts_unsorted), + 'person' => array_shift($IPLS_parts_unsorted) + ); + } + if (\count($IPLS_parts_unsorted) > 0) { + $this->warning("Odd number of {$parsedFrame['frame_name']} parts - expected even number forming role/name pairs"); + $IPLS_parts[] = array( + 'position' => array_shift($IPLS_parts_unsorted), + 'person' => '' + ); + } + } + $parsedFrame['data'] = $IPLS_parts; + + if (!empty($parsedFrame['framenameshort']) && !empty($parsedFrame['data'])) { + $info['id3v2']['comments'][$parsedFrame['framenameshort']] = $parsedFrame['data']; + } + + } elseif ($parsedFrame['frame_name'][0] == 'T') { // 4.2. T??[?] Text information frame // There may only be one text information frame of its kind in an tag. //
- // Text encoding $xx - // People list strings - - $frame_offset = 0; - $frame_textencoding = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - if ((($id3v2_majorversion <= 3) && ($frame_textencoding > 1)) || (($id3v2_majorversion == 4) && ($frame_textencoding > 3))) { - $this->warning('Invalid text encoding byte ('.$frame_textencoding.') in frame "'.$parsedFrame['frame_name'].'" - defaulting to ISO-8859-1 encoding'); - } - $parsedFrame['encodingid'] = $frame_textencoding; - $parsedFrame['encoding'] = $this->TextEncodingNameLookup($parsedFrame['encodingid']); - $parsedFrame['data_raw'] = (string) substr($parsedFrame['data'], $frame_offset); - - // https://www.getid3.org/phpBB3/viewtopic.php?t=1369 - // "this tag typically contains null terminated strings, which are associated in pairs" - // "there are users that use the tag incorrectly" - $IPLS_parts = array(); - if (strpos($parsedFrame['data_raw'], "\x00") !== false) { - $IPLS_parts_unsorted = array(); - if (((strlen($parsedFrame['data_raw']) % 2) == 0) && ((substr($parsedFrame['data_raw'], 0, 2) == "\xFF\xFE") || (substr($parsedFrame['data_raw'], 0, 2) == "\xFE\xFF"))) { - // UTF-16, be careful looking for null bytes since most 2-byte characters may contain one; you need to find twin null bytes, and on even padding - $thisILPS = ''; - for ($i = 0; $i < strlen($parsedFrame['data_raw']); $i += 2) { - $twobytes = substr($parsedFrame['data_raw'], $i, 2); - if ($twobytes === "\x00\x00") { - $IPLS_parts_unsorted[] = getid3_lib::iconv_fallback($parsedFrame['encoding'], $info['id3v2']['encoding'], $thisILPS); - $thisILPS = ''; - } else { - $thisILPS .= $twobytes; - } - } - if (strlen($thisILPS) > 2) { // 2-byte BOM - $IPLS_parts_unsorted[] = getid3_lib::iconv_fallback($parsedFrame['encoding'], $info['id3v2']['encoding'], $thisILPS); - } - } else { - // ISO-8859-1 or UTF-8 or other single-byte-null character set - $IPLS_parts_unsorted = explode("\x00", $parsedFrame['data_raw']); - } - if (count($IPLS_parts_unsorted) == 1) { - // just a list of names, e.g. "Dino Baptiste, Jimmy Copley, John Gordon, Bernie Marsden, Sharon Watson" - foreach ($IPLS_parts_unsorted as $key => $value) { - $IPLS_parts_sorted = preg_split('#[;,\\r\\n\\t]#', $value); - $position = ''; - foreach ($IPLS_parts_sorted as $person) { - $IPLS_parts[] = array('position'=>$position, 'person'=>$person); - } - } - } elseif ((count($IPLS_parts_unsorted) % 2) == 0) { - $position = ''; - $person = ''; - foreach ($IPLS_parts_unsorted as $key => $value) { - if (($key % 2) == 0) { - $position = $value; - } else { - $person = $value; - $IPLS_parts[] = array('position'=>$position, 'person'=>$person); - $position = ''; - $person = ''; - } - } - } else { - foreach ($IPLS_parts_unsorted as $key => $value) { - $IPLS_parts[] = array($value); - } - } - - } else { - $IPLS_parts = preg_split('#[;,\\r\\n\\t]#', $parsedFrame['data_raw']); - } - $parsedFrame['data'] = $IPLS_parts; - - if (!empty($parsedFrame['framenameshort']) && !empty($parsedFrame['data'])) { - $info['id3v2']['comments'][$parsedFrame['framenameshort']][] = $parsedFrame['data']; - } - - } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'MCDI')) || // 4.4 MCDI Music CD identifier (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'MCI'))) { // 4.5 MCI Music CD identifier // There may only be one 'MCDI' frame in each tag