GitHub Security Advisory: GHSA-8x8f-54wf-vv92
Package: praisonaiagents (PIP)
Severity: CRITICAL (CVSS 9.1)
CWE: CWE-306: Missing Authentication for Critical Function
Vulnerable versions: <= 1.5.139
Patched in: 1.5.140

Summary: PraisonAI Browser Server allows unauthenticated WebSocket clients to hijack connected extension sessions

Details:
### Summary
`praisonai browser start` exposes the browser bridge on `0.0.0.0` by default, and its `/ws` endpoint accepts websocket clients that omit the `Origin` header entirely. An unauthenticated network client can connect as a fake controller, send `start_session`, cause the server to forward `start_automation` to another connected browser-extension websocket, and receive the resulting action/status stream back over that hijacked session. This allows unauthorized remote use of a connected browser automation session without any credentials.

### Details
The issue is in the browser bridge trust model. The code assumes that websocket peers are trusted local components, but that assumption is not enforced.

Relevant code paths:

- Default network exposure: `src/praisonai/praisonai/browser/server.py:38-44` and `src/praisonai/praisonai/browser/cli.py:25-30`
- Optional-only origin validation: `src/praisonai/praisonai/browser/server.py:156-173`
- Unauthenticated `start_session` routing: `src/praisonai/praisonai/browser/server.py:237-240` and `src/praisonai/praisonai/browser/server.py:289-302`
- Cross-connection forwarding to any other idle websocket: `src/praisonai/praisonai/browser/server.py:344-356`
- Broadcast of action output back to the initiating unauthenticated client: `src/praisonai/praisonai/browser/server.py:412-423` and `src/praisonai/praisonai/browser/server.py:462-476`

The handshake logic only checks origin when an `Origin` header is present:

```python
origin = websocket.headers.get("origin")
if origin:
    ...
    if not is_allowed:
        await websocket.close(code=1008)
        return

await websocket.accept()
```

This means a non-browser client can omit `Origin` completely and still be accepted.

After that, any connected client can send `{"type":"start_session", ...}`. The server then looks for the first other websocket without a session and sends it a `start_automation` message:

```python
if client_conn != conn and client_conn.websocket and not client_conn.session_id:
    await client_conn.websocket.send_text(json_mod.dumps(start_msg))
    client_conn.session_id = session_id
    sent_to_extension = True
    break
```

When the extension-side connection responds with an observation, the resulting action is broadcast to every websocket with the same `session_id`, including the unauthenticated initiating client:

```python
action_response = {
    "type": "action",
    "session_id": session_id,
    **action,
}

for client_id, client_conn in self._connections.items():
    if client_conn.session_id == session_id and client_conn != conn:
        await client_conn.websocket.send_json(action_response)
```

I verified this on the latest local checkout: `praisonai` version `4.5.134` at commit `365f75040f4e279736160f4b6bdb2bdb7a3968d4`.

### PoC
I used `tmp/pocs/poc.sh` to reproduce the issue from a clean local checkout.

Run:

```bash
cd "/Users/r1zzg0d/Documents/CVE hunting/targets/PraisonAI"
./tmp/pocs/poc.sh
```

Expected vulnerable ou