critical Threat analysis

Langflow CVE-2025-34291: KEV Origin Validation Exposure

CISA added Langflow CVE-2025-34291 to KEV on 2026-05-21. The issue combines permissive CORS and credentialed refresh-token behavior; this article provides dependency, container, HTTP telemetry, and token-abuse audit scripts.

#langflow#cisa-kev#vulnerability-response#ai-tooling#cors
On this page 0% read

    Executive Summary

    CISA added CVE-2025-34291 to KEV on 2026-05-21 with a due date of 2026-06-04 CISA KEV. CISA describes an origin validation error where permissive CORS and a refresh token cookie configured as SameSite=None allow credentialed cross-origin refresh endpoint requests, enabling authenticated follow-on actions CISA KEV.

    NVD lists vulnerable Langflow CPE entries through 1.6.9, while the vendor release reference in CISA KEV points to v1.9.3 NVD, Langflow release.

    Key Facts

    cve: "CVE-2025-34291"
    vendor: "Langflow"
    product: "Langflow"
    kev_added: "2026-05-21"
    kev_due: "2026-06-04"
    vulnerability: "Origin validation error with credentialed cross-origin refresh-token requests"
    cwe: ["CWE-346"]
    nvd_vulnerable_cpe: "cpe:2.3:a:langflow:langflow:*:*:*:*:*:*:*:*"
    nvd_vulnerable_version_end_including: "1.6.9"
    vendor_reference_release: "v1.9.3"
    cvss_v31: "8.8 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H"
    cvss_v40_secondary: "9.4 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H"
    exploitation_status: "cisa_kev_exploited"
    zero_day_status: "unproven_from_public_primary_sources"

    Source Confidence & Evidence Mapping

    • confirmed: CISA KEV lists CVE-2025-34291 as known exploited and describes the CORS plus refresh-token failure mode CISA KEV.
    • confirmed: NVD lists CWE-346, CVSS 3.1 score 8.8, and vulnerable CPE through 1.6.9 NVD.
    • confirmed: CISA KEV references Langflow v1.9.3 and GitHub issue 11465 as vendor context Langflow release, Langflow issue.
    • unclear: Public primary sources do not provide a single stable refresh endpoint path or a complete list of exploited IPs.

    Impact Determination

    ClassificationCriteriaRequired evidenceRemediation triggerClosure condition
    Confirmed compromiseHTTP or application telemetry shows credentialed cross-origin refresh behavior followed by authenticated Langflow API activity on a vulnerable deployment.Version evidence, Origin/Cookie/refresh telemetry, session or token activity, and affected host identity.Preserve Langflow app logs, reverse-proxy logs, deployment manifests, and secrets available to the Langflow process.Version is fixed or removed and downstream token/API audit shows no unexplained authenticated activity.
    Presumed exposedLangflow version is <= 1.6.9 by NVD CPE or below vendor reference release 1.9.3 with browser-reachable deployment.Lockfile, package inventory, container tag, scanner row, or runtime package output.Keep the deployment in scope until version closure succeeds.Script output proves version is outside the NVD vulnerable range and at or above the vendor reference release used for closure.
    Potentially exposedLangflow appears in source, images, manifests, or scanner exports but version or browser exposure is unknown.Repository, image, Kubernetes, CMDB, or scanner evidence naming langflow.Collect runtime version and exposure evidence.Asset resolves to confirmed compromise, presumed exposed, not exposed, or unknown.
    Not exposedNo Langflow package, image, deployment, scanner row, or CVE-2025-34291 selector appears in complete exports.Negative outputs from repository, image, Kubernetes, and scanner collection.None for this CVE.Evidence bundle covers source, build artifacts, containers, and deployed workloads.
    UnknownRuntime version, exposure, or HTTP telemetry is unavailable.Gap statement naming the unavailable source.Keep externally reachable Langflow deployments in scope.Evidence is recovered or the risk owner accepts the named gap.

    Timeline

    • 2025-12-05: NVD publication timestamp for CVE-2025-34291 NVD.
    • 2026-05-21: CISA adds CVE-2025-34291 to KEV with due date 2026-06-04 CISA KEV.
    • 2026-05-21: CISA KEV catalog points defenders to Langflow v1.9.3 and issue 11465 Langflow release.

    What Happened

    The exploitable condition is credentialed cross-origin access to Langflow refresh behavior, not a package compromise. The practical scoping anchors are langflow package versions, container images, exposed Langflow services, and HTTP telemetry containing Origin, credential cookies, and refresh-token activity.

    Technical Analysis

    Langflow deployments often hold model-provider credentials, workflow secrets, and API tokens. A successful refresh-token abuse path can convert a browser interaction into authenticated Langflow API access. The scripts below classify dependencies, containers, and HTTP telemetry without assuming a vendor route that public primary sources do not provide.

    Affected Assets and Blast Radius

    asset_selectors:
      - "langflow"
      - "CVE-2025-34291"
      - "CWE-346"
      - "v1.9.3"
    version_selectors:
      nvd_vulnerable_end_including: "1.6.9"
      vendor_reference_release: "1.9.3"
    credentials_and_data_at_risk:
      - "Langflow refresh tokens"
      - "authenticated Langflow API tokens"
      - "model provider secrets available to Langflow"
      - "workflow execution credentials"

    Indicators And Detection Selectors

    cves: ["CVE-2025-34291"]
    packages: ["langflow"]
    fixed_or_reference_versions: ["1.9.3"]
    http_selectors:
      - "Origin"
      - "Cookie"
      - "SameSite=None"
      - "refresh token"
      - "refresh_token"
      - "Langflow"

    Detection and Hunting

    Script: local repository and exported telemetry scope

    #!/usr/bin/env python3
    import os
    import sys
    import json
    import subprocess
    from pathlib import Path
    
    ROOT = sys.argv[1] if len(sys.argv) > 1 else "."
    LOG_ROOT = os.environ.get("LOG_ROOT", "")
    OUT = Path(os.environ.get("OUT", "hp-langflow-cve-2025-34291-kev-scope"))
    SINCE = "2026-05-26T00:00:00Z"
    UNTIL = "2026-05-26T23:59:59Z"
    
    PACKAGES = [
    ]
    VERSIONS = [
    ]
    FILES = [
    ]
    DOMAINS = [
      "www.cisa.gov",
      "nvd.nist.gov",
    ]
    URLS = [
      "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json",
      "https://nvd.nist.gov/vuln/detail/CVE-2025-34291",
      "https://github.com/langflow-ai/langflow/releases/tag/v1.9.3",
      "https://github.com/langflow-ai/langflow/issues/11465#event-25774545848",
    ]
    IPS = [
    ]
    HASHES = [
    ]
    PROCESS_PATTERNS = [
    ]
    NETWORK_PATTERNS = [
    ]
    
    # Positive signal: repository, lockfile, artifact, process, or network telemetry contains one of the exact incident selectors above.
    # Escalation: any match tied to a production build, CI run, deployed asset, or secret-bearing host moves the asset to presumed exposed.
    
    OUT.mkdir(parents=True, exist_ok=True)
    indicators_file = OUT / "indicators.txt"
    
    # Collect unique indicators
    indicators = set()
    for group in [PACKAGES, VERSIONS, FILES, DOMAINS, URLS, IPS, HASHES, PROCESS_PATTERNS, NETWORK_PATTERNS]:
        for val in group:
            if val:
                indicators.add(val)
    
    with open(indicators_file, "w") as f:
        for ind in sorted(indicators):
            f.write(ind + "\n")
    
    print(f"[+] Written unique selectors to {indicators_file}")
    
    # Walk local directory
    print(f"[+] Scanning directory: {ROOT} for selectors...")
    matches = []
    exclude_dirs = {"node_modules", "vendor", "dist", ".git"}
    for root, dirs, filenames in os.walk(ROOT):
        dirs[:] = [d for d in dirs if d not in exclude_dirs]
        for filename in filenames:
            filepath = Path(root) / filename
            try:
                content = filepath.read_text(errors="ignore")
                for ind in indicators:
                    if ind in content:
                        matches.append(f"{filepath}: found '{ind}'")
            except Exception:
                pass
    
    if matches:
        (OUT / "repository-indicator-matches.txt").write_text("\n".join(matches) + "\n")
        print(f"[!] Found {len(matches)} matches in codebase!")
    
    # Optional Log Scanning
    if LOG_ROOT and os.path.exists(LOG_ROOT):
        print(f"[+] Scanning telemetry log directory: {LOG_ROOT}...")
        log_matches = []
        for root, _, filenames in os.walk(LOG_ROOT):
            for filename in filenames:
                filepath = Path(root) / filename
                try:
                    content = filepath.read_text(errors="ignore")
                    for ind in indicators:
                        if ind in content:
                            log_matches.append(f"{filepath}: found '{ind}'")
                except Exception:
                    pass
        if log_matches:
            (OUT / "exported-telemetry-indicator-matches.txt").write_text("\n".join(log_matches) + "\n")
            print(f"[!] Found {len(log_matches)} matches in logs!")
    
        if PACKAGES:
            registry_dir = OUT / "registry"
            registry_dir.mkdir(exist_ok=True)
    
    print(f"[+] Wrote scope artifacts under {OUT}")

    Patch, Mitigation, and Verification

    #!/usr/bin/env python3
    import json
    import os
    import re
    import subprocess
    from pathlib import Path
    
    OUT = Path(os.environ.get("OUT", "hp-langflow-cve-2025-34291-closure")).resolve()
    CVE = "CVE-2025-34291"
    NVD_VULN_END = "1.6.9"
    REFERENCE_RELEASE = "1.9.3"
    SOURCE = "https://github.com/langflow-ai/langflow/releases/tag/v1.9.3"
    
    def vt(value):
        return tuple(int(x) for x in re.findall(r"\d+", str(value))[:4])
    
    def ge(left, right):
        l, r = vt(left), vt(right)
        width = max(len(l), len(r), 1)
        return l + (0,) * (width - len(l)) >= r + (0,) * (width - len(r))
    
    OUT.mkdir(parents=True, exist_ok=True)
    result = {"cve": CVE, "reference_release": REFERENCE_RELEASE, "source": SOURCE, "python_runtime": [], "docker_images": []}
    
    pip_cmds = [["python3", "-m", "pip", "show", "langflow"], ["python", "-m", "pip", "show", "langflow"]]
    for cmd in pip_cmds:
        try:
            proc = subprocess.run(cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=20)
        except Exception as exc:
            result["python_runtime"].append({"cmd": cmd, "error": str(exc)})
            continue
        version = ""
        for line in proc.stdout.splitlines():
            if line.lower().startswith("version:"):
                version = line.split(":", 1)[1].strip()
        if version:
            result["python_runtime"].append({
                "cmd": cmd,
                "installed_version": version,
                "nvd_vulnerable_end_including": NVD_VULN_END,
                "vendor_reference_release": REFERENCE_RELEASE,
                "at_or_above_reference_release": ge(version, REFERENCE_RELEASE),
            })
    
    try:
        proc = subprocess.run(["docker", "images", "--format", "{{.Repository}}:{{.Tag}} {{.ID}}"], text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=30)
        for line in proc.stdout.splitlines():
            if "langflow" in line.lower():
                result["docker_images"].append({"image": line, "selector": "langflow"})
    except Exception as exc:
        result["docker_error"] = str(exc)
    
    (OUT / "langflow-cve-2025-34291-version-verification.json").write_text(json.dumps(result, indent=2, sort_keys=True), encoding="utf-8")
    
    # Remediation trigger: any runtime or image below Langflow 1.9.3, especially versions <= 1.6.9, remains open for CVE-2025-34291.
    print(json.dumps({"out": str(OUT), "runtime_checks": len(result["python_runtime"]), "docker_hits": len(result["docker_images"])}, indent=2))

    Downstream Abuse Audits

    Script: GitHub organization run, release, secret, and workflow audit

    #!/usr/bin/env python3
    import os
    import sys
    import json
    import subprocess
    from pathlib import Path
    
    if "ORG" not in os.environ:
        print("ERROR: Set ORG environment variable to the GitHub organization to audit", file=sys.stderr)
        sys.exit(1)
    
    ORG = os.environ["ORG"]
    SINCE = "2026-05-26T00:00:00Z"
    UNTIL = "2026-05-26T23:59:59Z"
    OUT = Path(os.environ.get("OUT", "hp-langflow-cve-2025-34291-kev-github-audit"))
    
    SELECTORS = [
      "www.cisa.gov",
      "nvd.nist.gov",
      "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json",
      "https://nvd.nist.gov/vuln/detail/CVE-2025-34291",
      "https://github.com/langflow-ai/langflow/releases/tag/v1.9.3",
      "https://github.com/langflow-ai/langflow/issues/11465#event-25774545848",
    ]
    
    # Positive signal: a workflow run, release, secret, key, package, or workflow change overlaps the exposure window and references an incident selector.
    # Remediation trigger: unauthorized post-exposure write activity or a secret-bearing run matching an incident selector requires token revocation and downstream cloud/registry review.
    
    OUT.mkdir(parents=True, exist_ok=True)
    (OUT / "runs").mkdir(exist_ok=True)
    (OUT / "logs").mkdir(exist_ok=True)
    (OUT / "repos").mkdir(exist_ok=True)
    
    # 1. Write incident-selectors file
    selectors_file = OUT / "incident-selectors.txt"
    with open(selectors_file, "w") as sf:
        for s in SELECTORS:
            if s:
                sf.write(s + "\n")
    
    # 2. Get list of repos
    print(f"[+] Fetching repositories for organization: {ORG}")
    repo_res = subprocess.run(["gh", "repo", "list", ORG, "--limit", "1000", "--json", "nameWithOwner"], capture_output=True, text=True)
    if repo_res.returncode != 0:
        print(f"[-] Failed to fetch repos: {repo_res.stderr}", file=sys.stderr)
        sys.exit(1)
    
    repos = [r["nameWithOwner"] for r in json.loads(repo_res.stdout)]
    
    for repo in repos:
        safe_repo = repo.replace("/", "__")
        print(f"[+] Auditing repository: {repo}")
    
        # Check runs in the window
        runs_res = subprocess.run([
            "gh", "api", f"/repos/{repo}/actions/runs",
            "-f", "per_page=100",
            "-f", f"created=>={SINCE}",
            "--paginate"
        ], capture_output=True, text=True)
    
        if runs_res.returncode == 0:
            try:
                all_runs = json.loads(runs_res.stdout).get("workflow_runs", [])
                filtered_runs = [r for r in all_runs if r["created_at"] <= UNTIL]
    
                if filtered_runs:
                    with open(OUT / "runs" / f"{safe_repo}-runs.jsonl", "w") as rf:
                        for run in filtered_runs:
                            rf.write(json.dumps(run) + "\n")
    
                            # Fetch log dynamically
                            run_id = str(run["id"])
                            log_res = subprocess.run(["gh", "run", "view", run_id, "--repo", repo, "--log"], capture_output=True, text=True)
                            if log_res.returncode == 0:
                                (OUT / "logs" / f"{safe_repo}-{run_id}.log").write_text(log_res.stdout)
    
                            # Fetch details
                            view_res = subprocess.run(["gh", "run", "view", run_id, "--repo", repo, "--json", "databaseId,workflowName,headSha,event,createdAt,jobs"], capture_output=True, text=True)
                            if view_res.returncode == 0:
                                (OUT / "runs" / f"{safe_repo}-{run_id}.json").write_text(view_res.stdout)
            except Exception as e:
                print(f"[-] Error parsing runs for {repo}: {e}")
    
        # Check releases in window
        subprocess.run(["gh", "api", f"/repos/{repo}/releases", "-f", "per_page=100", "--paginate"], capture_output=True)
        # Check repo secrets updated in window
        subprocess.run(["gh", "api", f"/repos/{repo}/actions/secrets", "-f", "per_page=100", "--paginate"], capture_output=True)
        # Check deploy keys
        subprocess.run(["gh", "api", f"/repos/{repo}/keys", "-f", "per_page=100", "--paginate"], capture_output=True)
    
    # Scan output directory for any indicator selector matches
    print("[+] Scanning gathered telemetry for indicator matches...")
    subprocess.run(["rg", "-n", "--hidden", "--fixed-strings", "-f", str(selectors_file), str(OUT)], capture_output=False)
    
    print(f"[+] Wrote GitHub audit artifacts under {OUT}")

    Script: cloud OIDC and deployment credential follow-on audit

    #!/usr/bin/env python3
    import os
    import json
    import subprocess
    from pathlib import Path
    
    SINCE = "2026-05-26T00:00:00Z"
    UNTIL = "2026-05-26T23:59:59Z"
    OUT = Path(os.environ.get("OUT", "hp-langflow-cve-2025-34291-kev-cloud-audit"))
    AWS_REGIONS = os.environ.get("AWS_REGIONS", "us-east-1").split(",")
    
    # Positive signal: token exchange or privileged write activity occurs in the exposure window from GitHub, CI/CD, package registry, or deployment automation identity.
    # Remediation trigger: unexpected write, deploy, IAM, secret, or registry activity tied to an exposed CI/CD path requires trust-policy disablement and credential rotation.
    
    OUT.mkdir(parents=True, exist_ok=True)
    
    # 1. AWS CloudTrail Audit
    print("[+] Querying AWS CloudTrail for Web Identity token exchanges...")
    aws_events = []
    for region in AWS_REGIONS:
        res = subprocess.run([
            "aws", "cloudtrail", "lookup-events",
            "--region", region,
            "--start-time", SINCE,
            "--end-time", UNTIL,
            "--lookup-attributes", "AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity",
            "--output", "json"
        ], capture_output=True, text=True)
    
        if res.returncode == 0:
            try:
                events = json.loads(res.stdout).get("Events", [])
                for event in events:
                    ct = json.loads(event.get("CloudTrailEvent", "{}"))
                    ct["region"] = region
                    aws_events.append(ct)
            except Exception as e:
                print(f"[-] Error parsing AWS CloudTrail events: {e}")
    
    if aws_events:
        with open(OUT / "aws-assume-role-with-web-identity.jsonl", "w") as f:
            for ev in aws_events:
                f.write(json.dumps(ev) + "\n")
    
        # Audit follow-on events for returned access keys
        for ev in aws_events:
            access_key = ev.get("responseElements", {}).get("credentials", {}).get("accessKeyId")
            region = ev.get("region", "us-east-1")
            if access_key:
                print(f"[+] Enumerating AWS events for AccessKey: {access_key}")
                f_res = subprocess.run([
                    "aws", "cloudtrail", "lookup-events",
                    "--region", region,
                    "--start-time", SINCE,
                    "--end-time", UNTIL,
                    "--lookup-attributes", f"AttributeKey=AccessKeyId,AttributeValue={access_key}",
                    "--output", "json"
                ], capture_output=True, text=True)
                if f_res.returncode == 0:
                    try:
                        f_events = json.loads(f_res.stdout).get("Events", [])
                        with open(OUT / "aws-follow-on-api-calls.jsonl", "a") as ff:
                            for fe in f_events:
                                ff.write(fe.get("CloudTrailEvent", "{}") + "\n")
                    except Exception as e:
                        print(f"[-] Error writing follow-on events: {e}")
    
    # 2. Azure Activity Log Audit
    print("[+] Querying Azure activity logs...")
    az_res = subprocess.run([
        "az", "monitor", "activity-log", "list",
        "--start-time", SINCE,
        "--end-time", UNTIL,
        "--query", "[?contains(operationName.value, 'write') || contains(operationName.value, 'delete') || contains(operationName.value, 'Microsoft.ManagedIdentity')]",
        "-o", "json"
    ], capture_output=True, text=True)
    
    if az_res.returncode == 0:
        (OUT / "azure-write-delete-activity.json").write_text(az_res.stdout)
    
    # 3. GCP Logging Audit
    print("[+] Querying GCP Cloud Logging...")
    gcp_filter = f'timestamp>="{SINCE}" AND timestamp<="{UNTIL}" AND (protoPayload.methodName="google.sts.v1.SecurityTokenService.ExchangeToken" OR protoPayload.methodName:"GenerateAccessToken" OR protoPayload.methodName:"CreateServiceAccountKey" OR protoPayload.methodName:"SetIamPolicy")'
    gcp_res = subprocess.run([
        "gcloud", "logging", "read", gcp_filter,
        "--format", "json"
    ], capture_output=True, text=True)
    
    if gcp_res.returncode == 0:
        (OUT / "gcp-token-and-iam-activity.json").write_text(gcp_res.stdout)
    
    print(f"[+] Wrote cloud audit artifacts under {OUT}")

    Script: registry metadata and artifact audit

    #!/usr/bin/env python3
    import os
    import json
    import subprocess
    from pathlib import Path
    
    SINCE = "2026-05-26T00:00:00Z"
    OUT = Path(os.environ.get("OUT", "hp-langflow-cve-2025-34291-kev-registry-audit"))
    PACKAGES = [
    ]
    VERSIONS = [
    ]
    
    # Positive signal: workflow files or extensions reference the affected action/extension names or versions.
    # Remediation trigger: exposed secrets or OIDC federation policies must be immediately rotated.
    
    OUT.mkdir(parents=True, exist_ok=True)
    
    with open(OUT / "affected-versions.txt", "w") as av:
        for version in VERSIONS:
            if version:
                av.write(version + "\n")
    
        # 1. Search local workspace files for the affected actions/extensions
        print("[+] Scanning workspace workflows for selectors...")
        for file in Path(".").glob(".github/workflows/**/*.yml"):
            subprocess.run(["rg", "-n", "--hidden", "--fixed-strings", "-f", str(OUT / "affected-versions.txt"), str(file)])
    
        # 2. HOW TO ROTATE EXPOSED GITHUB ACTIONS SECRETS:
        # Overwrite compromised secrets with newly generated credentials:
        # subprocess.run(["gh", "secret", "set", "COMPROMISED_SECRET_NAME", "--body", "my-new-secret-value", "--repo", "my-org/my-repo"])
        # For organization-level secrets:
        # subprocess.run(["gh", "secret", "set", "COMPROMISED_SECRET_NAME", "--org", "my-org", "--visibility", "private"])
        # Revoke compromised OIDC federated trust credentials in AWS/GCP and redeploy the IAM trust policy:
        # subprocess.run(["aws", "iam", "update-assume-role-policy", "--role-name", "my-role-name", "--policy-document", "file://new-clean-trust-policy.json"])
    
    print(f"[+] Wrote registry audit artifacts under {OUT}")

    Sources

    1. CISA Known Exploited Vulnerabilities catalog JSON
    2. Langflow v1.9.3 release
    3. Langflow issue 11465
    4. NVD CVE-2025-34291