Make WordPress Core

Changeset 65437


Ignore:
Timestamp:
11/12/2026 03:07:24 PM (8 hours ago)
Author:
desrosj
Message:

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:

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.

Location:
trunk/src/wp-includes/ID3
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/ID3/getid3.lib.php

    r62087 r65437  
    747747                                        // This function has been deprecated in PHP 8.0 because in libxml 2.9.0, external entity loading is
    748748                                        // disabled by default, but is still needed when LIBXML_NOENT is used.
    749                                         $loader = @libxml_disable_entity_loader(true);
     749                                        $loader = libxml_disable_entity_loader(true);
     750                                        if ($loader === false && !libxml_disable_entity_loader(true)) {
     751                                                // Failed to disable external entity loading — refuse to parse to prevent XXE
     752                                                return false;
     753                                        }
    750754                                        $XMLobject = simplexml_load_string($XMLstring, 'SimpleXMLElement', GETID3_LIBXML_OPTIONS);
    751755                                        $return = self::SimpleXMLelement2array($XMLobject);
    752                                         @libxml_disable_entity_loader($loader);
     756                                        libxml_disable_entity_loader($loader);
    753757                                        return $return;
    754758                                }
  • trunk/src/wp-includes/ID3/getid3.php

    r63520 r65437  
    388388        protected $startup_warning = '';
    389389
    390         const VERSION           = '1.9.26-202609042051';
     390        const VERSION           = '1.9.28-202610301738';
    391391        const FREAD_BUFFER_SIZE = 32768;
    392392
    … …  
    10091009                                                ),
    10101010
    1011                                 // DTS  - audio       - Dolby Theatre System
     1011                                // DTS  - audio       - Digital Theater Systems
    10121012                                'dts'  => array(
    10131013                                                        'pattern'   => '^\\x7F\\xFE\\x80\\x01',
    … …  
    16071607                        $GetFileFormatArray = $this->GetFileFormatArray();
    16081608                        $info = $GetFileFormatArray['cue'];
    1609                         $info['include']   = 'module.'.$info['group'].'.'.$info['module'].'.php';
     1609                        $info['include'] = 'module.'.$info['group'].'.'.$info['module'].'.php';
    16101610                        return $info;
    16111611                }
    … …  
    16301630                foreach ($array as $key => $value) {
    16311631
    1632                         // go recursive
    1633                         if (is_array($value)) {
     1632                        if ($key == 'picture') { // https://github.com/JamesHeinrich/getID3/issues/508
     1633
     1634                                // pictures are binary data and should not be altered
     1635                                continue;
     1636
     1637                        } elseif (is_array($value)) {
     1638
     1639                                // convert arrays recursively
    16341640                                $this->CharConvert($array[$key], $encoding);
    1635                         }
    1636 
    1637                         // convert string
    1638                         elseif (is_string($value)) {
     1641
     1642                        } elseif (is_string($value)) {
     1643
     1644                                // actually convert only strings
    16391645                                $array[$key] = trim(getid3_lib::iconv_fallback($encoding, $this->encoding, $value));
     1646
    16401647                        }
    16411648                }
    … …  
    16911698                                                        $value = trim($value, " \r\n\t"); // do not trim nulls from $value!! Unicode characters will get mangled if trailing nulls are removed!
    16921699                                                }
    1693                                                 if (isset($value) && $value !== "") {
     1700                                                if (isset($value) && ($value !== '')) {
    16941701                                                        if (!is_numeric($key)) {
    16951702                                                                $this->info['tags'][trim($tag_name)][trim($tag_key)][$key] = $value;
  • trunk/src/wp-includes/ID3/module.audio-video.quicktime.php

    r63520 r65437  
    828828                                        }
    829829
     830                                        // prevent memory exhaustion from crafted files: each "stsd" entry is at least 16 bytes (4 size + 4 data_format + 6 reserved + 2 reference_index)
     831                                        $max_entries = (int) floor(max(0, strlen($atom_data) - 8) / 16);
     832                                        if ($atom_structure['number_entries'] > $max_entries) {
     833                                                $this->warning('QuickTime atom "stsd" claims '.$atom_structure['number_entries'].' entries but only has data for '.$max_entries.', truncating');
     834                                                $atom_structure['number_entries'] = $max_entries;
     835                                        }
    830836                                        $stsdEntriesDataOffset = 8;
    831837                                        for ($i = 0; $i < (int) $atom_structure['number_entries']; $i++) {
    … …  
    11811187                                        $atom_structure['flags_raw']      = getid3_lib::BigEndian2Int(substr($atom_data,  1, 3)); // hardcoded: 0x0000
    11821188                                        $atom_structure['number_entries'] = getid3_lib::BigEndian2Int(substr($atom_data,  4, 4));
     1189                                        // prevent memory exhaustion from crafted files: each "dref" entry is at least 12 bytes (4 size + 4 type + 1 version + 3 flags)
     1190                                        $max_entries = (int) floor(max(0, strlen($atom_data) - 8) / 12);
     1191                                        if ($atom_structure['number_entries'] > $max_entries) {
     1192                                                $this->warning('QuickTime atom "dref" claims '.$atom_structure['number_entries'].' entries but only has data for '.$max_entries.', truncating');
     1193                                                $atom_structure['number_entries'] = $max_entries;
     1194                                        }
    11831195                                        $drefDataOffset = 8;
    11841196                                        for ($i = 0; $i < $atom_structure['number_entries']; $i++) {
    … …  
    13191331                                        $atom_structure['flags_raw']      = getid3_lib::BigEndian2Int(substr($atom_data,  1, 3)); // hardcoded: 0x0000
    13201332                                        $atom_structure['number_entries'] = getid3_lib::BigEndian2Int(substr($atom_data,  4, 4));
     1333                                        // prevent memory exhaustion from crafted files: each "elst" entry is at least 12 bytes (4 track_duration + 4 media_time + 4 media_rate)
     1334                                        $max_entries = (int) floor(max(0, strlen($atom_data) - 8) / 12);
     1335                                        if ($atom_structure['number_entries'] > $max_entries) {
     1336                                                $this->warning('QuickTime atom "elst" claims '.$atom_structure['number_entries'].' entries but only has data for '.$max_entries.', truncating');
     1337                                                $atom_structure['number_entries'] = $max_entries;
     1338                                        }
    13211339                                        for ($i = 0; $i < $atom_structure['number_entries']; $i++ ) {
    13221340                                                $atom_structure['edit_list'][$i]['track_duration'] =   getid3_lib::BigEndian2Int(substr($atom_data, 8 + ($i * 12) + 0, 4));
    … …  
    18011819                                        $atom_structure['flags_raw']     = getid3_lib::BigEndian2Int(substr($atom_data,  1, 3));
    18021820                                        $atom_structure['entry_count']   = getid3_lib::BigEndian2Int(substr($atom_data,  4, 4));
     1821                                        // prevent memory exhaustion from crafted files: each "keys" entry is at least 8 bytes (4 key_size + 4 key_namespace)
     1822                                        $max_entries = (int) floor(max(0, strlen($atom_data) - 8) / 8);
     1823                                        if ($atom_structure['entry_count'] > $max_entries) {
     1824                                                $this->warning('QuickTime atom "keys" claims '.$atom_structure['entry_count'].' entries but only has data for '.$max_entries.', truncating');
     1825                                                $atom_structure['entry_count'] = $max_entries;
     1826                                        }
    18031827                                        $keys_atom_offset = 8;
    18041828
  • trunk/src/wp-includes/ID3/module.tag.id3v2.php

    r63520 r65437  
    496496                                                }
    497497                                                break;
     498                                        case 'replaygain_album_peak':
     499                                                if (empty($info['replay_gain']['album']['peak']) && !empty($txxx_array['data'])) {
     500                                                        $info['replay_gain']['album']['peak'] = floatval($txxx_array['data']);
     501                                                }
     502                                                break;
    498503                                }
    499504                        }
    … …  
    623628                                        $this->warning('gzuncompress() support required to decompress ID3v2 frame "'.$parsedFrame['frame_name'].'"');
    624629                                } else {
    625                                         if ($decompresseddata = @gzuncompress(substr($parsedFrame['data'], 4))) {
    626                                         //if ($decompresseddata = @gzuncompress($parsedFrame['data'])) {
     630                                        // https://github.com/JamesHeinrich/getID3/security/advisories/GHSA-c2xw-vp6w-gpph
     631                                        // Set a limit of 1/4 of PHP memory limit (if known), or default hard cap of 32MB if unknown.
     632                                        $decompression_limit = ($this->getid3->memory_limit > 0) ? (int) round($this->getid3->memory_limit / 4) : 32*1024*1024;
     633                                        if ($decompresseddata = @gzuncompress(substr($parsedFrame['data'], 4), $decompression_limit)) {
    627634                                                $parsedFrame['data'] = $decompresseddata;
    628635                                                unset($decompresseddata);
    … …  
    701708                        }
    702709                        //unset($parsedFrame['data']); do not unset, may be needed elsewhere, e.g. for replaygain
     710
     711
     712                } elseif ((($id3v2_majorversion == 3) && ($parsedFrame['frame_name'] == 'IPLS')) || // 4.4  IPLS Involved people list (ID3v2.3 only)
     713                                (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'IPL')) ||    // 4.4  IPL  Involved people list (ID3v2.2 only)
     714                                (($id3v2_majorversion >= 4) && ($parsedFrame['frame_name'] == 'TIPL')) ||   // 4.2.2 TIPL Involved people list (ID3v2.4 only)
     715                                (($id3v2_majorversion >= 4) && ($parsedFrame['frame_name'] == 'TMCL'))) {   // 4.2.2 TMCL Musician credits list (ID3v2.4 only)
     716                        // http://id3.org/id3v2.3.0#sec4.4
     717                        //   There may only be one 'IPL' frame in each tag
     718                        // <Header for 'Involved people list', ID: 'IPL'>
     719                        // Text encoding     $xx
     720                        // People list strings    <textstrings>
     721
     722                        $frame_offset = 0;
     723                        $frame_textencoding = ord(substr($parsedFrame['data'], $frame_offset++, 1));
     724                        if ((($id3v2_majorversion <= 3) && ($frame_textencoding > 1)) || (($id3v2_majorversion == 4) && ($frame_textencoding > 3))) {
     725                                $this->warning('Invalid text encoding byte ('.$frame_textencoding.') in frame "'.$parsedFrame['frame_name'].'" - defaulting to ISO-8859-1 encoding');
     726                        }
     727                        $parsedFrame['encodingid'] = $frame_textencoding;
     728                        $parsedFrame['encoding']   = $this->TextEncodingNameLookup($parsedFrame['encodingid']);
     729                        $parsedFrame['data_raw']   = (string) substr($parsedFrame['data'], $frame_offset);
     730
     731                        // https://www.getid3.org/phpBB3/viewtopic.php?t=1369
     732                        // "this tag typically contains null terminated strings, which are associated in pairs"
     733                        // "there are users that use the tag incorrectly"
     734                        $IPLS_parts_unsorted = array();
     735                        if (((strlen($parsedFrame['data_raw']) % 2) == 0) && ((substr($parsedFrame['data_raw'], 0, 2) == "\xFF\xFE") || (substr($parsedFrame['data_raw'], 0, 2) == "\xFE\xFF"))) {
     736                                // 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
     737                                $thisILPS = '';
     738                                for ($i = 0; $i < strlen($parsedFrame['data_raw']); $i += 2) {
     739                                        $twobytes = substr($parsedFrame['data_raw'], $i, 2);
     740                                        if ($twobytes === "\x00\x00") {
     741                                                $IPLS_parts_unsorted[] = getid3_lib::iconv_fallback($parsedFrame['encoding'], $info['id3v2']['encoding'], $thisILPS);
     742                                                $thisILPS = '';
     743                                        } else {
     744                                                $thisILPS .= $twobytes;
     745                                        }
     746                                }
     747                                if (strlen($thisILPS) > 0) { // extract the last part if any, even if it's only a BOM with no text following
     748                                        $IPLS_parts_unsorted[] = getid3_lib::iconv_fallback($parsedFrame['encoding'], $info['id3v2']['encoding'], $thisILPS);
     749                                }
     750                        } else {
     751                                // ISO-8859-1 or UTF-8 or other single-byte-null character set
     752                                $IPLS_parts_unsorted = explode("\x00", $parsedFrame['data_raw']);
     753                                if (end($IPLS_parts_unsorted) === '') {
     754                                        // there was a terminating null after the last part and explode appended an empty string; drop it
     755                                        array_pop($IPLS_parts_unsorted);
     756                                }
     757                        }
     758
     759                        $IPLS_parts = array();
     760                        if (count($IPLS_parts_unsorted) == 1) {
     761                                // Just a list of names, e.g. "Dino Baptiste, Jimmy Copley, John Gordon, Bernie Marsden, Sharon Watson".
     762                                // Use the normal output format but with empty roles.
     763                                $IPLS_parts_sorted = preg_split('#[;,\\r\\n\\t]#', $IPLS_parts_unsorted[0]);
     764                                foreach ($IPLS_parts_sorted as $person) {
     765                                        $IPLS_parts[] = array(
     766                                                'position' => '',
     767                                                'person'   => $person
     768                                        );
     769                                }
     770                        }
     771                        else {
     772                                while (\count($IPLS_parts_unsorted) >= 2) {
     773                                        $IPLS_parts[] = array(
     774                                                'position' => array_shift($IPLS_parts_unsorted),
     775                                                'person'   => array_shift($IPLS_parts_unsorted)
     776                                        );
     777                                }
     778                                if (\count($IPLS_parts_unsorted) > 0) {
     779                                        $this->warning("Odd number of {$parsedFrame['frame_name']} parts - expected even number forming role/name pairs");
     780                                        $IPLS_parts[] = array(
     781                                                'position' => array_shift($IPLS_parts_unsorted),
     782                                                'person'   => ''
     783                                        );
     784                                }
     785                        }
     786                        $parsedFrame['data'] = $IPLS_parts;
     787
     788                        if (!empty($parsedFrame['framenameshort']) && !empty($parsedFrame['data'])) {
     789                                $info['id3v2']['comments'][$parsedFrame['framenameshort']] = $parsedFrame['data'];
     790                        }
    703791
    704792
    … …  
    801889                        }
    802890                        unset($parsedFrame['data']);
    803 
    804 
    805                 } elseif ((($id3v2_majorversion == 3) && ($parsedFrame['frame_name'] == 'IPLS')) || // 4.4  IPLS Involved people list (ID3v2.3 only)
    806                                 (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'IPL'))) {     // 4.4  IPL  Involved people list (ID3v2.2 only)
    807                         // http://id3.org/id3v2.3.0#sec4.4
    808                         //   There may only be one 'IPL' frame in each tag
    809                         // <Header for 'User defined URL link frame', ID: 'IPL'>
    810                         // Text encoding     $xx
    811                         // People list strings    <textstrings>
    812 
    813                         $frame_offset = 0;
    814                         $frame_textencoding = ord(substr($parsedFrame['data'], $frame_offset++, 1));
    815                         if ((($id3v2_majorversion <= 3) && ($frame_textencoding > 1)) || (($id3v2_majorversion == 4) && ($frame_textencoding > 3))) {
    816                                 $this->warning('Invalid text encoding byte ('.$frame_textencoding.') in frame "'.$parsedFrame['frame_name'].'" - defaulting to ISO-8859-1 encoding');
    817                         }
    818                         $parsedFrame['encodingid'] = $frame_textencoding;
    819                         $parsedFrame['encoding']   = $this->TextEncodingNameLookup($parsedFrame['encodingid']);
    820                         $parsedFrame['data_raw']   = (string) substr($parsedFrame['data'], $frame_offset);
    821 
    822                         // https://www.getid3.org/phpBB3/viewtopic.php?t=1369
    823                         // "this tag typically contains null terminated strings, which are associated in pairs"
    824                         // "there are users that use the tag incorrectly"
    825                         $IPLS_parts = array();
    826                         if (strpos($parsedFrame['data_raw'], "\x00") !== false) {
    827                                 $IPLS_parts_unsorted = array();
    828                                 if (((strlen($parsedFrame['data_raw']) % 2) == 0) && ((substr($parsedFrame['data_raw'], 0, 2) == "\xFF\xFE") || (substr($parsedFrame['data_raw'], 0, 2) == "\xFE\xFF"))) {
    829                                         // 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
    830                                         $thisILPS  = '';
    831                                         for ($i = 0; $i < strlen($parsedFrame['data_raw']); $i += 2) {
    832                                                 $twobytes = substr($parsedFrame['data_raw'], $i, 2);
    833                                                 if ($twobytes === "\x00\x00") {
    834                                                         $IPLS_parts_unsorted[] = getid3_lib::iconv_fallback($parsedFrame['encoding'], $info['id3v2']['encoding'], $thisILPS);
    835                                                         $thisILPS  = '';
    836                                                 } else {
    837                                                         $thisILPS .= $twobytes;
    838                                                 }
    839                                         }
    840                                         if (strlen($thisILPS) > 2) { // 2-byte BOM
    841                                                 $IPLS_parts_unsorted[] = getid3_lib::iconv_fallback($parsedFrame['encoding'], $info['id3v2']['encoding'], $thisILPS);
    842                                         }
    843                                 } else {
    844                                         // ISO-8859-1 or UTF-8 or other single-byte-null character set
    845                                         $IPLS_parts_unsorted = explode("\x00", $parsedFrame['data_raw']);
    846                                 }
    847                                 if (count($IPLS_parts_unsorted) == 1) {
    848                                         // just a list of names, e.g. "Dino Baptiste, Jimmy Copley, John Gordon, Bernie Marsden, Sharon Watson"
    849                                         foreach ($IPLS_parts_unsorted as $key => $value) {
    850                                                 $IPLS_parts_sorted = preg_split('#[;,\\r\\n\\t]#', $value);
    851                                                 $position = '';
    852                                                 foreach ($IPLS_parts_sorted as $person) {
    853                                                         $IPLS_parts[] = array('position'=>$position, 'person'=>$person);
    854                                                 }
    855                                         }
    856                                 } elseif ((count($IPLS_parts_unsorted) % 2) == 0) {
    857                                         $position = '';
    858                                         $person   = '';
    859                                         foreach ($IPLS_parts_unsorted as $key => $value) {
    860                                                 if (($key % 2) == 0) {
    861                                                         $position = $value;
    862                                                 } else {
    863                                                         $person   = $value;
    864                                                         $IPLS_parts[] = array('position'=>$position, 'person'=>$person);
    865                                                         $position = '';
    866                                                         $person   = '';
    867                                                 }
    868                                         }
    869                                 } else {
    870                                         foreach ($IPLS_parts_unsorted as $key => $value) {
    871                                                 $IPLS_parts[] = array($value);
    872                                         }
    873                                 }
    874 
    875                         } else {
    876                                 $IPLS_parts = preg_split('#[;,\\r\\n\\t]#', $parsedFrame['data_raw']);
    877                         }
    878                         $parsedFrame['data'] = $IPLS_parts;
    879 
    880                         if (!empty($parsedFrame['framenameshort']) && !empty($parsedFrame['data'])) {
    881                                 $info['id3v2']['comments'][$parsedFrame['framenameshort']][] = $parsedFrame['data'];
    882                         }
    883891
    884892
Note: See TracChangeset for help on using the changeset viewer.