
Local Docker lab for CVE-2026-44338, an authentication bypass in PraisonAI's legacy Flask API server.
This lab demonstrates the unauthenticated access condition on the legacy API routes. It intentionally uses a safe route-level reproduction instead of a full PraisonAI deployment, so the proof stays focused on the authentication flaw and does not trigger real agent workflows or external LLM calls.
CVE-2026-44338 affects PraisonAI versions >= 2.5.6 and <= 4.6.33.
In the vulnerable legacy API server, authentication was disabled by default. As a result, an unauthenticated caller who could reach the API server could access /agents and trigger the /chat workflow route without a bearer token.
The issue was fixed in PraisonAI 4.6.34 by changing the default behavior to require authentication unless explicitly disabled.
In the vulnerable version, the legacy API server used insecure authentication defaults:
AUTH_ENABLED = False
AUTH_TOKEN = None
def check_auth():
if not AUTH_ENABLED:
return True
Because check_auth() returned True when authentication was disabled, protected routes failed open.
Affected routes included:
GET /agentsPOST /chatThe patched version changes the default posture so authentication is enabled unless explicitly disabled through configuration.
The core issue was not a complex exploit primitive. It came from insecure defaults in the legacy Flask API server.
v4.6.33In v4.6.33, authentication was disabled by default:
AUTH_ENABLED = False
AUTH_TOKEN = None
The authentication check then failed open:
def check_auth():
if not AUTH_ENABLED:
return True
This means the request was accepted whenever authentication was disabled, even if the caller did not send an Authorization header.
The vulnerable flow was:
AUTH_ENABLED = False
↓
check_auth() returns True
↓
GET /agents is allowed
POST /chat is allowed
↓
unauthenticated caller can access agent metadata and reach the workflow trigger route
The sensitive part is that /chat was not just a status endpoint. It accepted a user message and then called the PraisonAI workflow runner using agents.yaml.
v4.6.34In v4.6.34, the default behavior was changed to require authentication unless the operator explicitly disables it:
AUTH_ENABLED = os.environ.get("PRAISONAI_API_AUTH", "enabled").strip().lower() != "disabled"
AUTH_TOKEN = os.environ.get("PRAISONAI_API_TOKEN") or None
The patched version also improves the token handling behavior:
secrets.compare_digest()127.0.0.1 by default instead of exposing itself on all interfacesThe fixed flow is:
AUTH_ENABLED = True by default
↓
request must include a valid Bearer token
↓
missing or invalid token returns 401
↓
/agents and /chat are no longer reachable anonymously
This lab mirrors that source-level difference:
vuln -> auth disabled by default, unauthenticated requests return 200
patched -> auth required by default, unauthenticated requests return 401
The lab contains two local services:
| Service | URL | Behavior |
|---|---|---|
vuln | http://127.0.0.1:8081 | Reproduces vulnerable fail-open auth behavior |
patched | http://127.0.0.1:8082 | Requires bearer-token authentication |
Both services are bound to 127.0.0.1 only.
The /chat route uses a dummy runner instead of a real PraisonAI workflow. This provides observable proof that the unauthenticated request reaches the workflow trigger path without causing external side effects.
.
├── docker-compose.yml
├── vuln
│ ├── Dockerfile
│ └── start_server.py
├── patched
│ ├── Dockerfile
│ └── start_server.py
├── poc
│ └── poc.py
└── .gitignore
└── README.md
docker compose up --build -d
python3 poc/poc.py
The vulnerable service allows unauthenticated access:
=== vuln ===
[unauthenticated] GET /agents
status: 200
[unauthenticated] POST /chat
status: 200
verdict: LIKELY_VULNERABLE
The patched service blocks unauthenticated access:
=== patched ===
[unauthenticated] GET /agents
status: 401
[unauthenticated] POST /chat
status: 401
verdict: NOT_VULNERABLE_OR_PROTECTED
Final expected summary:
vuln: LIKELY_VULNERABLE
patched: NOT_VULNERABLE_OR_PROTECTED
Check the vulnerable route:
curl -i http://127.0.0.1:8081/agents
Expected vulnerable response:
HTTP/1.1 200 OK
Check the patched route:
curl -i http://127.0.0.1:8082/agents
Expected patched response:
HTTP/1.1 401 UNAUTHORIZED
Server logs should show the difference clearly:
vuln: "GET /agents HTTP/1.1" 200
patched: "GET /agents HTTP/1.1" 401
docker compose down -v
This lab is intended for local security research only.
The PoC does not:
GitHub Advisory: GHSA-6rmh-7xcm-cpxj https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-6rmh-7xcm-cpxj
NVD: CVE-2026-44338 https://nvd.nist.gov/vuln/detail/CVE-2026-44338
OSV: GHSA-6rmh-7xcm-cpxj https://osv.dev/vulnerability/GHSA-6rmh-7xcm-cpxj
Vulnerable source: PraisonAI v4.6.33 src/praisonai/api_server.py
https://raw.githubusercontent.com/MervinPraison/PraisonAI/v4.6.33/src/praisonai/api_server.py
Patched source: PraisonAI v4.6.34 src/praisonai/api_server.py
https://raw.githubusercontent.com/MervinPraison/PraisonAI/v4.6.34/src/praisonai/api_server.py