# RCA Report: TaskingAI Plugin web_reader SSRF (ARGUS-TASKINGAI-WEB-READER-SSRF-20260625)

## Summary

The TaskingAI Plugin server exposes an unauthenticated `/v1/execute` API endpoint that allows callers to invoke bundled plugins with arbitrary user-supplied input parameters. The `web_reader/read_web_page` plugin accepts a `url` parameter with no validation of scheme, hostname, IP range, or private network restrictions. When invoked through the API, the plugin server performs an outbound HTTP GET request to the attacker-supplied URL using `aiohttp.ClientSession().get(url=url, proxy=CONFIG.PROXY)` and returns the fetched content in the API response. This enables server-side request forgery (SSRF) against internal services accessible from the plugin container's network.

## Impact

- **Package/component affected**: TaskingAI Plugin server (`taskingai/taskingai-plugin:latest` Docker image, version approximately v0.3.5-1)
- **Affected versions**: At least commit `f0092d6b2dd82e98e188e0b9849fdd4c7230dd98` and the current `latest` Docker image
- **Risk level**: High
- **Consequences**: An attacker who can reach the plugin service can force it to make HTTP requests to internal addresses, potentially exfiltrating metadata, accessing internal APIs, or scanning the container network.

## Root Cause

The root cause is missing input validation on the `url` parameter in the `read_web_page` plugin and the `/v1/execute` route that passes arbitrary `input_params` directly to the plugin implementation.

Code path:
1. `plugin/app/routes/execute.py:117-123` strips only `None` values and invokes the selected plugin with user-supplied `input_params`.
2. `plugin/bundles/web_reader/plugins/read_web_page/plugin.py:9-13` reads `url` from `plugin_input.input_params` and passes it directly to `aiohttp.ClientSession().get(url=url, proxy=CONFIG.PROXY)` without any allow-list, deny-list, or private-IP validation.
3. The plugin schema at `plugin/bundles/web_reader/plugins/read_web_page/plugin_schema.yml:3-8` declares `url` as a required plain string with no URL restrictions.

No fix commit is known at this time; the ticket references a vulnerable commit but does not name a patched version.

## Reproduction Steps

The reproduction script is `bundle/repro/reproduction_steps.sh`. It performs the following steps:

1. Creates a dedicated Docker bridge network (`taskingai-ssrf-net`).
2. Starts a Python HTTP listener container on the network that logs every incoming request and returns a unique marker string (`PRUVA_TASKINGAI_SSRF_MARKER`).
3. Starts the official `taskingai/taskingai-plugin:latest` Docker container on the same network with required environment variables (`MODE=PROD`, `OBJECT_STORAGE_TYPE=local`, `HOST_URL`, etc.).
4. Waits for the plugin service to become healthy (checks `/v1/execute` from a client container on the same network).
5. Sends a `POST /v1/execute` request to the plugin with `bundle_id=web_reader`, `plugin_id=read_web_page`, and `input_params.url` pointing to the internal listener (`http://taskingai-listener:9000/internal/proof`).
6. Verifies both required signals:
   - The listener logs show `REQUEST_RECEIVED: /internal/proof`.
   - The plugin API response contains `"result":"PRUVA_TASKINGAI_SSRF_MARKER"`.

### Expected evidence of reproduction

- `bundle/logs/listener.log` should contain the listener access log showing the request.
- `bundle/logs/execute_response.json` should contain the JSON response from the plugin with the marker string inside `data.data.result`.

## Evidence

- **Log file locations**:
  - `bundle/logs/execute_response.json` — the `/v1/execute` response from the plugin server
  - `bundle/logs/listener.log` — the listener access log showing the SSRF request
  - `bundle/logs/plugin_startup.log` — plugin startup log confirming the service loaded all bundles successfully
- **Key excerpts**:
  - Response: `{"status":"success","data":{"status":200,"data":{"result":"PRUVA_TASKINGAI_SSRF_MARKER"}}}`
  - Listener log: `REQUEST_RECEIVED: /internal/proof` followed by `"GET /internal/proof HTTP/1.1" 200 -`
- **Environment details**: Docker-based reproduction using the official `taskingai/taskingai-plugin:latest` image (digest `sha256:d38f821e1585be4b56e827b81fdfcc1f42958cf063e8fe6e1f95defb8b1a1159`) on a Linux host with Docker bridge networking.

## Recommendations / Next Steps

1. **Input validation**: Add strict URL validation in the `read_web_page` plugin (or in the `/v1/execute` route) before performing the HTTP request. Reject URLs with private IP ranges (e.g., `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `127.0.0.0/8`, `169.254.0.0/16`) and loopback hostnames (`localhost`).
2. **URL scheme allow-list**: Restrict allowed schemes to `http` and `https` only, and block `file://`, `ftp://`, etc.
3. **Redirect handling**: If redirects are followed, validate the destination of each redirect hop to prevent redirect-based SSRF bypasses.
4. **Network isolation**: Run the plugin service in a restricted network segment with egress controls to limit internal exposure.
5. **Testing**: Add unit and integration tests that verify the plugin rejects internal URLs and returns an appropriate error response.

## Additional Notes

- **Idempotency**: The reproduction script is fully idempotent. It creates and cleans up its own Docker network and containers, and it runs successfully every time (confirmed with 2 consecutive successful runs).
- **Edge cases**: The vulnerability depends only on the `url` parameter reaching the `aiohttp.get()` call. Any URL string that the container can resolve is exploitable, including DNS names that resolve to internal addresses.
- **Limitations**: The reproduction was performed using Docker networking. In a production deployment, the attacker would need network reachability to the plugin service; the SSRF destination would be any address reachable from the plugin server's network context.
