{
  "variant_id": "cve-2026-33017-variant-stored-public-custom-component-rce",
  "created_at": "2026-07-02T00:00:00Z",
  "variant_summary": "Bypass of the CVE-2026-33017 v1.9.0 fix: the unauthenticated public flow build path (POST /api/v1/build_public_tmp/{flow_id}/flow) no longer accepts a client-supplied `data` parameter, but it loads the PUBLIC flow definition from the database and exec()'s each node's stored custom-component `code` at graph-build time via prepare_global_scope()/eval_custom_component_code. The only added gate, validate_flow_for_current_settings(flow.data), is a no-op under the default allow_custom_components=true, so an attacker who stores a malicious CustomComponent inside a PUBLIC flow (via POST /api/v1/flows/ using the AUTO_LOGIN superuser token -- the same capability the original CVE repro uses to create a public flow) obtains unauthenticated RCE on the 'fixed' langflow 1.9.0. Confirmed empirically (2/2 attempts: exploit_status=200, proof written, uid=1000(user)). Closed only in v1.10.1 by upstream follow-up commit 626365f088 ('run trusted server code on unauthenticated public flow builds', H1-3754930 follow-up).",
  "relation": "newer_version_sibling",
  "origin_kind": "pruva_variant",
  "repository": "langflow-ai/langflow",
  "submitted_target": {
    "target_kind": "docker_image",
    "version": "1.9.0",
    "ref": "v1.9.0",
    "commit_sha": "a47f2ad17eb662e940c550cfccb64a87dddd7e0b",
    "display": "langflowai/langflow:1.9.0 (CVE-2026-33017 'fixed' version; claimed patched)"
  },
  "variant_target": {
    "target_kind": "docker_image",
    "version": "1.9.0",
    "ref": "v1.9.0",
    "commit_sha": "a47f2ad17eb662e940c550cfccb64a87dddd7e0b",
    "display": "langflowai/langflow:1.9.0 -> proven STILL vulnerable to unauthenticated RCE via stored-custom-component bypass"
  },
  "same_root_cause_confidence": "high",
  "same_surface_confidence": "high",
  "claimed_surface": "api_remote",
  "validated_surface": "api_remote",
  "required_entrypoint_kind": "api_remote",
  "required_entrypoint_detail": "POST /api/v1/build_public_tmp/{flow_id}/flow (unauthenticated, client_id cookie only; no `data` field in the request body). Pre-condition: a PUBLIC flow whose stored `data` carries a malicious CustomComponent node, created via POST /api/v1/flows/ with an AUTO_LOGIN superuser token (no credentials).",
  "attacker_controlled_input": "PUBLIC flow stored `data` containing a CustomComponent node with a top-level `_rce = os.system('id > /tmp/rce-proof ...')` payload (an ast.Assign node that prepare_global_scope() exec()'s at graph-build time).",
  "trigger_path": "POST /api/v1/flows/ (store malicious PUBLIC flow) -> POST /api/v1/build_public_tmp/{flow_id}/flow (no data, client_id cookie) -> start_flow_build(data=None, source_flow_id=flow_id) -> generate_flow_events -> create_graph -> build_graph_from_db -> Graph.from_payload -> create_class -> prepare_global_scope -> exec(compiled_module, exec_globals)",
  "observed_impact_class": "code_execution",
  "exploitability_confidence": "high",
  "evidence_scope": "production_path",
  "runtime_manifest_present": true,
  "end_to_end_target_reached": true,
  "inferred": false,
  "blocking_mitigation": "Closed in v1.10.1 by commit 626365f088 which, on the public path, substitutes the server's trusted code for each known component type and rejects unknown/custom component types carrying code (HTTP 400 'This flow cannot be executed.'), gated by the new allow_public_custom_components setting (default false). v1.9.0 through v1.10.0 remain vulnerable to this bypass.",
  "claim_block_reason": null,
  "file_path": "src/backend/base/langflow/api/v1/chat.py",
  "line_start": 640,
  "line_end": 725,
  "secondary_anchors": [
    {
      "file_path": "src/lfx/src/lfx/utils/flow_validation.py",
      "line_start": 158,
      "line_end": 166
    },
    {
      "file_path": "src/lfx/src/lfx/utils/flow_validation.py",
      "line_start": 206,
      "line_end": 216
    },
    {
      "file_path": "src/lfx/src/lfx/services/settings/base.py",
      "line_start": 386,
      "line_end": 386
    },
    {
      "file_path": "src/lfx/src/lfx/custom/validate.py",
      "line_start": 218,
      "line_end": 222
    }
  ],
  "review_scope_paths": [
    "src/backend/base/langflow/api/v1/chat.py",
    "src/backend/base/langflow/api/build.py",
    "src/lfx/src/lfx/utils/flow_validation.py",
    "src/lfx/src/lfx/services/settings/base.py",
    "src/lfx/src/lfx/services/settings/groups/security.py",
    "src/lfx/src/lfx/custom/validate.py",
    "src/backend/base/langflow/api/v1/flows.py"
  ],
  "artifact_refs": {
    "variant_manifest": "bundle/vuln_variant/variant_manifest.json",
    "validation_verdict": "bundle/vuln_variant/validation_verdict.json",
    "runtime_manifest": "bundle/vuln_variant/runtime_manifest.json",
    "source_identity": "bundle/vuln_variant/source_identity.json",
    "root_cause_equivalence": "bundle/vuln_variant/root_cause_equivalence.json",
    "repro_log": "bundle/logs/vuln_variant/reproduction_steps.log",
    "reproducer": [
      "bundle/vuln_variant/reproduction_steps.sh",
      "bundle/vuln_variant/variant_attempt.py"
    ]
  }
}
