Captaince
### Description The Server Action `testLLMProviderAction` (exported from `app/(app)/apps/settings/actions.ts`) lacks any authentication or session validation. An attacker can call it directly via the `Next-Action` header, providing fully controlled `provider`, `apiKey`, `model`, and `baseUrl` parameters. When `provider` is set to `"openai_compatible"`, the `baseUrl` is passed unsanitised to the underlying OpenAI client's `configuration.baseURL`. The server then makes an HTTP POST request to `{baseUrl}/chat/completions`. Crucially, **both connection errors and the raw response body from the target endpoint are reflected back** in the `message` field of the API response. This turns the issue into a fully exploitable SSRF with **response echo**, allowing an attacker to: - **Probe internal networks** – distinguish open vs. closed ports via connection error messages (e.g., `ECONNREFUSED` vs. structured error responses). - **Read internal HTTP services** – retrieve error/JSON responses from internal dashboards, management interfaces, or cloud metadata endpoints (e.g., `http://169.254.169.254/latest/meta-data/`). --- ### Steps to Reproduce 1. **Obtain the Server Action ID** The action is registered under the `/apps/settings` route. Its ID can be extracted from `server-reference-manifest.json` inside the `.next` directory, or from the client-side chunks loaded when visiting the settings page (though the page itself may require login in SaaS mode). 2. **Send an unauthenticated POST request** to the action endpoint with the `Next-Action` header, using `openai_compatible` as the provider and a malicious `baseUrl`. Example (simplified payload structure): ```json { "provider": "openai_compatible", "apiKey": "dummy", "model": "gpt-3.5-turbo", "baseUrl": "http://127.0.0.1:8080/internal-admin" } ``` 3. **Observe the response** – the returned `message` will contain the connection error or the HTTP response body from the internal target, confirming the SSRF. --- ### Expected Behavior The Server Action should: - Reject unauthenticated requests (require a valid session). - Validate/whitelist the `baseUrl` (e.g., allow only known trusted domains or block private IP ranges). - Sanitise error messages – do not echo raw connection details or remote response bodies back to the client. ### Actual Behavior - No authentication check is performed inside the action. - `baseUrl` is passed directly to the HTTP client without restrictions. - Raw error messages and response bodies from the internal target are returned to the caller. ### Environment - Affected versions: **0.8.5 and earlier** (including the latest `ghcr.io/vas3k/taxhacker:latest` image). - **Self-hosted mode** (`SELF_HOSTED_MODE=true`): fully exploitable without any session. - **SaaS mode** (`SELF_HOSTED_MODE=false`): the settings page is protected by middleware, but the Server Action itself lacks inline auth checks (it relies solely on route-level protection). A logged-in user (or a compromised session) could still exploit it. Regardless, the function body should enforce its own authorisation. ### Additional Context - The middleware (`proxy.ts`) does not cover Server Action POST endpoints directly; actions are dispatched based on the `Next-Action` header and the route they are registered under. - The action ID is deterministic per build and can be trivially extracted, making this low-hanging fruit for an attacker. - This is a **security-critical** issue, especially for cloud-hosted deployments where internal metadata services and internal APIs are common targets. ### Proposed Fix - Add `getSession()` / `getCurrentUser()` validation at the start of `testLLMProviderAction`. - Implement a strict allowlist for `baseUrl` (e.g., only allow `api.openai.com`, `generativelanguage.googleapis.com`, etc., or require the user to choose from pre-configured providers). - If custom base URLs must be supported, at least block private IP ranges (`127.0.0.0/8`, `10.0.0.0/8`, `192.168.0.0/16`, `169.254.0.0/16`, etc.) and prevent protocols other than HTTPS. - Do **not** echo raw connection errors or remote response bodies to the client; return only a generic failure message (e.g., "Connection test failed") in production.