# Vulnerability Verification Report

## 1. Summary
- **Vulnerability Type**: CORS misconfiguration with missing authentication (CWE-942/CWE-306)
- **Flagged Location**: `src/praisonai-agents/praisonaiagents/ui/agui/agui.py:124-140`
- **Vulnerability Description**: The `POST /agui` route exposes agent execution without route-level authentication and returns streaming responses with a hardcoded wildcard `Access-Control-Allow-Origin: *`, allowing untrusted web origins to read agent output.

## 2. Analysis Logic

### Step 1: Examining the flagged CORS sink at `src/praisonai-agents/praisonaiagents/ui/agui/agui.py:124-140`
I read the flagged AGUI implementation. The route is registered as a FastAPI POST endpoint and returns a `StreamingResponse` with hardcoded CORS headers:

```python
@router.post("/agui")
async def run_agent_agui(run_input: RunAgentInput):
    """Run the agent via AG-UI protocol."""
    async def event_generator():
        async for event in self._run_agent(run_input):
            yield encoder.encode(event)

    return StreamingResponse(
        event_generator(),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
            "Access-Control-Allow-Origin": "*",
            "Access-Control-Allow-Methods": "POST, GET, OPTIONS",
            "Access-Control-Allow-Headers": "*",
        },
    )
```

Analysis: This directly satisfies the first CORS decision-tree condition: CORS is explicitly configured. The origin is not restricted; it is the wildcard `*`. The response is a streaming agent output endpoint, not a static asset endpoint.

### Step 2: Checking whether the route or router enforces authentication
The router is constructed without dependencies or authentication middleware:

```python
self._router = APIRouter(prefix=self.prefix, tags=self.tags)

# Attach routes
self._attach_routes(self._router)
```

This is at `src/praisonai-agents/praisonaiagents/ui/agui/agui.py:111-114`. The route itself at `src/praisonai-agents/praisonaiagents/ui/agui/agui.py:124-125` has only the body model parameter:

```python
@router.post("/agui")
async def run_agent_agui(run_input: RunAgentInput):
```

The request model contains only execution data and no credential field (`src/praisonai-agents/praisonaiagents/ui/agui/types.py:159-166`):

```python
class RunAgentInput(BaseModel):
    """Input for running an agent."""
    thread_id: str
    run_id: Optional[str] = None
    messages: Optional[List[Message]] = Field(default_factory=list)
    state: Optional[Dict[str, Any]] = None
    forwarded_props: Optional[Dict[str, Any]] = None
```

I also searched the AGUI package for `Depends`, `Security`, `HTTPBearer`, `APIKey`, `Authorization`, `auth`, `token`, and middleware references and found no matches. Analysis: the route is unauthenticated in the AGUI implementation itself, and there is no model-level token or API-key field that the handler validates.

### Step 3: Tracing attacker-controlled request data into agent execution
The endpoint passes `run_input` into `_run_agent()` (`src/praisonai-agents/praisonaiagents/ui/agui/agui.py:127-128`):

```python
async for event in self._run_agent(run_input):
    yield encoder.encode(event)
```

Inside `_run_agent()`, the request body messages are read, the last user message is extracted, state is accepted, and the resulting `user_input` is passed into either a single agent or a multi-agent workflow (`src/praisonai-agents/praisonaiagents/ui/agui/agui.py:162-185`):

```python
messages = run_input.messages or []

# Extract user input
user_input = extract_user_input(messages)

# Validate state
session_state = validate_state(run_input.state, thread_id)

# Run appropriate handler
if self.agent:
    async for event in async_stream_agent_response(
        agent=self.agent,
        user_input=user_input,
        thread_id=thread_id,
        run_id=run_id,
        session_state=session_state,
        messages=agui_messages_to_praisonai(messages),
    ):
        yield event
```

The extractor returns the last user-controlled message content without sanitization or authorization logic (`src/praisonai-agents/praisonaiagents/ui/agui/conversion.py:194-211`):

```python
def extract_user_input(messages: List[Message]) -> str:
    """
    Extract the last user message content.
    """
    if not messages:
        return ""

    for msg in reversed(messages):
        if msg.role == "user" and msg.content:
            return msg.content

    return ""
```

