# REPRO-2026-00197: TaskingAI web_reader plugin SSRF via /v1/execute ## Summary Status: published Severity: high Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00197 ## Package Name: taskingai Ecosystem: github Affected: commit f0092d6b2dd82e98e188e0b9849fdd4c7230dd98 Fixed: Unknown ## Root Cause # 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. ## Reproduction Details Reproduced: 2026-07-02T04:55:50.934Z Duration: 1055 seconds Tool calls: 62 Turns: Unknown Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00197 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00197&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00197/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - Source: https://github.com/TaskingAI/TaskingAI ## Artifacts - bundle/repro/reproduction_steps.sh (reproduction_script, 5181 bytes) - bundle/repro/rca_report.md (analysis, 5926 bytes) - bundle/repro/runtime_manifest.json (other, 482 bytes) - bundle/repro/validation_verdict.json (other, 768 bytes) - bundle/ticket.json (other, 7115 bytes) - bundle/ticket.md (ticket, 4530 bytes) - bundle/logs/execute_response.err (other, 0 bytes) - bundle/logs/plugin_startup.log (log, 4931 bytes) - bundle/logs/reproduction_steps_run2.log (log, 5715 bytes) - bundle/logs/execute_response.json (other, 91 bytes) - bundle/logs/listener.log (log, 71 bytes) - bundle/logs/reproduction_steps.log (log, 5715 bytes) - bundle/logs/image_inspect.json (other, 866 bytes) - bundle/logs/plugin_inspect.log (log, 7751 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00197 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00197/artifacts/bundle/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00197 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev