Commit 9826725c69 for wordpress.org
commit 9826725c69515758b1bdabea20fcc1bb137983da
Author: desrosj <desrosj@git.wordpress.org>
Date: Thu Nov 12 15:10:00 2026 +0000
External Libraries: Update getID3 to version `1.9.28`.
This update includes `1.9.27` and brings bug fixes and hardening improvements:
- Entry counts in the QuickTime `stsd`, `dref`, `elst`, and `keys` atoms are now capped to the data available, which prevents memory exhaustion from crafted files.
- Decompressing compressed ID3v2 frames is limited to a quarter of the PHP memory limit (or 32MB when unknown).
- `getid3_lib::XML2array()` refuses to parse when external entity loading cannot be disabled (PHP < 8.0 only).
- Embedded picture data is no longer run through character set conversion.
- `replaygain_album_peak` is now read from ID3v2 `TXXX` frames.
The involved people list frames (ID3v2.2 `IPL`, ID3v2.3 `IPLS`) and the ID3v2.4 `TIPL` and `TMCL` frames are now parsed into `position`/`person` pairs. Because `wp_add_id3_tag_data()` stores the first value for each key, the `involved_people_list` and `musician_credits_list` attachment metadata values for newly uploaded audio files will now contain only the first pair. These values are not displayed anywhere in Core, and this only applies to newly uploaded files (preexisting metadata values are unchanged).
Only the bundled audio/video subset was updated. No modules were added, and all bundled files are identical to upstream v1.9.28.
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.28]
Developed in https://github.com/WordPress/wordpress-develop/pull/15878.
Follow-up to r47601, r48278, r52254, r54376, r56975, r61253, r61889, r62087, r63520.
Props desrosj, johnbillion.
Fixes #66646.
Built from https://develop.svn.wordpress.org/trunk@65437
git-svn-id: http://core.svn.wordpress.org/trunk@64576 1a063a9b-81f0-0310-95a4-ce76da25c4cd
diff --git a/wp-includes/ID3/getid3.lib.php b/wp-includes/ID3/getid3.lib.php
index 911876eed6..c0aa1ee3b1 100644
--- a/wp-includes/ID3/getid3.lib.php
+++ b/wp-includes/ID3/getid3.lib.php
@@ -746,10 +746,14 @@ class getid3_lib
// 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/wp-includes/ID3/getid3.php b/wp-includes/ID3/getid3.php
index 4e7ccf3356..a2cdc481aa 100644
--- a/wp-includes/ID3/getid3.php
+++ b/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 @@ class getID3
'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 @@ class getID3
// 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 @@ class getID3
// 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 @@ class getID3
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/wp-includes/ID3/module.audio-video.quicktime.php b/wp-includes/ID3/module.audio-video.quicktime.php
index 161702ba18..d298024e11 100644
--- a/wp-includes/ID3/module.audio-video.quicktime.php
+++ b/wp-includes/ID3/module.audio-video.quicktime.php
@@ -827,6 +827,12 @@ $this->error('HEIF files not currently supported');
}
}
+ // 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 @@ $this->warning('incomplete/incorrect handling of "stsd" with Parrot metadata in
$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 @@ $this->warning('incomplete/incorrect handling of "stsd" with Parrot metadata in
$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 @@ $this->warning('incomplete/incorrect handling of "stsd" with Parrot metadata in
$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/wp-includes/ID3/module.tag.id3v2.php b/wp-includes/ID3/module.tag.id3v2.php
index 51a6b54104..7a5a8fcdaf 100644
--- a/wp-includes/ID3/module.tag.id3v2.php
+++ b/wp-includes/ID3/module.tag.id3v2.php
@@ -495,6 +495,11 @@ class getid3_id3v2 extends getid3_handler
$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 @@ class getid3_id3v2 extends getid3_handler
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 @@ class getid3_id3v2 extends getid3_handler
//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
+ // <Header for 'Involved people list', ID: 'IPL'>
+ // Text encoding $xx
+ // People list strings <textstrings>
+
+ $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.
// <Header for 'Text information frame', ID: 'T000' - 'TZZZ',
@@ -802,86 +890,6 @@ class getid3_id3v2 extends getid3_handler
unset($parsedFrame['data']);
- } 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)
- // http://id3.org/id3v2.3.0#sec4.4
- // There may only be one 'IPL' frame in each tag
- // <Header for 'User defined URL link frame', ID: 'IPL'>
- // Text encoding $xx
- // People list strings <textstrings>
-
- $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
diff --git a/wp-includes/version.php b/wp-includes/version.php
index 80028418f3..a2799374f4 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
*
* @global string $wp_version
*/
-$wp_version = '7.2-beta5-65417';
+$wp_version = '7.2-beta5-65437';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.