Analysis: The data-flow chain is request body `RunAgentInput.messages` -> `_run_agent()` -> `extract_user_input()` -> `async_stream_agent_response()`. There is no sanitizer, origin allowlist check, authentication check, or content-type guard in this path.

### Step 4: Confirming the endpoint triggers agent/tool execution and returns the result
`async_stream_agent_response()` invokes the configured agent with the extracted user input (`src/praisonai-agents/praisonaiagents/ui/agui/streaming.py:259-307`):

```python
async def async_stream_agent_response(
    agent,
    user_input: str,
    thread_id: str,
    run_id: str,
    session_state: Optional[Dict[str, Any]] = None,
    messages: Optional[List[Dict[str, Any]]] = None
) -> AsyncIterator[BaseEvent]:
    ...
    if hasattr(agent, 'achat'):
        response = await agent.achat(user_input)
    elif hasattr(agent, 'chat'):
        loop = asyncio.get_event_loop()
        response = await loop.run_in_executor(None, agent.chat, user_input)
    ...
    if response:
        yield TextMessageContentEvent(message_id=message_id, delta=str(response))
```

The `Agent` constructor explicitly supports user-configured tools (`src/praisonai-agents/praisonaiagents/agent/agent.py:203` and `src/praisonai-agents/praisonaiagents/agent/agent.py:272-274`):

```python
tools: Optional[List[Any]] = None,
```

```python
tools (Optional[List[Any]], optional): List of tools, functions, or capabilities
available to the agent for task execution. Can include callables, tool objects,
or MCP instances. Defaults to None.
```

The standard OpenAI client path executes tool calls and appends tool results back into the model conversation (`src/praisonai-agents/praisonaiagents/llm/openai_client.py:951-996`):

```python
if tool_calls and execute_tool_fn:
    ...
    tool_result = execute_tool_fn(function_name, arguments)
    results_str = json.dumps(tool_result) if tool_result else "Function returned an empty output"
    ...
    messages.append({
        "role": "tool",
        "tool_call_id": tool_call.id if hasattr(tool_call, 'id') else tool_call['id'],
        "content": results_str
    })
```

Analysis: A cross-origin request is not merely reaching a harmless status endpoint. It can drive the configured agent with attacker-controlled prompt text, and the resulting assistant response may incorporate tool outputs depending on the configured tools.

### Step 5: Confirming the streamed response is readable cross-origin
The AGUI response uses the SSE encoder (`src/praisonai-agents/praisonaiagents/ui/agui/encoder.py:20-30`):

```python
class EventEncoder:
    """Encodes AG-UI events to SSE format."""
    ...
    def encode(self, event: BaseEvent) -> str:
        return f"data: {event.model_dump_json(by_alias=True, exclude_none=True)}\n\n"
```

The route returns this encoded stream with `media_type="text/event-stream"` and `Access-Control-Allow-Origin: "*"` (`src/praisonai-agents/praisonaiagents/ui/agui/agui.py:130-140`):

```python
return StreamingResponse(
    event_generator(),
    media_type="text/event-stream",
    headers={
        "Cache-Control": "no-cache",
        "Connection": "keep-alive",
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Methods": "POST, GET, OPTIONS",
        "Access-Control-Allow-Headers": "*",
    },
)
```

The test suite also asserts that the AGUI endpoint includes the wildcard header (`src/praisonai-agents/tests/agui/test_router.py:188-210`):

```python
def test_agui_endpoint_cors_headers(self):
    """Test /agui endpoint includes CORS headers."""
    ...
    # Check CORS headers
    assert response.headers.get("access-control-allow-origin") == "*"
```

Analysis: The response body contains serialized AGUI events, including assistant output, and the wildcard origin header authorizes any requesting web origin to read it.

### Step 6: Checking framework/dependency context and code context exclusions
The AGUI feature is packaged source code, not only a test fixture. The package configuration includes `praisonaiagents*` packages (`src/praisonai-agents/pyproject.toml:92-94`):

```toml
[tool.setuptools.packages.find]
where = ["."]
include = ["praisonaiagents*"]
```

FastAPI is an optional runtime dependency for API/AGUI-style usage (`src/praisonai-agents/pyproject.toml:55-58`):

```toml
api = [
    "fastapi>=0.115.0",
    "uvicorn>=0.34.0"
]
```

