Scope and safety. This article analyzes a public vulnerability record I reported, together with its defensive implications. The reproduction shown below is the local, non-destructive status-code check published in the advisory; it contains no weaponized payload, no exploitation of third-party systems, and no instructions for attacking deployments you do not own.
Executive summary
CVE-2026-55533 is an authentication fail-open condition in the PraisonAI Recipe HTTP server, affecting versions before 4.6.58. The server supports three authentication modes — none, api-key, and jwt. When an operator selects api-key or jwt but the corresponding secret is absent, the middleware does not reject the request and does not refuse to start. It simply passes the request through.
The result is a server that reports itself as authenticated while behaving exactly like auth: none. The condition is made externally reachable by a second defect: the CLI safety check that guards non-localhost binding verifies only that the mode name is not "none". It never verifies that a usable credential exists. A command such as praisonai recipe serve --host 0.0.0.0 --auth api-key therefore passes the guard, binds to every interface, and accepts anonymous requests.
The issue is classified as CWE-287: Improper Authentication and CWE-306: Missing Authentication for Critical Function. The GitHub advisory rates it High, with the CVSS 3.1 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:N — a base score of 8.2 by the CVSS 3.1 formula.
PraisonAI fixed the issue in 4.6.58. Operators should upgrade, then confirm that a real secret is present in every environment where authentication is nominally enabled.
Evidence labels used below:
- Verified means stated by the linked CVE, GitHub advisory, or project release material.
- Inferred means a deployment or architectural consequence derived from those facts and clearly identified as such.
Key facts
| Item | Finding | Confidence |
|---|---|---|
| Identifier | CVE-2026-55533 | Verified |
| Advisory | GHSA-gfq8-hmph-9gjv | Verified |
| Reporter | hoanggxyuuki | Verified (advisory credits) |
| Analyst | NguyenHuyTrung | Verified (advisory credits) |
| Package | PraisonAI (pip) | Verified |
| Affected versions | Versions before 4.6.58 | Verified |
| Fixed version | 4.6.58 | Verified |
| Weakness | Improper authentication (CWE-287), missing authentication (CWE-306) | Verified |
| Component | Recipe HTTP server, src/praisonai/praisonai/recipe/serve.py |
Verified |
| Secondary component | CLI serve guard, src/praisonai/praisonai/cli/features/recipe.py |
Verified |
| Trigger condition | auth set to api-key or jwt with no key or secret configured |
Verified |
| Exposed endpoints | POST /v1/recipes/run, /v1/recipes/stream, /v1/recipes/validate, and POST /admin/reload when admin is enabled |
Verified |
| Severity | High, AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:N |
Verified (vector); score 8.2 derived from the vector |
| EPSS | 26th percentile at time of publication | Verified |
| Fix commit | MervinPraison/PraisonAI@2f9677a |
Verified |
Why fail-open authentication is worse than no authentication
An unauthenticated service is at least honest about what it is. An operator who runs a service with auth: none knows they are responsible for every layer of protection in front of it, and will usually put the service behind a network boundary and treat it accordingly.
A fail-open service breaks that contract. The operator selects an authentication mode, the process starts without complaint, the configuration file and the deployment manifest both say auth: api-key, and every audit artifact agrees that the endpoint is protected. Nothing in the running system contradicts the belief — until an anonymous request succeeds.
Three properties make this class especially damaging:
- It is silent. There is no warning log, no startup failure, and no different response shape. The only observable difference is that a request without a credential returns
200instead of401. - It is invisible to configuration review. Reading the deployment manifest confirms the intent, not the effect. A reviewer sees
--auth api-keyand stops looking. - It can appear after a successful deployment. Inferred: the secret does not have to be forgotten at design time. A rotated key that is never re-injected, a Kubernetes Secret that fails to mount, a
.envfile missing from a rebuilt image, or a CI variable scoped to the wrong environment all produce the same outcome — a service that was authenticated yesterday and is anonymous today, with no signal that anything changed.
For an agent framework the stakes are higher than for a typical CRUD API, because the endpoints that fall open do not return data — they execute work.
Root-cause analysis
The middleware allows the request when no secret exists
The Recipe server builds its authentication middleware in create_auth_middleware(). The API-key branch resolves the expected credential from an explicit argument or from the environment, and then makes a decision that determines the entire outcome of this CVE:
1 | expected_key = api_key or os.environ.get("PRAISONAI_API_KEY") |
The JWT branch is structurally identical:
1 | secret = jwt_secret or os.environ.get("PRAISONAI_JWT_SECRET") |
Read in isolation, each branch looks like a reasonable convenience: if nothing is configured, do not get in the user’s way. The comment even states the intent plainly. The problem is that “nothing is configured” is being treated as “the operator does not want authentication,” when the surrounding configuration has already established the opposite — the operator explicitly asked for api-key or jwt.
An absent secret is not a request to disable a control. It is a broken control, and a broken security control must fail closed.
The middleware is attached, which makes the failure convincing
The server only installs the middleware when a mode other than none is selected:
1 | auth_type = config.get("auth") |
This is worth noting because it means the middleware really is present in the ASGI stack. Anything that introspects the application — a startup log, a health check, a debugging session — will confirm that authentication middleware is installed. It is installed, mounted, and running. It just returns call_next(request) for every caller.
The CLI guard checks the label, not the capability
The path that turns a local weakness into a network-reachable one is the non-localhost safety check in cmd_serve:
1 | if host != "127.0.0.1" and host != "localhost" and auth == "none": |
The intent behind this guard is exactly right: do not let a user publish this service to every interface without authentication. The implementation checks the wrong predicate. It asks “has the operator named an authentication mode?” when the invariant it is trying to enforce is “will an anonymous request be rejected?”
Those two questions have the same answer in every case except the one that matters. So the following command passes the guard, prints no warning, and binds to all interfaces with no effective authentication:
1 | praisonai recipe serve --host 0.0.0.0 --auth api-key |
Verified: the advisory confirms that with no --api-key and no PRAISONAI_API_KEY in the environment, requests are still accepted.
The two defects compose
Neither defect is maximally severe alone. The middleware fail-open on a loopback-bound server is a local exposure. The CLI guard is only a guard — it protects nothing on its own. Together they form a complete path: the guard grants permission to bind publicly on the strength of a control that the middleware has quietly disabled.
1 | Operator intent: "expose the recipe server, but require an API key" |
Reproduction: a status-code differential
The published proof of concept is deliberately boring, and that is the point. It does not exploit anything. It builds the application in-process with Starlette’s TestClient, with the relevant environment variables removed, and compares status codes across three configurations.
1 | # Ensure no secrets are present in the environment. |
The observed behaviour on an affected version:
| Configuration | Request | Status |
|---|---|---|
auth: api-key, no key |
GET /openapi.json |
200 |
auth: api-key, no key |
POST /admin/reload |
200 |
auth: api-key, key set |
POST /admin/reload, no key sent |
401 |
auth: api-key, key set |
POST /admin/reload, wrong key |
401 |
auth: api-key, key set |
POST /admin/reload, correct key |
200 |
auth: jwt, no secret |
GET /openapi.json |
200 |
The middle three rows are the control group: with a key configured, the middleware rejects correctly, which proves the enforcement logic itself is sound. The failure is entirely in the decision about what to do when the secret is missing. A single test asserting 401 for the no-secret case would have caught this, which is the practical lesson at the end of this article.
Impact and deployment-dependent blast radius
Verified impact
- An unauthenticated attacker can reach Recipe server endpoints on a deployment where the operator selected
api-keyorjwtauthentication. - Recipe execution endpoints —
POST /v1/recipes/runandPOST /v1/recipes/stream— are reachable without credentials. - When
enable_adminis set,POST /admin/reloadis reachable without credentials. - The CVSS vector records network attack vector, low complexity, no privileges, and no user interaction, with High integrity impact and Low confidentiality impact.
The integrity-weighted vector is the correct reading. This is not primarily a data-disclosure bug; it is an unauthorized-execution bug. The attacker’s main capability is to make the server do things.
Deployment-dependent consequences
The following are inferred, and depend entirely on which recipes a given installation exposes and what those recipes are permitted to do:
- invocation of arbitrary registered recipe workflows, including any that call tools, write files, or reach internal services;
- consumption of the operator’s LLM provider quota and budget through unauthenticated recipe runs;
- use of the server’s network position to reach services that trust it;
- exposure of whatever recipe definitions, names, and parameter schemas the API surface reveals;
- administrative reload operations that pick up changed recipe definitions, where the admin endpoint is enabled;
- denial of service through resource exhaustion, though availability is rated
A:Nin the official vector.
The upper bound is set by the recipes themselves. A deployment whose recipes only summarize text is materially different from one whose recipes have filesystem or shell tooling attached — but in both cases the operator believed the endpoint required a credential, and it did not.
Safe validation and detection
Configuration review
- Inventory PraisonAI installations and record the version. Flag anything before 4.6.58.
- Find every place the Recipe server is started: shell scripts, systemd units, Dockerfiles, Compose files, Kubernetes manifests, CI jobs, and process supervisors.
- For each, extract the
--host/hostand--auth/authvalues. - For each invocation where
authisapi-keyorjwt, confirm that a secret is actually present in the environment the process runs in — not merely referenced in a template. A${PRAISONAI_API_KEY}reference that resolves to an empty string is the vulnerable case. - Treat any host other than
127.0.0.1,localhost, or::1as published until proven otherwise, including binds to0.0.0.0behind a network policy you have not read this quarter.
Non-destructive runtime check
On a system you own or are authorized to assess, the cheapest confirmation is a single credential-free request to a harmless endpoint:
1 | # On your own deployment only. A 200 here means auth is not being enforced. |
401 means the control is effective. 200 on a server configured for api-key or jwt means it is not. No recipe is executed and no state is changed by this request.
Log and telemetry review
Look for:
- successful
2xxresponses to/v1/recipes/run,/v1/recipes/stream, or/admin/reloadon requests that carried noAuthorizationorX-API-Keyheader; - requests to the recipe endpoints from source addresses outside the expected client set;
- recipe executions that do not correlate with a known upstream job, schedule, or user session;
/admin/reloadcalls at any time they were not part of a deployment;- LLM provider usage or spend that does not match your own workload;
- discovery-shaped traffic against
/openapi.jsonfollowed by direct endpoint calls.
Inferred: if your access logs do not record whether an authentication header was present, add that field before you need it. Reconstructing which historical requests were anonymous is far harder after the fact.
Remediation and hardening
Required remediation
Upgrade to PraisonAI 4.6.58 or later, then verify — do not assume — that each environment has a real secret. The upgrade converts a silent pass-through into an enforced rejection, which means a deployment that has been quietly running without a secret will begin returning 401 after the upgrade. That is the fix working correctly. Plan for it: check every environment’s secret injection before the rollout rather than discovering the gap through a broken client.
Defense in depth
- Bind the Recipe server to loopback and place an authenticated, fail-closed reverse proxy in front of it.
- Do not publish agent-execution APIs directly to the internet.
- Make the secret’s presence a startup precondition in your own orchestration: fail the container’s readiness check if the variable is empty, so a missing secret becomes a failed deploy instead of an open endpoint.
- Alert on empty-but-required environment variables in your deployment pipeline.
- Leave
enable_adminoff unless an administrative workflow genuinely requires it, and expose it on a separate, more tightly restricted path when it does. - Restrict callers by network policy and service identity, not only by credential.
- Scope the runtime: non-root user, minimal mounts, restricted egress, and no ambient cloud credentials that recipes could reach.
- Apply rate limits and per-run resource ceilings so that anonymous or stolen access cannot be converted into unbounded provider spend.
- If you cannot rule out that a vulnerable version was reachable without a secret, rotate the credentials the process could access and review recipe execution history for the exposure window.
Network controls are compensating controls, not a substitute for the upgrade. An internal caller, a compromised neighbouring workload, or a misrouted ingress can all reach an endpoint that was never really protected.
Timeline
| Date | Event | Source certainty |
|---|---|---|
| 2026-06-13 | GitHub shows the advisory published in MervinPraison/PraisonAI, alongside the v4.6.58 release containing fix commit 2f9677a. |
Primary advisory and release page |
| Around 2026-08-25 | GitHub records publication to the GitHub Advisory Database, with review completed and metadata last updated around the same time. | Advisory page, displayed as a relative timestamp at time of writing |
| 2026-08-29 | This analysis published. | This site |
Advisory records are reserved, published, reviewed, and enriched at different times, and GitHub displays some of these as relative timestamps that drift as the page is re-rendered. Read the live records for authoritative chronology; nothing here should be treated as the vendor-notification or discovery date.
Lessons for builders
Fail closed, always
A security control that cannot function must refuse to serve, not step aside. The single-line change in posture is:
1 | # Vulnerable shape. |
Returning 500 rather than 401 is deliberate here: the caller did nothing wrong, and the operator needs a distinguishable signal. Better still, refuse at startup — see below.
Validate configuration once, at startup, not per request
Per-request handling of a missing secret means the same broken state is re-evaluated thousands of times and never surfaces. Configuration is not request data: resolve it when the application is constructed, and raise if the combination is incoherent.
1 | # Defensive pseudocode. |
A process that will not start is a loud, immediate, unmissable failure. A process that starts and silently accepts anonymous traffic is the worst possible outcome of the same mistake.
Guards must test the invariant they promise
The CLI check enforced auth != "none" while promising “auth required for non-localhost binding.” Whenever a guard’s condition and its error message describe different things, that gap is a bug waiting to be found. The guard should have called the same resolution function the middleware uses, and asked whether it produced a usable credential.
This generalizes: a check on a setting’s name is not a check on a capability. Enforce on the resolved value, not on the label the operator typed.
Test the negative path
Every one of the failing behaviours here is one assertion away from being caught:
1 | def test_api_key_auth_without_key_rejects(): |
Positive-path tests confirm that a correct credential works — and the control group in the reproduction above proves those tests passed. The missing test is the one that asserts what happens when the server itself is misconfigured. For any security control, write the test that describes what must never succeed.
Treat agent execution endpoints as privileged by default
A recipe endpoint is not a data endpoint. It runs workflows, spends money, touches tools, and may reach internal systems. Defaults for such surfaces should be conservative — loopback binding, admin off, and an explicit, loudly-confirmed decision required before anything becomes reachable outside a private control plane.
Related reading
- CVE-2026-55529: PraisonAI MCP Origin Validation Bypass — the second issue fixed in PraisonAI 4.6.58, where a
startswith()origin check lets a malicious webpage reach a local MCP server. - CVE-2026-57572: Crawl4AI Unauthenticated RCE via Chromium Argument Injection — a related pattern in AI infrastructure, where a flexible configuration field crosses a process-launch trust boundary.
References
- CVE.org record: CVE-2026-55533
- NIST National Vulnerability Database: CVE-2026-55533
- GitHub Security Advisory: GHSA-gfq8-hmph-9gjv
- PraisonAI v4.6.58 release
- Fix commit 2f9677a
- CWE-287: Improper Authentication
- CWE-306: Missing Authentication for Critical Function
Public vulnerability records change as maintainers and databases add references or revise metadata. Recheck the primary sources above during remediation and incident response.