VYPR
Medium severity6.5NVD Advisory· Published Mar 20, 2026· Updated May 14, 2026

CVE-2026-32889

CVE-2026-32889

Description

tinytag is a Python library for reading audio file metadata. Version 2.2.0 allows an attacker who can supply MP3 files for parsing to trigger a non-terminating loop while the library parses an ID3v2 SYLT (synchronized lyrics) frame. In server-side deployments that automatically parse attacker-supplied files, a single 498-byte MP3 can cause the parsing operation to stop making progress and remain busy until the worker or process is terminated. The root cause is that _parse_synced_lyrics assumes _find_string_end_pos always returns a position greater than the current offset. That assumption is false when no string terminator is present in the remaining frame content. This issue has been fixed in version 2.2.1.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
tinytagPyPI
< 2.2.12.2.1

Affected products

1
  • tinytag/tinytagv5
    Range: >= 2.2.0, < 2.2.1

Patches

3
44e496310f7c

ID3: Bail out if SYLT offset exceeds content length

https://github.com/tinytag/tinytagMatMar 13, 2026via ghsa
4 files changed · +29 0
  • tinytag/tests/samples/synced_lyrics_no_terminator_latin1.mp3+0 0 added
  • tinytag/tests/samples/synced_lyrics_no_terminator_utf16.mp3+0 0 added
  • tinytag/tests/test_all.py+27 0 modified
    @@ -667,6 +667,33 @@
             'duration': 0.1306122448979592,
             'samplerate': 44100,
         }),
    +    ('synced_lyrics_no_terminator_latin1.mp3', {
    +        'other': OtherFields({
    +            'lyrics': ['[18246:35.58]\n[18246:35.58]\n[18246:35.58]\n'
    +                       '[18246:35.58]\n[18246:35.58]\n[18246:35.58]\n'
    +                       '[18246:35.58]\n[18246:35.58]\n[18246:35.58]\n'
    +                       '[18246:35.58]\n[18246:35.58]\n[18246:35.58]'],
    +        }),
    +        'filesize': 498,
    +        'bitrate': 128.0,
    +        'channels': 2,
    +        'duration': 0.026122448979591838,
    +        'samplerate': 44100,
    +    }),
    +    ('synced_lyrics_no_terminator_utf16.mp3', {
    +        'other': OtherFields({
    +            'lyrics': ['[71580:52.86]\n[18175:35.68]\n[18175:35.68]\n'
    +                       '[18175:35.68]\n[18175:35.68]\n[18175:35.68]\n'
    +                       '[18175:35.68]\n[18175:35.68]\n[18175:35.68]\n'
    +                       '[18175:35.68]\n[18175:35.68]\n[18175:35.68]\n'
    +                       '[18175:35.68]'],
    +        }),
    +        'filesize': 507,
    +        'bitrate': 128.0,
    +        'channels': 2,
    +        'duration': 0.026122448979591838,
    +        'samplerate': 44100,
    +    }),
         ('empty.ogg', {
             'other': OtherFields(),
             'duration': 3.684716553287982,
    
  • tinytag/tinytag.py+2 0 modified
    @@ -1233,6 +1233,8 @@ def _parse_synced_lyrics(self, content: bytes) -> str:
                 value = self._decode_string(
                     encoding + content[offset:end_pos]).lstrip('\n')
                 offset = end_pos
    +            if offset + 4 > content_length:
    +                break
                 time = unpack('>I', content[offset:offset + 4])[0]
                 offset += 4
                 if found_line:
    
5cd321521ff0

ID3: Ensure string end position is never smaller than start position

https://github.com/tinytag/tinytagMatMar 13, 2026via ghsa
1 file changed · +4 3
  • tinytag/tinytag.py+4 3 modified
    @@ -1343,13 +1343,14 @@ def _find_string_end_pos(content: bytes,
                                  start_pos: int = 0) -> int:
             # latin1 and utf-8 are 1 byte
             if encoding in {b'\x00', b'\x03'}:
    -            return content.find(b'\x00', start_pos) + 1
    -        end_pos = 0
    +            end_pos = content.find(b'\x00', start_pos)
    +            return start_pos if end_pos < 0 else end_pos + 1
    +        end_pos = -1
             for i in range(start_pos, len(content), 2):
                 if content[i:i + 2] == b'\x00\x00':
                     end_pos = i + 2
                     break
    -        return end_pos
    +        return start_pos if end_pos < 0 else end_pos
     
         def _decode_string(self, value: bytes, language: bool = False) -> str:
             default_encoding = 'ISO-8859-1'
    
4d649b9c314a

ID3: Make synced lyrics available in 'other.lyrics' (LRC format) (#270)

https://github.com/tinytag/tinytagMatDec 15, 2025via ghsa
4 files changed · +63 1
  • tinytag/tests/samples/synced_lyrics_invalid.mp3+0 0 added
  • tinytag/tests/samples/synced_lyrics_milliseconds.mp3+0 0 added
  • tinytag/tests/test_all.py+21 0 modified
    @@ -646,6 +646,27 @@
             'album': 'some album',
             'artist': 'some artist',
         }),
    +    ('synced_lyrics_milliseconds.mp3', {
    +        'other': OtherFields({
    +            'lyrics': ['[00:00.00]\n[00:01.00]first line\n[00:01.55]second '
    +                       'line\n[00:05.99]third line'],
    +        }),
    +        'filesize': 2007,
    +        'bitrate': 57.39124999999999,
    +        'channels': 1,
    +        'duration': 0.1306122448979592,
    +        'samplerate': 44100,
    +    }),
    +    ('synced_lyrics_invalid.mp3', {
    +        'other': OtherFields({
    +            'lyrics': ['\nfirst line\nsecond line\nthird line'],
    +        }),
    +        'filesize': 2007,
    +        'bitrate': 57.39124999999999,
    +        'channels': 1,
    +        'duration': 0.1306122448979592,
    +        'samplerate': 44100,
    +    }),
         ('empty.ogg', {
             'other': OtherFields(),
             'duration': 3.684716553287982,
    
  • tinytag/tinytag.py+42 1 modified
    @@ -798,6 +798,7 @@ class _ID3(TinyTag):
         _EMPTY_FRAME_IDS = {b'\x00\x00\x00\x00', b'\x00\x00\x00'}
         _IMAGE_FRAME_IDS = {b'APIC', b'PIC'}
         _CUSTOM_FRAME_IDS = {b'TXXX', b'TXX'}
    +    _SYNCED_LYRICS_FRAME_IDS = {b'SYLT', b'SLT'}
         _IGNORED_FRAME_IDS = {
             b'AENC', b'CRA',
             b'APIC', b'PIC',
    @@ -826,7 +827,6 @@ class _ID3(TinyTag):
             b'SEEK',
             b'SIGN',
             b'SYTC', b'STC',
    -        b'SYLT', b'SLT',
         }
         _ID3V1_TAG_SIZE = 128
         _MAX_ESTIMATION_SEC = 30.0
    @@ -1209,6 +1209,44 @@ def _parse_image(self,
             return self._create_tag_image(
                 content[desc_end_pos:], pic_type, mime_type, desc)
     
    +    @staticmethod
    +    def _lrc_timestamp(seconds: float) -> str:
    +        cs = int(seconds * 100)
    +        minutes, cs = divmod(cs, 6000)
    +        seconds, cs = divmod(cs, 100)
    +        return f"{minutes:02d}:{seconds:02d}.{cs:02d}"
    +
    +    def _parse_synced_lyrics(self, content: bytes) -> str:
    +        # Convert ID3 synced lyrics to LRC format
    +        content_length = len(content)
    +        encoding = content[:1]
    +        # skip language (3)
    +        timestamp_format = content[4:5]
    +        # skip content type (1)
    +        start_pos = 6
    +        end_pos = self._find_string_end_pos(content, encoding, start_pos)
    +        lyrics = ""
    +        offset = end_pos
    +        found_line = False
    +        while offset < content_length:
    +            end_pos = self._find_string_end_pos(content, encoding, offset)
    +            value = self._decode_string(
    +                encoding + content[offset:end_pos]).lstrip('\n')
    +            offset = end_pos
    +            time = unpack('>I', content[offset:offset + 4])[0]
    +            offset += 4
    +            if found_line:
    +                lyrics += '\n'
    +            found_line = True
    +            if timestamp_format == b'\x02':
    +                # time in milliseconds
    +                timestamp = self._lrc_timestamp(time / 1000)
    +            else:
    +                lyrics += value
    +                continue
    +            lyrics += f'[{timestamp}]{value}'
    +        return lyrics
    +
         def _parse_frame(self,
                          fh: BinaryIO,
                          total_size: int,
    @@ -1275,6 +1313,9 @@ def _parse_frame(self,
                     should_set_field = False
                 if should_set_field:
                     self._set_field(fieldname, value)
    +        elif self._parse_tags and frame_id in self._SYNCED_LYRICS_FRAME_IDS:
    +            lyrics = self._parse_synced_lyrics(fh.read(frame_size))
    +            self._set_field('other.lyrics', lyrics)
             elif self._parse_tags and frame_id in self._CUSTOM_FRAME_IDS:
                 # custom fields
                 value = self._decode_string(fh.read(frame_size))
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.