# Root Cause Analysis — CVE-2026-10129

## Summary

CVE-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.

## Impact

- **Package/component affected:** `lfx.components.data_source.api_request.APIRequestComponent`, used by Langflow OSS as the "API Request" component.
- **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.
- **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.

## Impact Parity

- **Disclosed/claimed maximum impact:** Privilege escalation through an SSRF protection bypass to an internal/admin endpoint.
- **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`.
- **Parity:** `full` for the ticket claim. The validated surface is `api_remote`, and the observed impact is `privilege_escalation`.
- **Not demonstrated:** The proof does not attempt arbitrary code execution or cloud metadata compromise; it focuses on the claimed SSRF-to-privilege-escalation chain.

## Root Cause

The vulnerable API Request component performs SSRF validation only for the initial user-supplied URL:

1. It parses and validates the URL and resolves the initial hostname.
2. It rejects private, loopback, link-local, and otherwise blocked IP ranges for that initial host.
3. It creates an SSRF-protected transport that pins DNS for the original hostname.
4. It calls the HTTP request layer with `follow_redirects=follow_redirects`.

When `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.

The 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`.

## Reproduction Steps

1. **Script:** `bundle/repro/reproduction_steps.sh`.
2. **What the script does:**
   - Reuses/creates `bundle/data/venv` and installs the vulnerable stack: `langflow==1.9.3`, `langflow-base==0.9.3`, `lfx==0.4.6`.
   - Starts a real Langflow server on `127.0.0.1:7860` in multi-user mode with SSRF protection enabled.
   - Configures a real Langflow superuser (`admin`) and creates a real low-privileged user (`attacker`) via the public user API.
   - 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`.
   - 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`.
   - 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.
   - Runs a negative control showing a direct `http://127.0.0.1:9999/admin-token` API Request is blocked by SSRF protection.
   - Runs a fixed control by swapping in the official fixed `api_request.py`, showing redirect re-validation blocks the same public-to-loopback redirect.
3. **Expected evidence of reproduction:**
   - `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`.
   - `logs/direct_summary.json` contains `marker_found=false` and `ssrf_blocked=true`.
   - `logs/fixed_harness.json` contains `ok=false` and a blocked-redirect SSRF protection error.
   - `bundle/repro/runtime_manifest.json` records `entrypoint_kind="api_remote"`, `service_started=true`, `healthcheck_passed=true`, and `target_path_reached=true`.

## Evidence

Primary evidence artifacts:

- `bundle/logs/reproduction_steps.log` — high-level script log and final verdict.
- `bundle/logs/exploit_summary.json` — structured exploit and privilege-escalation evidence.
- `bundle/logs/build_exploit.ndjson` — raw Langflow build events showing the API Request component response from the loopback internal service.
- `bundle/logs/direct_summary.json` and `bundle/logs/build_direct.ndjson` — negative control showing direct loopback access is blocked.
- `bundle/logs/fixed_harness.json` — fixed-control evidence that redirect re-validation blocks the attack.
- `bundle/logs/vuln_harness.json` — secondary component-level evidence that the vulnerable component follows the redirect.
- `bundle/logs/langflow_server.log` — real Langflow server runtime log.
- `bundle/logs/internal_admin.log` — loopback internal admin service log.
- `bundle/repro/runtime_manifest.json` — runtime manifest written by the reproduction script.

Key excerpts from the successful run:

- 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`.
- 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`.
- SSRF bypass: `marker_found: true`, `reached_internal: true`, and `stolen_admin_token_found: true` in `logs/exploit_summary.json`.
- Admin-token use: `stolen_token_whoami_status: 200`, `stolen_token_username: admin`, `stolen_token_is_superuser: true`, and `stolen_token_admin_list_status: 200`.
- Privilege escalation: `promote_attacker_status: 200`, `attacker_is_superuser_after: true`, and `privilege_escalation_observed: true`.
- 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`.
- Fixed control: `ValueError: SSRF Protection: blocked redirect to http://127.0.0.1:9999/admin-token`.

Environment 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`.

## Recommendations / Next Steps

- 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.
- 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.
- Upgrade affected deployments to a Langflow/Langflow-base/LFX build containing `_follow_redirects_with_validation` or an equivalent redirect-validation fix.
- Add regression tests for public-to-loopback, public-to-private, public-to-link-local, and multi-hop redirect chains with SSRF protection enabled.
- 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.

## Additional Notes

- Idempotency: `bundle/repro/reproduction_steps.sh` was executed twice consecutively and exited successfully both times.
- The primary proof uses the real Langflow HTTP API and real API Request component. The direct component harnesses are secondary controls only.
- 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.
- The proof redacts JWT values in summaries where possible while preserving enough context to verify that an internal admin credential was retrieved and used.