{"id":"CVE-2026-39308","aliases":["GHSA-r9x3-wx45-2v7f","PYSEC-2026-2922"],"title":"PraisonAI recipe registry publish path traversal allows out-of-root file write","summary":"PraisonAI recipe registry publish path traversal allows out-of-root file write","severity":"high","cvss":7.1,"cvssVector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:L","vendor":"praisonai","product":"praisonai","ecosystem":"pip","affected":["praisonai < 4.5.113"],"patched":["praisonai 4.5.113"],"published":"2026-04-06","updated":"2026-07-13","source":"OSV","sourceUrl":"https://osv.dev/vulnerability/GHSA-r9x3-wx45-2v7f","references":[{"url":"https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-r9x3-wx45-2v7f"},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-39308"},{"url":"https://github.com/MervinPraison/PraisonAI"},{"url":"https://github.com/MervinPraison/PraisonAI/releases/tag/v4.5.113"}],"tags":["osv","pip"],"epss":0.00334,"epssPercentile":0.26931,"ingestedAt":"2026-07-13T18:58:02.604Z","slug":"CVE-2026-39308","body":"## Overview\n\n### Summary\n\nPraisonAI's recipe registry publish endpoint writes uploaded recipe bundles to a filesystem path derived from the bundle's internal `manifest.json` before it verifies that the manifest `name` and `version` match the HTTP route. A malicious publisher can place `../` traversal sequences in the bundle manifest and cause the registry server to create files outside the configured registry root even though the request is ultimately rejected with HTTP `400`.\n\nThis is an arbitrary file write / path traversal issue on the registry host. It affects deployments that expose the recipe registry publish flow. If the registry is intentionally run without a token, any network client that can reach the service can trigger it. If a token is configured, any user with publish access can still exploit it.\n\n### Details\n\nThe bug is caused by the order of operations between the HTTP handler and the registry storage layer.\n\n1. `RegistryServer._handle_publish()` in `src/praisonai/praisonai/recipe/server.py:370-426` parses `POST /v1/recipes/{name}/{version}`, writes the uploaded `.praison` file to a temporary path, and immediately calls:\n\n```python\nresult = self.registry.publish(tmp_path, force=force)\n```\n\n2. `LocalRegistry.publish()` in `src/praisonai/praisonai/recipe/registry.py:214-287` opens the uploaded tarball, reads `manifest.json`, and trusts the attacker-controlled `name` and `version` fields:\n\n```python\nname = manifest.get(\"name\")\nversion = manifest.get(\"version\")\nrecipe_dir = self.recipes_path / name / version\nrecipe_dir.mkdir(parents=True, exist_ok=True)\nbundle_name = f\"{name}-{version}.praison\"\ndest_path = recipe_dir / bundle_name\nshutil.copy2(bundle_path, dest_path)\n```\n\n3. Validation helpers already exist in the same file:\n\n```python\ndef _validate_name(name: str) -> bool:\ndef _validate_version(version: str) -> bool:\n```\n\nbut they are not called before the filesystem write.\n\n4. Only after `publish()` returns does the route compare the manifest values with the URL values:\n\n```python\nif result[\"name\"] != name or result[\"version\"] != version:\n    self.registry.delete(result[\"name\"], result[\"version\"])\n    return self._error_response(...)\n```\n\nAt that point the out-of-root artifact has already been created. The request returns an error, but the write outside the registry root remains on disk.\n\nVerified vulnerable behavior:\n\n- Request path: `/v1/recipes/safe/1.0.0`\n- Internal manifest name: `../../outside-dir`\n- Server response: HTTP `400`\n- Leftover artifact: `/tmp/praisonai-publish-traversal-poc/outside-dir-1.0.0.praison`\n\nThis demonstrates that the write occurs before the consistency check and rollback.\n\n### PoC\n\nRun the single verification script from the checked-out repository:\n\n```bash\ncd \"/Users/r1zzg0d/Documents/CVE hunting/targets/PraisonAI\"\npython3 tmp/pocs/poc.py\n```\n\nExpected vulnerable output:\n\n```text\n[+] Publish response status: 400\n{\n  \"ok\": false,\n  \"error\": \"Bundle name/version (../../outside-dir@1.0.0) doesn't match URL (safe@1.0.0)\",\n  \"code\": \"error\"\n}\n[+] Leftover artifact exists: True\n[+] Artifact under registry root: False\n[+] RESULT: VULNERABLE - upload was rejected, but an out-of-root artifact was still created.\n```\n\nThen verify the artifact manually:\n\n```bash\nls -l /tmp/praisonai-publish-traversal-poc/outside-dir-1.0.0.praison\nfind /tmp/praisonai-publish-traversal-poc -maxdepth 2 | sort\n```\n\nWhat the script does internally:\n\n1. Starts a local PraisonAI recipe registry server.\n2. Builds a malicious `.praison` bundle whose internal `manifest.json` contains `name = ../../outside-dir`.\n3. Uploads that bundle to the apparently safe route `/v1/recipes/safe/1.0.0`.\n4. Receives the expected `400` mismatch error.\n5. Confirms that `outside-dir-1.0.0.praison` was still written outside the configured registry directory.\n\n### Impact\n\nThis is a path traversal / arbitrary file write vulnerability in the recipe registry publish flow.\n\nImpacted parties:\n\n- Registry operators running the PraisonAI recipe registry service.\n- Any deployment that allows remote recipe publication.\n- Any environment where adjacent writable filesystem locations contain sensitive application data, service files, or staged content that could be overwritten or planted.\n\nSecurity impact:\n\n- Integrity impact is high because an attacker can create or overwrite files outside the registry root.\n- Availability impact is possible if the attacker targets adjacent runtime or application files.\n- The issue can be chained with other local loading or deployment behaviors if nearby files are later consumed by another component.\n\n### Remediation\n\n1. Validate `manifest.json` `name` and `version` before any path join or filesystem write. Reject path separators, `..`, absolute paths, and any value that fails the existing `_validate_name()` / `_validate_version()` checks.\n\n2. Resolve the final destination path and enforce that it remains under the configured registry root before calling `mkdir()` or `copy2()`. For example, compare the resolved destination against `self.recipes_path.resolve()`.\n\n3. Move the URL-to-manifest consistency check ahead of `self.registry.publish(...)`, or refactor `publish()` so it receives already-validated route parameters instead of trusting attacker-controlled manifest values for storage paths.\n\n## Affected packages\n\n- `praisonai < 4.5.113`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `praisonai 4.5.113`","depth":"twilight","depthScore":39,"depthScoreParts":{"impact":39.1,"likelihood":0.1,"exploitation":0,"ransomware":0},"changes":[]}