Skip to content

Security: art3nu/hexmask

Security

SECURITY.md

Security

Short version: this tool runs entirely on your own machine, opens no account, sends nothing anywhere, and the one port that is open to your network is open because your phone has to reach it.

Long version below, including the two things that look alarming in a virus scanner and are not, and the one thing you should genuinely be careful about.


What listens, and who can reach it

What Port Bound to Who can connect
Live Link Face bridge (bridge/llf_bridge.py) UDP 11111 0.0.0.0 anything on your local network
Hero picker (picker/picker_server.py) TCP 8796 127.0.0.1 this machine only
GSI listener (bridge/gsi.py), optional TCP 8794 127.0.0.1 this machine only
Warudo's own API (not ours) TCP 19053 ::1 this machine only

Only the bridge is open to the network, and it has to be. Your iPhone sends its face data over WiFi; a socket bound to 127.0.0.1 could not receive it. That is the same trade every phone-as-a-webcam app makes.

What that means in practice: anyone on the same WiFi can send packets to that port. They cannot read anything back (it never replies), they cannot make it run anything (it only ever converts numbers and forwards them to Warudo on localhost), and they cannot crash it — every packet is parsed inside a guard and any packet that does not make sense is counted and dropped. Run python bridge/llf_bridge.py --selftest to see the hostile-packet cases the parser is tested against.

The worst a hostile device on your WiFi can do is send fake face data, which would make your avatar pull faces. If you are on a network you do not trust (a hotel, a con, a LAN party), either do not use the phone route there, or add a Windows Firewall rule limiting UDP 11111 to your phone's IP.

The hero picker has no password. It does not need one, because it only listens on 127.0.0.1 — nothing on your network can reach it at all. Three guards keep it that way:

  • it refuses to bind to anything but localhost unless you pass --allow-lan and mean it;
  • it checks the Host header, so a malicious website cannot reach it by pointing its own domain name at 127.0.0.1 (a trick called DNS rebinding);
  • POST /api/switch refuses cross-site requests, so a random web page you have open in another tab cannot change your avatar in the middle of your stream.

If you ever do run it with --allow-lan, understand what you are agreeing to: anyone who can reach that port can switch your avatar while you are live. Prefer a Tailscale or SSH tunnel over opening the port.

Two things that look scary in a virus scanner

1. It reads another program's command line. picker/warudo_api.py runs a PowerShell query (Get-CimInstance Win32_Process) to read the command line of warudo-client-electron.exe. That is genuinely a technique malware uses to hunt for passwords, and a strict EDR may flag it.

Here is why it is here: Warudo generates a fresh API token every time it launches and hands it to its own editor window as a command-line argument. It is never written to disk and never put in the registry. Reading that command line is the only way to talk to Warudo's live API — which is what makes switching heroes instant instead of a scene reload. The token is used once, on ws://[::1]:19053, and never leaves your machine. It is not stored. If you are not comfortable with it, you can switch heroes by loading the .vrm in Warudo's own UI; you just lose the one-click picker.

2. It writes into your Steam folder. ensure_link() creates a directory junction (mklink /J) inside Warudo/Warudo_Data/StreamingAssets/Characters/, pointing at the folder where your built avatars live. No file is copied, nothing of Warudo's is modified, and no administrator rights are needed. It exists so Warudo can see 1.3 GB of avatars without a second copy of them. Delete the Dota junction any time; the tool recreates it. Nothing is ever written into the Dota 2 folder — Dota's archive is only ever read.

Supply chain

This project downloads exactly one binary: Source2Viewer-CLI, the open-source (MIT) tool that reads Valve's .vpk archives. Version and hash are pinned in tools/get_source2viewer.py, and the download is refused and deleted if the SHA-256 does not match:

ValveResourceFormat 19.2, cli-windows-x64.zip
  50,837,364 bytes
  sha256 53e7e8dac1ddd876078346de709c8dbe613a967e94cd0c969aa34c61ec07680d
  -> Source2Viewer-CLI.exe, 108,603,232 bytes
     sha256 36d8c9208eefa61dd695bd577e49618bb161569941318f629294a4e4af00edc0

Check a copy you already have:

python tools/get_source2viewer.py --verify

Python dependencies live in requirements.txt with security floors, not convenience floors. Re-check them yourself at any time:

pip install pip-audit
pip-audit -r requirements.txt

Everything else — Dota 2, Warudo, OBS, Blender — you install yourself from its own publisher. This project never bundles or re-hosts any of them.

What this tool never does

  • No network calls except the pinned Source2Viewer download.
  • No telemetry, no analytics, no crash reporting, no account.
  • No eval, no exec, no pickle, no yaml.load, no shell=True anywhere in the codebase. (bandit -r . and ruff check --select S confirm.)
  • No writes outside your own user-data folder, the avatar output folder, and the one junction described above.
  • No Valve artwork is distributed. Everything is extracted, on your machine, from the copy of Dota 2 you already own.

Reporting a problem

Open a GitHub issue. If it is something you would rather not post publicly, say so in the issue without details and a private channel will be arranged.

Please include your OS, Python version, and the output of python -m hexmask.doctor.

There aren't any published security advisories