Severity Daily

IT and AI security incidents, checked against the primary source

Tag: PyPI

  • Weights & Biases fixed a path traversal in run file downloads and logged it as an artifact fix

    Weights & Biases fixed a path traversal in run file downloads and logged it as an artifact fix

    The fix is real, it shipped three weeks ago, and the release note describing it names two artifact methods and not the run file API that the CVE published this morning is actually about.

    What happened

    NVD published CVE-2026-91771 on Tuesday, September 15, 2026, at 2:16 a.m. UTC. The record’s vulnStatus is “Received.” VulnCheck, the assigning CNA, scores it 8.8 under CVSS v3.1 with the vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H, and 8.7 under CVSS v4.0. The weakness is CWE-22, path traversal.

    The description reads: “Weights & Biases wandb before 0.29.0 fails to validate the file name from server responses in the File.download function, allowing path traversal attacks.”

    The wandb Python client is the SDK that machine learning teams import to log runs, metrics, and artifacts to Weights & Biases, either the hosted service or a self-managed server. File.download is the public-API method that pulls a file a run previously saved back down to a local directory.

    The vulnerable code is short and readable. In wandb/apis/public/files.py at tag v0.27.2, inside the download wrapper:

    path = os.path.join(root, self.name)

    root is the caller’s destination directory, defaulting to ".". self.name is not the caller’s — it comes from the file’s attributes as returned by the server. os.path.join discards the first argument entirely if the second is absolute, and preserves ../ segments to be resolved on write. The resulting path goes straight to download_file_from_url.

    The public report spells out what that buys an attacker. GitHub issue #12592, titled “Path traversal in public API File.download enables arbitrary file write,” was opened by the researcher geo-chen on June 15, 2026. It identifies the os.path.join call by line, describes a malicious or compromised backend returning crafted filenames, and lists the write targets that matter: shell startup files, cron entries, SSH keys, and directories on the Python import path. It also proposed the fix the project eventually used — apply the make_file_path_upload_safe validation already used on the upload side, or verify the resolved destination stays inside the resolved root. The reporter noted follow-ups on June 29 and August 4.

    The fix, and what the changelog says about it

    Pull request #12516, “fix(artifacts): validate path when using run file api to download,” was opened on August 20, 2026, at 10:19 p.m. UTC and merged on August 21, 2026, at 7:54 p.m. UTC as commit e52558f. The change to files.py is two lines added and one removed: an import of validate_fspath, and the replacement of the os.path.join call with path = validate_fspath(root, self.name). The commit also added validate_relpath and validate_fspath to wandb/sdk/lib/paths.py, a validate_artifact_root_name helper to the artifacts validators, and 28 parametrized test cases.

    The PR body explains the reasoning. It cites an internal ticket, WB-38900, and an earlier pull request, #11735, which “added validation to reject relative and abs paths when downloading artifact files.” It then says: “But there are more ways to download: artifact can be downloaded using the file api via artifact.files() file.download, which does not check path. This PR applies same logic to File.download before sending the path to go core for download.”

    The body does not reference issue #12592, or any security report, or any reporter.

    The release note follows the PR’s framing. Under the Security heading in the 0.29.0 notes, the entry reads: “Reject file or artifact name that contain relative path when downloading artifacts via `artifact.files()`, `artifact.checkout()`”.

    That is accurate as far as it goes. artifact.files() returns File objects whose download method is the patched function. But File.download is also what run.files() returns, and the run file API is the entry point the pull request named in its own title and the one the CVE description points at. It appears nowhere in the release notes.

    A wandb user reading the 0.29.0 notes to work out whether this affects them sees two artifact methods. If their code does run.files() and iterates file.download(root=...) — the documented way to retrieve run outputs — nothing in the release notes tells them the fix is theirs.

    The dates

    PyPI’s upload records put wandb 0.29.0 on the index on August 26, 2026, at 11:42 p.m. UTC, five days after the merge. Version 0.30.0 followed on September 9, 2026, at 12:16 a.m. UTC and is the current release; the validate_fspath call is present in both, at the same line.

    Laid end to end: the public report landed June 15, the fix merged August 21 — 67 days later — the fixed build reached PyPI on August 26, and the CVE record appeared this morning, 92 days after the report and 20 days after anyone could install the fix.

    Why it matters

    The interesting part is not the bug. It is a textbook os.path.join traversal, correctly fixed, in a codebase that had already built the validation helper for the upload direction and simply had not wired it into the download direction. That happens everywhere.

    What is worth attention is how a fix gets described, and by whom. The pull request was written as the continuation of an earlier piece of hardening work, tracked internally, framed in artifact terms. The changelog entry inherited that frame. Neither document was wrong; both were written by someone solving a class of problem, not answering a report. The report existed, in public, for 67 days, and it named the exact function and the exact attack. Nothing connects the two in the public record except the CVE that arrived this morning, and the CVE arrived from VulnCheck — a third party — not from the vendor.

    This has a practical consequence for anyone who triages by changelog, which is most teams that pin dependencies. The signal that would tell a security-conscious wandb user to prioritize 0.29.0 is the phrase “run file” or “File.download,” and it is not there. The signal that is there — “downloading artifacts” — is easy to read as not applicable if your pipeline does not use the artifact API. Severity Daily reported the same shape last week on The Events Calendar, where three releases of changelog never said the word that mattered.

    The threat model deserves stating plainly, because the 8.8 overstates how easy this is. The filename is server-controlled, so the attacker needs to control or influence what the W&B backend returns. That means a compromised or malicious self-managed server, a hostile instance an engineer was pointed at, or an attacker who can alter a file’s stored name on a shared team account. It is not a flaw a random internet attacker reaches. But the payoff is a write to ~/.bashrc or a directory on the Python path, on a machine that by definition has cloud credentials and training data on it, and it happens during an operation nobody inspects — downloading your own files from your own experiment tracker.

    Finally: the bug sat on a public tracker, readable by anyone, for more than two months while the vulnerable version remained current. That is no criticism of the reporter, who did what the tracker is for. It is a reminder that across much of the ML tooling stack, the gap between a public exploit path and a fixed build runs to months, and nothing in that window emits a signal a scanner can read.

    What to do

    • Upgrade to wandb 0.30.0. The fix first shipped in 0.29.0, but 0.30.0 is current and carries the same validate_fspath call. Pin it in requirements.txt, pyproject.toml, and any container base image used for training or evaluation jobs.
    • Check for wandb in build and CI images, not just notebooks. The client is routinely baked into training containers and inherited by every job that runs them.
    • Verify the version with pip show wandb rather than by reading a lockfile, since the affected range is everything below 0.29.0 and many pinned environments sit on the 0.27 and 0.28 lines from earlier this year.
    • Confirm which W&B backend your SDK talks to. If WANDB_BASE_URL points at a self-managed or third-party instance, that host is the trust anchor this bug depends on. Treat unexplained files appearing in home directories or on sys.path on ML workstations as worth investigating.
    • Do not use the 0.29.0 release notes to decide whether you are affected. If your code calls download() on anything returned by run.files(), you were affected, whatever the artifact-worded entry suggests.

    Sourcing note

    Checked: the NVD record for CVE-2026-91771, carrying VulnCheck’s CVSS v3.1 and v4.0 scores, CWE-22, the affected range, and six reference URLs; the VulnCheck advisory page, which gives the description and the fixed version but publishes no timeline; wandb/apis/public/files.py read directly at tags v0.27.2, v0.29.0, and v0.30.0, confirming the os.path.join call before the fix and the validate_fspath call after it; commit e52558f and pull request #12516 through the GitHub API for the title, author, creation and merge timestamps, body text, and per-file diff; GitHub issue #12592 for the report and its date; the 0.29.0 release notes for the Security entry, quoted verbatim; and PyPI’s upload records for 0.29.0 and 0.30.0.

    Not established: whether Weights & Biases and the reporter corresponded outside the public issue, and whether the vendor requested or was consulted on the CVE — the record was assigned by VulnCheck, and no vendor security advisory for this issue was found. The closing state of issue #12592 could not be confirmed; GitHub’s issues API returned 403 to automated fetching. No exploitation has been reported by any source, and the CVE is not in CISA’s Known Exploited Vulnerabilities catalog, read directly from its published JSON at version 2026.09.14. The KEV catalog was reached through that public JSON file rather than cisa.gov, which returns 403 to automated requests.

  • Langflow’s component scanner ran the code it was checking and returned ‘validated: true’; the fix shipped in June and the CVE arrived Monday

    Langflow’s component scanner ran the code it was checking and returned ‘validated: true’; the fix shipped in June and the CVE arrived Monday

    IBM’s bulletin says the scanner’s DANGEROUS_IMPORTS blocklist missed socket and urllib, so submitted code ran server-side during the check and the check reported success; the fix reached PyPI on June 23 and the CVE record arrived Monday evening.

    What happened

    NVD published CVE-2026-12944 on Monday, September 14, 2026, at 10:17 p.m. UTC — 5:17 p.m. Central — with vulnStatus of “Received.” IBM’s PSIRT scores it 9.6 with the vector CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N. The assigned weakness is CWE-918, server-side request forgery.

    The record’s description: “IBM Langflow OSS 1.0.0 through 1.10.0 can allow attackers to execute arbitrary Python code with root privileges (UID=0) on the Langflow server by submitting components containing socket or urllib imports.”

    The referenced IBM security bulletin is titled “Incomplete Security Scanner Blocklist Enables Network-Based Code Execution,” and its summary is the part worth reading closely. Langflow’s code security scanner maintains a blocklist named DANGEROUS_IMPORTS, intended to reject components that import modules capable of doing damage. That blocklist did not include socket or urllib. An authenticated user could therefore submit a component importing either one, and — in IBM’s account — the dangerous network operations “executed server-side during validation while returning a false ‘validated: true’ signal.” The bulletin lists the consequences as SSRF, reverse shells, and exfiltration of sensitive data.

    Two mechanics in that sentence matter separately. The first is that the code ran at all: validation was not a static inspection that decided whether to accept the component, it was an execution that happened before the decision. The second is that the decision came back positive. The control designed to catch dangerous components both ran the dangerous component and then marked it safe.

    IBM gives the affected range as Langflow OSS 1.0.0 through 1.10.0 and the remedy as an upgrade to 1.10.1, distributed through PyPI. It documents no workarounds. The bulletin credits KIM MINJUN, GitHub handle 31n6.

    The dates

    The bulletin’s change history has one entry: “02 Jul 2026: Initial Publication.” PyPI’s own upload records for the langflow package put 1.10.1 on the index at 2026-06-23T23:49:18 UTC. So the fixed build was publicly installable on June 23, 2026, the bulletin describing what it fixed appeared nine days later on July 2, and the CVE record that a scanner or an SBOM pipeline would key on arrived 83 days after the fix and 74 days after the bulletin.

    That gap is the reason this is a new story rather than an old one. Severity Daily covered nine Langflow CVEs on September 4, all of which trailed their bulletins by about a week. This one trailed by ten times that.

    Two further notes on the record as published. The 1.10.1 IBM names is not a current release: PyPI’s latest langflow is 1.12.1, uploaded on September 8, 2026, and eight stable releases separate it from the fix IBM points at. And the weakness class, CWE-918, describes SSRF — which is one of the things the bulletin lists, but the description in the same record says arbitrary Python code execution as root. A triage queue sorted on CWE will place this alongside a metadata-endpoint fetch, and the record’s own text says it is worse than that.

    Why it matters

    Langflow is a visual builder for LLM applications. Its unit of work is a component — a block of Python that a user writes or pastes into a flow. The entire product model is that users bring code and the platform runs it. A platform built that way has exactly one thing standing between a user and the host: the check that decides which code is acceptable. This CVE is a failure of that one thing, in two directions at once.

    Consider what the blocklist approach commits you to. DANGEROUS_IMPORTS is a deny list, which means its correctness depends on having enumerated every module that can reach the network, the filesystem, or a subprocess — and on doing so again every time Python’s standard library grows a way to do those things. socket and urllib are not exotic. They are the two most obvious network modules in the standard library, which is what makes their absence instructive rather than embarrassing: a deny list maintained by hand will be incomplete, and the only question is which entries are missing this quarter. An allow list of permitted imports fails closed when it is wrong. A deny list fails open.

    The false validated: true is the more serious half. A control that misses something leaves you where you would have been without it. A control that misses something and reports success leaves you worse off, because it has manufactured evidence. Anyone operating a shared Langflow instance between 1.0.0 and 1.10.0 has a log of validation results that says components were checked and passed. Those records are not a clean bill of health for the period; they are the output of a scanner that could return that answer for a component that had already opened a socket.

    The privileges tell the rest. PR:L means an authenticated user is required, which sounds like a meaningful barrier and often is not — a shared team instance of an AI flow builder typically has accounts for everyone who builds flows, and this is a privilege-escalation path from “can edit a flow” to root on the host. UID=0 in the description means the process runs as root, so the common containerized deployment offers no user-level separation between the flow runtime and the container. S:C, scope changed, is IBM’s own acknowledgment that the impact crosses out of the vulnerable component. The reverse shell and the credential theft the bulletin mentions are not the interesting consequences by themselves; the interesting consequence is that whatever the Langflow host can reach — internal APIs, databases, cloud metadata endpoints — an ordinary flow author could reach with a component that came back marked safe.

    There is no evidence of exploitation here, and none is claimed. What there is instead is a long quiet window: three months in which the fix existed, the bulletin existed, and the machine-readable record that most organizations actually depend on did not. An organization that upgraded through the summer for other reasons is fine and never knew there was anything to know. An organization that pins versions and upgrades on CVE signals learned about this on Monday evening.

    What to do

    Determine your installed langflow version. If it is 1.0.0 through 1.10.0, you are in the affected range. IBM’s stated fix is 1.10.1; the current release is 1.12.1, and unless something specific pins you to the 1.10 line, 1.12.1 is the version to move to, since taking 1.10.1 today means accepting a build from late June and none of the fixes since.

    There is no workaround, so the upgrade is the remediation. Until it lands, the practical mitigation is access: an instance where component authoring is restricted to a small, trusted set of accounts is a much smaller problem than one where any team member can add a component.

    Do not treat past validation results as assurance. If you retain flow or component history from an affected version, the components submitted in that window are worth a look — specifically for imports of socket or urllib in components that were accepted. That is a narrow, checkable question with a clear answer, and it is the closest thing to a compromise assessment available without host-level logs.

    Separately, check what the Langflow process can reach. If it runs as root in a container with broad network access to internal services, the upgrade closes this path and leaves the shape of the blast radius unchanged for the next one. Egress restrictions and a non-root runtime are the durable answer for a platform whose job is running code that users bring.

    Sourcing note

    Checked: the NVD record for CVE-2026-12944 for publication time, status, CVSS vector and score, CWE, affected range, and description; IBM’s security bulletin at ibm.com/support/pages/node/7278919 for the title, the DANGEROUS_IMPORTS mechanism, the “validated: true” wording, the affected versions, the 1.10.1 remedy, the absence of workarounds, the change-history date, and the credit; and PyPI’s JSON API for the langflow package for the 1.10.1 upload timestamp and the current release. All quotations are from the NVD record or the IBM bulletin.

    Unresolved: IBM does not say why the CVE record trailed the bulletin by 74 days, and no exploitation has been reported by IBM or observed publicly. The bulletin does not state whether the fix replaced the deny list with a different approach or simply added the two missing modules, and that distinction determines whether the next standard-library network module reopens the same hole; this page could not establish it from the bulletin text. The affected range ends at 1.10.0, so releases on the 1.11 and 1.12 lines are outside it, but IBM has not stated the fix’s presence in those lines explicitly.