#292

Security: bind to localhost, require an API token off-host, confine file paths

Openwilsonhjwants to merge intofrom

Summary

The API server listens on every network interface and has no authentication. Anyone who can reach port 8888 can therefore run commands on the host. This is CVE-2025-35028 / GHSA-2j8m-rmq4-rcw5.

The cause is that app.run() hard-codes 0.0.0.0, even though the startup banner says 127.0.0.1.

This PR makes the server local-only by default. It requires a token before the server will listen on any other address.

Nothing changes for local use.

Fixes #122 Fixes #115 Addresses #124, #263

What changes

AreaBeforeAfter
Bind addressAlways 0.0.0.0127.0.0.1 by default; --host or HEXSTRIKE_HOST to change it
AuthenticationNoneWith HEXSTRIKE_API_TOKEN set, every request needs Authorization: Bearer <token> (constant-time compare)
No-token modeAny Host header acceptedOnly localhost, 127.0.0.1 or ::1. Blocks DNS rebinding.
StartupStarts anywhereRefuses a non-local --host without a token, and refuses --debug on a non-local host
File and env pathsRequest paths joined uncheckedKept inside /tmp/hexstrike_files and /tmp/hexstrike_envs. Rejects absolute paths, .., symlink escapes, and deleting the base folder.
Browser agentAny URL schemehttp and https only
MCP clientNo credentialsSends the token from HEXSTRIKE_API_TOKEN

Also in this PR:

  • SECURITY.md: threat model, deployment rules, private reporting.
  • README: --host and token setup.
  • Sample MCP config: an env block for the token.
  • Minimum Flask version raised to 2.3.2.
  • 21 regression tests.
  • A step-by-step testing guide for reviewers.

Issues

IssueStatus
#122: binds all interfaces✅ Fixed
#115: no SECURITY.md; CVE-2025-35028✅ Fixed
#124: /api/command RCE✅ Fixed for unauthenticated callers ¹
#263, #224: injection in /api/tools/*✅ Fixed for unauthenticated callers ¹
#204: pickle deserialization➖ Not reproducible ²
#161: Flask CVE-2023-30861➖ Not exploitable here; Flask floor raised ³

¹ The API runs commands by design, so an authenticated caller still can. The token is the control, as SECURITY.md documents. ² No request data is deserialized. The pickle code (hexstrike_server.py:7124, :8189) is text that the exploit generator outputs. ³ The CVE requires Flask sessions, and the server uses none.

It also fixes four problems that have no open issue:

  • Path traversal in /api/files/*.
  • DNS rebinding.
  • The Werkzeug debugger exposed on 0.0.0.0.
  • file:// and javascript: URLs in the browser agent.

⚠️ Breaking change: remote setups

A server on another machine now needs the same token on both sides, or it exits at startup.

On the server:

export HEXSTRIKE_API_TOKEN="$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')"
python3 hexstrike_server.py --host 0.0.0.0

In the MCP client config:

"env": { "HEXSTRIKE_API_TOKEN": "<same value>" }

The token is sent over plain HTTP. Across networks, use an SSH tunnel or a TLS reverse proxy.

Testing

To validate this yourself, follow tests/SECURITY_TESTING.md.

  • Part A (about 2 minutes) runs the regression tests and shows that they fail on master.
  • Part B (about 10 minutes) checks a running server by hand. It gives the expected output for each step.

Quick check:

pip install flask requests
python3 -m unittest discover -s tests
  • 21 of 21 tests pass, on Python 3.13 with mitmproxy stubbed, and on Python 3.12 with the real dependencies (Flask 3.0.3, mitmproxy 10.4.2).
  • 19 of the 21 fail on master. The two that pass there check normal file operations.
  • Every manual step in the guide was run against a live server:
    • Startup refusals: exit 1.
    • Bind address: 127.0.0.1.
    • Foreign Host: 403.
    • Escaping file paths: refused.
    • Token mode: 401, 401, 200.
    • MCP client: connects with the token, gets 401 without it.
    • The optional browser step (B7) needs chromedriver and was not run.
  • Security review of the full diff: no high-confidence findings.

Not verified:

  • A full MCP stdio session driving tools against a live server.
  • The client needs mcp<2, because its from mcp.server.fastmcp import FastMCP import fails with mcp 2.x. This predates this PR.

Out of scope

  • Converting the ~90 /api/tools/* handlers from shell=True to argument lists. It would give authenticated callers no capability beyond what /api/command already allows.
  • TLS.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YPrL9aG45iKPhpYzo3mgR6