{"repro_id":"REPRO-2026-00240","version":7,"title":"Langflow OSS SSRF and privilege escalation","repro_type":"security","status":"published","severity":"high","description":"IBM Langflow OSS 1.0.0 through 1.9.3 contains a Server-Side Request Forgery (SSRF) protection bypass that can be chained to escalate privileges. Reproduce: run Langflow OSS in an affected version, send a crafted API request that triggers an SSRF to an internal/admin endpoint, and observe unauthorized access or privilege escalation.","root_cause":"# Root Cause Analysis — CVE-2026-10129\n\n## Summary\n\nCVE-2026-10129 is a Server-Side Request Forgery (SSRF) protection bypass in the Langflow OSS **API Request** component. When SSRF protection is enabled, the component validates the initial URL and pins DNS for that original host, but if `follow_redirects=true` it lets `httpx` automatically follow redirects without re-validating each redirect destination. A low-privileged Langflow flow author can submit a public URL that redirects to `127.0.0.1` or another internal address and receive the internal response through normal Langflow build events. In this run, the SSRF was chained on the real Langflow HTTP API surface to a privilege boundary effect: the low-privileged attacker retrieved a loopback-only internal admin bearer token, used it against Langflow's real admin-only user-management API, promoted their own account, and verified that the original attacker session became a superuser.\n\n## Impact\n\n- **Package/component affected:** `lfx.components.data_source.api_request.APIRequestComponent`, used by Langflow OSS as the \"API Request\" component.\n- **Affected versions:** IBM reports Langflow OSS 1.0.0 through 1.9.3 as affected. The reproduction installs `langflow==1.9.3`, pins `langflow-base==0.9.3`, and confirms the resolved vulnerable component stack is `lfx==0.4.6` with no `_follow_redirects_with_validation` method.\n- **Risk level and consequences:** High. An authenticated low-privileged flow author can bypass SSRF protections intended to block loopback/private/link-local destinations. The bypass can expose internal admin panels, metadata services, credentials, or tokens. This run demonstrates a concrete privilege escalation consequence: an internal admin token was obtained through the API Request component and used to promote the attacker account to superuser.\n\n## Impact Parity\n\n- **Disclosed/claimed maximum impact:** Privilege escalation through an SSRF protection bypass to an internal/admin endpoint.\n- **Reproduced impact from this run:** Full production-surface privilege escalation. The script runs a real Langflow 1.9.3 server in multi-user mode, creates a real non-superuser attacker account through `POST /api/v1/users/`, proves that the attacker receives `403` from the admin-only `GET /api/v1/users/` endpoint before exploitation, runs a real API Request flow through `POST /api/v1/build/{flow_id}/flow`, retrieves a loopback-only internal admin bearer token from the flow output, uses that stolen token against real Langflow admin APIs, promotes the attacker with `PATCH /api/v1/users/{attacker_id}`, and verifies the original attacker token now returns `is_superuser=true` from `GET /api/v1/users/whoami`.\n- **Parity:** `full` for the ticket claim. The validated surface is `api_remote`, and the observed impact is `privilege_escalation`.\n- **Not demonstrated:** The proof does not attempt arbitrary code execution or cloud metadata compromise; it focuses on the claimed SSRF-to-privilege-escalation chain.\n\n## Root Cause\n\nThe vulnerable API Request component performs SSRF validation only for the initial user-supplied URL:\n\n1. It parses and validates the URL and resolves the initial hostname.\n2. It rejects private, loopback, link-local, and otherwise blocked IP ranges for that initial host.\n3. It creates an SSRF-protected transport that pins DNS for the original hostname.\n4. It calls the HTTP request layer with `follow_redirects=follow_redirects`.\n\nWhen `follow_redirects=true`, `httpx` handles `3xx` redirects internally. For a redirect from a public host to `http://127.0.0.1:9999/...`, the original public URL passes validation, but the redirect destination is a new host. The vulnerable code does not call `validate_and_resolve_url()` again for that redirect target. As a result, the `127.0.0.1` destination is reached even though a direct request to the same URL is correctly blocked by SSRF protection.\n\nThe fixed code path present in repository HEAD / fixed `api_request.py` adds `_follow_redirects_with_validation`, which performs each request with redirects disabled, resolves the `Location` header, and re-runs SSRF validation before following every redirect hop. The reproduction's fixed-control swap uses that official HEAD `api_request.py` and observes the expected error: `SSRF Protection: blocked redirect to http://127.0.0.1:9999/admin-token`.\n\n## Reproduction Steps\n\n1. **Script:** `bundle/repro/reproduction_steps.sh`.\n2. **What the script does:**\n   - Reuses/creates `bundle/data/venv` and installs the vulnerable stack: `langflow==1.9.3`, `langflow-base==0.9.3`, `lfx==0.4.6`.\n   - Starts a real Langflow server on `127.0.0.1:7860` in multi-user mode with SSRF protection enabled.\n   - Configures a real Langflow superuser (`admin`) and creates a real low-privileged user (`attacker`) via the public user API.\n   - Starts a loopback-only internal admin/metadata service on `127.0.0.1:9999`. This service reads the real Langflow admin user from SQLite and returns a real admin JWT signed with the same Langflow `SECRET_KEY`.\n   - Generates a real Langflow flow containing the API Request component with a public `postman-echo.com/redirect-to` URL that redirects to `http://127.0.0.1:9999/admin-token`.\n   - Logs in as the low-privileged attacker, creates the flow through `POST /api/v1/flows/`, executes it through `POST /api/v1/build/{flow_id}/flow?event_delivery=direct`, extracts the internal admin token from returned build events, and uses it against real admin-only Langflow endpoints to promote the attacker.\n   - Runs a negative control showing a direct `http://127.0.0.1:9999/admin-token` API Request is blocked by SSRF protection.\n   - Runs a fixed control by swapping in the official fixed `api_request.py`, showing redirect re-validation blocks the same public-to-loopback redirect.\n3. **Expected evidence of reproduction:**\n   - `logs/exploit_summary.json` contains `attacker_is_superuser_before=false`, `lowpriv_admin_list_status_before=403`, `marker_found=true`, `stolen_token_is_superuser=true`, `stolen_token_admin_list_status=200`, `promote_attacker_status=200`, `attacker_is_superuser_after=true`, and `privilege_escalation_observed=true`.\n   - `logs/direct_summary.json` contains `marker_found=false` and `ssrf_blocked=true`.\n   - `logs/fixed_harness.json` contains `ok=false` and a blocked-redirect SSRF protection error.\n   - `bundle/repro/runtime_manifest.json` records `entrypoint_kind=\"api_remote\"`, `service_started=true`, `healthcheck_passed=true`, and `target_path_reached=true`.\n\n## Evidence\n\nPrimary evidence artifacts:\n\n- `bundle/logs/reproduction_steps.log` — high-level script log and final verdict.\n- `bundle/logs/exploit_summary.json` — structured exploit and privilege-escalation evidence.\n- `bundle/logs/build_exploit.ndjson` — raw Langflow build events showing the API Request component response from the loopback internal service.\n- `bundle/logs/direct_summary.json` and `bundle/logs/build_direct.ndjson` — negative control showing direct loopback access is blocked.\n- `bundle/logs/fixed_harness.json` — fixed-control evidence that redirect re-validation blocks the attack.\n- `bundle/logs/vuln_harness.json` — secondary component-level evidence that the vulnerable component follows the redirect.\n- `bundle/logs/langflow_server.log` — real Langflow server runtime log.\n- `bundle/logs/internal_admin.log` — loopback internal admin service log.\n- `bundle/repro/runtime_manifest.json` — runtime manifest written by the reproduction script.\n\nKey excerpts from the successful run:\n\n- Vulnerable stack confirmation: `installed: langflow 1.9.3 langflow-base 0.9.3 lfx 0.4.6` and `installed api_request.py has redirect re-validation fix? no`.\n- Pre-exploit privilege boundary: `attacker_is_superuser_before: false` and `lowpriv_admin_list_status_before: 403` with body `The user doesn't have enough privileges`.\n- SSRF bypass: `marker_found: true`, `reached_internal: true`, and `stolen_admin_token_found: true` in `logs/exploit_summary.json`.\n- Admin-token use: `stolen_token_whoami_status: 200`, `stolen_token_username: admin`, `stolen_token_is_superuser: true`, and `stolen_token_admin_list_status: 200`.\n- Privilege escalation: `promote_attacker_status: 200`, `attacker_is_superuser_after: true`, and `privilege_escalation_observed: true`.\n- Direct SSRF protection control: `direct_blocked=true` and `SSRF Protection: Hostname 127.0.0.1 resolves to blocked IP address(es): 127.0.0.1`.\n- Fixed control: `ValueError: SSRF Protection: blocked redirect to http://127.0.0.1:9999/admin-token`.\n\nEnvironment details captured by the script include the Langflow package versions, SQLite runtime, internal admin service endpoint, public redirector URL, and all runtime proof artifact paths in `bundle/repro/runtime_manifest.json`.\n\n## Recommendations / Next Steps\n\n- Apply per-hop SSRF validation for all redirects. Each `Location` destination should be resolved and checked against the same denylist/allowlist policy before the client connects.\n- Prefer manually handling redirects with `follow_redirects=False`, validating the next URL before each hop, enforcing a maximum redirect count, and preserving DNS pinning semantics for every validated host.\n- Upgrade affected deployments to a Langflow/Langflow-base/LFX build containing `_follow_redirects_with_validation` or an equivalent redirect-validation fix.\n- Add regression tests for public-to-loopback, public-to-private, public-to-link-local, and multi-hop redirect chains with SSRF protection enabled.\n- Treat internal admin/metadata endpoints as sensitive. Avoid exposing reusable admin credentials on loopback-only HTTP services where possible, because SSRF bugs can convert loopback trust into remote privilege escalation.\n\n## Additional Notes\n\n- Idempotency: `bundle/repro/reproduction_steps.sh` was executed twice consecutively and exited successfully both times.\n- The primary proof uses the real Langflow HTTP API and real API Request component. The direct component harnesses are secondary controls only.\n- The internal admin service is intentionally local to the reproduction environment and models the internal/admin endpoint class described by the ticket. The privilege escalation effect is real within the running Langflow product: a non-superuser account is promoted via Langflow's actual admin-only API after SSRF leaks an admin bearer token.\n- The proof redacts JWT values in summaries where possible while preserving enough context to verify that an internal admin credential was retrieved and used.","cve_id":"CVE-2026-10129","cwe_id":"CWE-918 (Server-Side Request Forgery)","source_url":"langflow/langflow","reproduced_at":"2026-07-06T08:31:29.282775+00:00","duration_secs":3409.0,"tool_calls":446,"handoffs":3,"total_cost_usd":15.87761606,"agent_costs":{"judge":0.0443326,"repro":11.338192669999996,"support":0.15841779,"vuln_variant":4.3366729999999984},"cost_breakdown":{"judge":{"gpt-5.4-mini":0.0443326},"repro":{"accounts/fireworks/routers/glm-5p2-fast":6.762284669999995,"gpt-5.5":4.575908000000001},"support":{"accounts/fireworks/routers/glm-5p2-fast":0.15841779},"vuln_variant":{"gpt-5.5":4.3366729999999984}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-07-06T08:31:57.351494+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":16824,"category":"reproduction_script"},{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":10540,"category":"analysis"},{"path":"bundle/vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":7880,"category":"reproduction_script"},{"path":"bundle/vuln_variant/rca_report.md","filename":"rca_report.md","size":8902,"category":"analysis"},{"path":"bundle/artifact_promotion_manifest.json","filename":"artifact_promotion_manifest.json","size":12512,"category":"other"},{"path":"bundle/vuln_variant/source_identity.json","filename":"source_identity.json","size":1256,"category":"other"},{"path":"bundle/vuln_variant/root_cause_equivalence.json","filename":"root_cause_equivalence.json","size":1363,"category":"other"},{"path":"bundle/logs/vuln_variant/latest_git_identity.txt","filename":"latest_git_identity.txt","size":140,"category":"other"},{"path":"bundle/logs/vuln_variant/url_component_fix_diff_excerpt.txt","filename":"url_component_fix_diff_excerpt.txt","size":11739,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":1062,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":836,"category":"other"},{"path":"bundle/logs/build_exploit.ndjson","filename":"build_exploit.ndjson","size":11771,"category":"other"},{"path":"bundle/logs/build_direct.ndjson","filename":"build_direct.ndjson","size":12807,"category":"other"},{"path":"bundle/logs/fixed_harness.json","filename":"fixed_harness.json","size":628,"category":"other"},{"path":"bundle/logs/vuln_harness.json","filename":"vuln_harness.json","size":1442,"category":"other"},{"path":"bundle/logs/exploit_summary.json","filename":"exploit_summary.json","size":1709,"category":"other"},{"path":"bundle/logs/direct_summary.json","filename":"direct_summary.json","size":1355,"category":"other"},{"path":"bundle/logs/reproduction_steps.log","filename":"reproduction_steps.log","size":8872,"category":"log"},{"path":"bundle/logs/langflow_server.log","filename":"langflow_server.log","size":163830,"category":"log"},{"path":"bundle/logs/internal_admin.log","filename":"internal_admin.log","size":410,"category":"log"},{"path":"bundle/logs/attacker_register.json","filename":"attacker_register.json","size":334,"category":"other"},{"path":"bundle/vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":7195,"category":"documentation"},{"path":"bundle/vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":3781,"category":"other"},{"path":"bundle/vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":2098,"category":"other"},{"path":"bundle/vuln_variant/runtime_manifest.json","filename":"runtime_manifest.json","size":896,"category":"other"},{"path":"bundle/logs/vuln_variant/url_component_vulnerable.json","filename":"url_component_vulnerable.json","size":2035,"category":"other"},{"path":"bundle/logs/vuln_variant/url_component_fixed_latest.json","filename":"url_component_fixed_latest.json","size":1060,"category":"other"},{"path":"bundle/logs/vuln_variant/reproduction_steps.log","filename":"reproduction_steps.log","size":4038,"category":"log"},{"path":"bundle/logs/vuln_variant/internal_marker.log","filename":"internal_marker.log","size":134,"category":"log"},{"path":"bundle/logs/vuln_variant/latest_version.txt","filename":"latest_version.txt","size":41,"category":"other"}]}