Security model
A tool that demonstrates credential theft has to survive the question "what if it is pointed at me?". v1 did not; v2 was rebuilt around the answer, and the test suite pins each control closed.
What v1 got wrong
| Sev | v1 problem | v2 control |
|---|---|---|
| CRIT | Listened on :8080 — the console was LAN-reachable |
Binds 127.0.0.1 only; ListenAndServe refuses a non-loopback address |
| CRIT | Token derived from the PID — visible to any process — and echoed to unauthenticated GET / |
256-bit crypto/rand token, regenerated per run, compared in constant time |
| CRIT | CheckOrigin: return true — any web page could open the WebSocket and drive the console |
Origin must be loopback and the ?t= token must match |
| CRIT | No Host validation — DNS rebinding could read the token out of the page | Every route rejects a non-loopback Host |
| HIGH | Mixed GET mutations, no method enforcement | ServeMux method patterns: automatic 405 on the wrong verb |
| HIGH | Token in HTML readable cross-origin (no CSP, no headers) | Same-origin only, plus CSP, nosniff, DENY framing, no-store |
| HIGH | DOM XSS: database-derived URLs rendered through innerHTML |
All server data escaped through window.esc() before it reaches the DOM |
| MED | Auto-UAC on every start | Elevation only when asked for: --elevate, or a module that cannot work unelevated |
| MED | Credentialed collectors reachable by default | Locked behind --enable-*, failing closed with an actionable 403 |
| MED | Privileged modules failed opaquely — SAM errors looked like "no SAM material", LSASS failures hung the UI | The real cause is streamed to the dashboard (sam_error / lsass_error), UAC is requested up front, and a relaunch that yields no elevated token fails loudly instead of looping |
Where the boundary sits
Anyone who can read the dashboard page — loopback, same origin — is the operator. That is the design: the console answers to the token, and the token is a lock on the door, not on the room.
Against a hostile web page, the boundary is airtight, and each layer is independent:
It can reach the port
But a cross-origin fetch() cannot read the response — same-origin policy.
It cannot set the header
The custom X-Signal-Token header forces a CORS preflight, which is never approved, so the request is never sent. This is what protects state-changing routes from CSRF.
It cannot guess the token
256 bits from crypto/rand, regenerated per run, compared in constant time.
Rebinding gets nothing
A rebinding page reaches the port with its own hostname in Host, which is refused before any handler runs.
Two limits, documented rather than hidden
GET / serves the dashboard with the token inside it, and that route is guarded only
by the loopback Host check. Any process that can reach loopback — including one
running as a different local user, since loopback is machine-scoped — can read the token
and then use POST /v1/sys/cmd to execute commands as the user running the console.
The practical consequence is a deployment rule: run the console on a single-user
workstation. On a shared or RDP host it is a cross-user escalation path.
POST /v1/sys/opt changes the machine with no flag.
It writes HKLM policy and stops + disables the DiagTrack service, and unlike every
other state-changing capability it requires only the token — there is no
--enable-* gate, because the action is privacy hardening rather than credential
access. It is listed on the Capabilities page so nobody
discovers it by accident.
Both are written into the repository's SECURITY.md under "Documented limits" rather than left to be discovered by whoever reads the code next. The discussion, the trade-offs and the rejected options live in issue #6, which is closed.
Intentional capabilities
These are features, not bugs — they are the reason the tool exists:
POST /v1/sys/cmdexecutes shell commands as the token holder.--enable-collectdecrypts browser credential stores and the local SAM/SYSTEM hives on the local host.--enable-injectperforms W^X process injection into an operator-selected local PID.--enable-persistcreates and removes local persistence footholds.--enable-lateralexecutes commands on operator-supplied remote hosts over WinRM or SCM.--enable-evasionneuters ETW/AMSI in this process only.
They are reachable only over loopback, only with the per-run token, and the credential, persistence, evasion, lateral and injection modules additionally require their flag at startup. Hardening regressions in that chain are in scope for vulnerability reports.
Out of scope
Physical access, kernel-level attackers, and the operator console itself once the token holder is trusted. Reports about the intentional capabilities above are answered with a pointer to this page unless they show the gate failing — a route reachable without its flag, a non-loopback bind, a missing Host or Origin check, a token comparison that leaks, or a state-changing route that a hostile page can reach.
Reporting a vulnerability
Report privately, never in a public issue with exploit details: GitHub →
Security → Report a vulnerability on
DevCop95/pullgoscript,
or open a minimal issue titled [SECURITY] asking for a private channel. Include the
affected version or commit, impact, reproduction steps and any suggested fix. Expect an
acknowledgement within 72 hours and a status update within 10 business days; fixes are credited
in the release notes unless you prefer otherwise. The full policy is in
SECURITY.md.