VDB
EN
HIGH 8.2

GHSA-gfq8-hmph-9gjv

PraisonAI: Authentication fail-open in Recipe server allows unauthenticated access when API key or JWT auth is configured without a secret

빠른 조치

GHSA-gfq8-hmph-9gjv — praisonai: 아래 명령으로 수정 버전으로 올리세요.

pip install --upgrade 'praisonai>=4.6.58'

상세

### Summary

The PraisonAI Recipe HTTP server silently allows unauthenticated requests when `auth` is configured as `api-key` or `jwt` but the corresponding secret is missing.

This 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.

The 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.

### Details

The Recipe server documents the following authentication modes:

- `none` - `api-key` - `jwt`

Relevant source locations:

- `src/praisonai/praisonai/recipe/serve.py` - `src/praisonai/praisonai/cli/features/recipe.py`

In `create_auth_middleware()`, the API key middleware resolves the expected key as:

```python 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) ```

This means `auth: api-key` does not enforce authentication if `api_key` / `PRAISONAI_API_KEY` is missing.

The JWT middleware has the same fail-open behavior:

```python secret = jwt_secret or os.environ.get("PRAISONAI_JWT_SECRET") if not secret: return await call_next(request) ```

The auth middleware is still attached when `auth` is configured:

```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"), ) if auth_middleware: middleware.append(Middleware(auth_middleware)) ```

The 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"`:

```python if host != "127.0.0.1" and host != "localhost" and auth == "none": self._print_error("Auth required for non-localhost binding. Use --auth api-key or --auth jwt") return self.EXIT_POLICY_DENIED ```

Therefore, this command passes the safety check:

```bash praisonai recipe serve --host 0.0.0.0 --auth api-key ```

However, if no `--api-key` or `PRAISONAI_API_KEY` is configured, requests are still accepted without authentication.

Affected endpoints include:

- `POST /v1/recipes/run` - `POST /v1/recipes/stream` - `POST /v1/recipes/validate` - optional `POST /admin/reload` when `enable_admin` is true

### PoC

The following local PoC verifies that `api-key` and `jwt` authentication fail open when the corresponding secret is missing.

Run from the repository root with test dependencies installed:

```bash python3 poc_recipe_auth_fail_open.py ```

`poc_recipe_auth_fail_open.py`:

```python import os import sys from pathlib import Path

from starlette.testclient import TestClient

ROOT = Path.cwd() sys.path.insert(0, str(ROOT / "src" / "praisonai")) sys.path.insert(0, str(ROOT / "src" / "praisonai-agents"))

# Ensure no secrets are present in the environment. os.environ.pop("PRAISONAI_API_KEY", None) os.environ.pop("PRAISONAI_JWT_SECRET", None)

from praisonai.recipe.serve import create_app

# api-key auth selected, but no key configured. app_open = create_app({"auth": "api-key", "enable_admin": True}) client_open = TestClient(app_open)

print("api-key auth with missing key:") print("GET /openapi.json:", client_open.get("/openapi.json").status_code) print("POST /admin/reload:", client_open.post("/admin/reload").status_code)

# api-key auth selected with an actual key configured. app_closed = create_app({ "auth": "api-key", "api_key": "expected", "enable_admin": True, }) client_closed = TestClient(app_closed)

print("\napi-key auth with configured key:") print("missing key:", client_closed.post("/admin/reload").status_code) print("wrong key:", client_closed.post( "/admin/reload", headers={"X-API-Key": "wrong"}, ).status_code) print("correct key:", client_closed.post( "/admin/reload", headers={"X-API-Key": "expected"}, ).status_code)

# jwt auth selected, but no JWT secret configured. app_jwt_open = create_app({"auth": "jwt"}) client_jwt_open = TestClient(app_jwt_open)

print("\njwt auth with missing secret:") print("GET /openapi.json:", client_jwt_open.get("/openapi.json").status_code) ```

Observed output:

```text api-key auth with missing key: GET /openapi.json: 200 POST /admin/reload: 200

api-key auth with configured key: missing key: 401 wrong key: 401 correct key: 200

jwt auth with missing secret: GET /openapi.json: 200 ```

The 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.

### Impact

In an exposed deployment, an unauthenticated attacker can access Recipe server endpoints even though the operator selected `api-key` or `jwt` authentication.

This gives unauthenticated access to recipe execution endpoints such as:

- `POST /v1/recipes/run` - `POST /v1/recipes/stream`

If admin endpoints are enabled, the attacker can also access:

- `POST /admin/reload`

The 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.

이 버전이 영향받나요?

사용 중인 패키지 버전을 입력하면 즉시 평가합니다.

영향 패키지

PyPI / praisonai
최초 영향 버전: 0 수정 버전: 4.6.58
수정 pip install --upgrade 'praisonai>=4.6.58'

참고