The AGUI module documents direct application usage by including the router in a FastAPI app (`src/praisonai-agents/praisonaiagents/ui/agui/agui.py:41-50`):

```python
Usage:
    from praisonaiagents import Agent
    from praisonaiagents.ui.agui import AGUI
    from fastapi import FastAPI

    agent = Agent(name="Assistant", role="Helper", goal="Help users")
    agui = AGUI(agent=agent)

    app = FastAPI()
    app.include_router(agui.get_router())
```

The AGUI package `__init__` exports `AGUI` and repeats the same usage pattern (`src/praisonai-agents/praisonaiagents/ui/agui/__init__.py:8-21`):

```python
from praisonaiagents.ui.agui.agui import AGUI

__all__ = ["AGUI"]
```

Analysis: This is library source code intended for consumers to mount into applications. It is not located under a test, example, generated-code, or scaffold-only path. I searched for other `/agui` route registrations and found only this route in source plus tests, so there is no alternate secured AGUI route overriding this implementation.

### Analysis Process
- Q1: Is CORS configured (`Access-Control-Allow-Origin` header set)? → YES. `src/praisonai-agents/praisonaiagents/ui/agui/agui.py:134-140` sets `"Access-Control-Allow-Origin": "*"` in the streaming response headers.
- Q2: Is the origin restricted to a specific allowlist? → NO. The same header block uses the wildcard `*`, and the `AGUI.__init__` signature at `src/praisonai-agents/praisonaiagents/ui/agui/agui.py:61-68` has no allowed-origins parameter to configure an allowlist.
- Q3: Is `Access-Control-Allow-Credentials: true` present? → NO. The AGUI header block at `src/praisonai-agents/praisonaiagents/ui/agui/agui.py:134-140` sets cache, connection, allow-origin, allow-methods, and allow-headers only. However, the endpoint does not require cookie/session credentials: `src/praisonai-agents/praisonaiagents/ui/agui/agui.py:124-125` has no auth dependency and `src/praisonai-agents/praisonaiagents/ui/agui/types.py:159-166` has no credential field.
- Q4: Does the endpoint return sensitive or attacker-relevant data without auth? → YES. `src/praisonai-agents/praisonaiagents/ui/agui/streaming.py:289-303` executes `agent.achat(user_input)` or `agent.chat(user_input)` and yields the result as `TextMessageContentEvent`; `src/praisonai-agents/praisonaiagents/llm/openai_client.py:951-996` shows tool results are appended into the model conversation when tool calls occur.
- Q5: Is the code in test/demo/dead/generated context? → NO. `src/praisonai-agents/pyproject.toml:92-94` packages `praisonaiagents*`, `src/praisonai-agents/praisonaiagents/ui/agui/__init__.py:19-21` exports `AGUI`, and `src/praisonai-agents/praisonaiagents/ui/agui/agui.py:41-50` documents direct FastAPI mounting.
- → Reached leaf: TRUE_POSITIVE.

## 3. Verdict
**TRUE_POSITIVE**

**Key Evidence:**
- `src/praisonai-agents/praisonaiagents/ui/agui/agui.py:124-140` exposes `POST /agui` and hardcodes `Access-Control-Allow-Origin: "*"` on the streaming response.
- `src/praisonai-agents/praisonaiagents/ui/agui/agui.py:111-125` creates the router and route without FastAPI auth dependencies, and `src/praisonai-agents/praisonaiagents/ui/agui/types.py:159-166` defines no credential field.
- `src/praisonai-agents/praisonaiagents/ui/agui/agui.py:162-185` and `src/praisonai-agents/praisonaiagents/ui/agui/streaming.py:289-303` trace request messages into `agent.achat()`/`agent.chat()` and then into streamed response events.

## 4. Remediation Suggestions
- Remove the hardcoded wildcard CORS headers from the AGUI route and let the containing FastAPI application configure CORS with an explicit trusted-origin allowlist.
- Add authentication/authorization support to the AGUI router, such as a FastAPI dependency validating a bearer token or API key before agent execution.
- Add a server-side request content-type check for `application/json` as defense in depth, and re-check that unauthenticated/simple cross-origin browser requests cannot reach agent execution or read streamed output.
