Skip to content

security: add HTTP security headers to MindServer and bind browser viewer to localhost#717

Draft
Z0mb13V1 wants to merge 1 commit intomindcraft-bots:developfrom
Z0mb13V1:security/harden-mindserver-viewer
Draft

security: add HTTP security headers to MindServer and bind browser viewer to localhost#717
Z0mb13V1 wants to merge 1 commit intomindcraft-bots:developfrom
Z0mb13V1:security/harden-mindserver-viewer

Conversation

@Z0mb13V1
Copy link

@Z0mb13V1 Z0mb13V1 commented Mar 3, 2026

3/3/2026
PR #716 consolidated everything from #710, #714, #717, and #718.
This PR was superseded by #716.

Security: Harden MindServer HTTP Headers & Browser Viewer Binding

Summary

This PR adds two targeted security hardening measures to the MindServer web UI and the prismarine-viewer browser viewer. Both changes are zero-breaking, require no new dependencies, and are fully backward-compatible with Docker and cloud deployments.

2 files changed · +26 / −2 lines


Changes

1. MindServer Security Headers (src/mindcraft/mindserver.js)

Adds a middleware layer to all HTTP responses from the MindServer Express app with standard security headers:

Header Value Purpose
X-Content-Type-Options nosniff Prevents browsers from MIME-sniffing responses away from the declared Content-Type
X-Frame-Options DENY Prevents the MindServer HUD from being embedded in iframes on other sites (clickjacking protection)
X-XSS-Protection 1; mode=block Enables the browser's built-in XSS filter
Referrer-Policy strict-origin-when-cross-origin Limits referrer information sent to external origins
Content-Security-Policy default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' ws: wss:; font-src 'self'; Restricts all resource loading to same-origin, with exceptions for inline scripts/styles (needed by the HUD), data URIs for images, and WebSocket connections

Socket.IO CORS Origin Restriction

The Server constructor now receives a cors option that restricts WebSocket connections based on the host_public parameter:

  • host_public = false (default, local development): Only http://localhost:{port} and http://127.0.0.1:{port} are allowed as origins
  • host_public = true (Docker / cloud deployments): Origin is unrestricted (undefined) to allow connections from any host — required when the UI is accessed via a public IP or domain

This ensures that in the default local development mode, no external origin can open a Socket.IO connection to the MindServer.

// Before
io = new Server(server);

// After
const allowedOrigins = host_public
    ? undefined
    : [`http://localhost:${port}`, `http://127.0.0.1:${port}`];

io = new Server(server, {
    cors: {
        origin: allowedOrigins,
        methods: ['GET', 'POST'],
    },
});

2. Browser Viewer Localhost Binding (src/agent/vision/browser_viewer.js)

Adds host: '127.0.0.1' to the mineflayerViewer() call. Without this parameter, prismarine-viewer binds to 0.0.0.0 by default, making the bot's first-person camera feed accessible on all network interfaces — including public-facing ones if the machine has a public IP.

// Before — binds to 0.0.0.0 (all interfaces)
mineflayerViewer(bot, { port: 3000+count_id, firstPerson: true });

// After — binds to localhost only
mineflayerViewer(bot, { host: '127.0.0.1', port: 3000+count_id, firstPerson: true });

Docker compatibility: This does not break Docker deployments. Docker's -p 3000:3000 port mapping connects to the container's loopback interface, so the viewer remains accessible via the mapped port on the host. Only direct network access to the container's IP on port 3000+ is blocked.


Why This Matters

MindServer Headers

The MindServer web UI (/public/index.html) is served over HTTP with no security headers. If a user exposes port 8080 (intentionally or accidentally), the lack of headers means:

  • The page can be embedded in a malicious iframe (clickjacking)
  • Browsers may MIME-sniff responses, potentially executing uploaded content as scripts
  • No Content-Security-Policy means any injected <script> tag loads and executes without restriction
  • Any origin can open a Socket.IO connection and send commands to agents

Browser Viewer Binding

By default, prismarine-viewer binds to 0.0.0.0. On a machine with a public IP (e.g., an EC2 instance), this exposes the bot's live first-person camera feed to the internet on ports 3000+. The feed is unauthenticated and shows everything the bot sees in real-time.


Backward Compatibility

Scenario Before After Breaking?
Local development (default) Works Works, now with security headers + CORS restriction No
Docker with port mapping Works Works — Docker maps to container loopback No
host_public = true (cloud) Works Works — CORS unrestricted when flag is set No
Existing Socket.IO clients on localhost Works Works — localhost origins always allowed No
External Socket.IO clients (local mode) Works (no restriction) Blocked by CORS Yes (intentional)

The only behavioral change is that Socket.IO connections from non-localhost origins are now rejected when host_public = false. This is the intended security improvement.


Testing

  • ESLint passes (pre-existing upstream lint issues in mindserver.js lines 218/226/229 are unrelated — tab indentation + process global)
  • MindServer HUD loads correctly at http://localhost:8080
  • Socket.IO bot connections work (agents connect from same process)
  • Browser viewer renders at http://localhost:3000 with host: '127.0.0.1'
  • Docker docker-compose up — viewer accessible via port mapping
  • host_public = true — no CORS restriction, external access works

Related

…ewer to localhost

- Add security headers middleware to mindserver.js:
  - X-Content-Type-Options: nosniff
  - X-Frame-Options: DENY
  - X-XSS-Protection: 1; mode=block
  - Referrer-Policy: strict-origin-when-cross-origin
  - Content-Security-Policy: restricts resource loading to same-origin
- Add Socket.IO CORS origin restriction:
  - Localhost-only when host_public=false (default)
  - Unrestricted when host_public=true (Docker/cloud deployments)
- Bind prismarine-viewer to 127.0.0.1 instead of default 0.0.0.0:
  - Prevents bot camera feeds from being accessible on all network interfaces
  - Only affects local development; Docker port mapping still works
Copilot AI review requested due to automatic review settings March 3, 2026 15:27
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens local/dev HTTP exposure by adding security-related HTTP response headers to the MindServer Express app and by changing the prismarine-viewer bind address to localhost to reduce unintended network exposure.

Changes:

  • Add Socket.IO CORS origin restriction keyed off host_public, and add standard HTTP security headers (incl. CSP) to MindServer responses.
  • Bind the prismarine-viewer browser viewer to 127.0.0.1 instead of all interfaces.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/mindcraft/mindserver.js Adds Socket.IO CORS config and sets multiple security headers (including CSP) for all HTTP responses.
src/agent/vision/browser_viewer.js Changes prismarine-viewer to bind to 127.0.0.1 rather than the default.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

export function addBrowserViewer(bot, count_id) {
if (settings.render_bot_view)
mineflayerViewer(bot, { port: 3000+count_id, firstPerson: true, });
mineflayerViewer(bot, { host: '127.0.0.1', port: 3000+count_id, firstPerson: true });
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Binding prismarine-viewer to 127.0.0.1 will make the viewer unreachable from outside the process namespace (e.g., typical Docker -p/docker-compose port publishing won’t reach a service that only listens on loopback inside the container). Consider making the bind host configurable (e.g., default to localhost for non-container local dev, but allow 0.0.0.0 when explicitly requested) so existing container/remote viewing setups keep working while still enabling a secure default.

Copilot uses AI. Check for mistakes.
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' ws: wss:; font-src 'self';"
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CSP connect-src 'self' ws: wss: allows WebSocket connections to any host over ws/wss (scheme-only sources), which largely defeats the intent of restricting connections to same-origin. If the UI only needs same-origin Socket.IO/WebSockets, remove ws:/wss: and rely on 'self'; otherwise, specify explicit websocket endpoints/hosts instead of allowing all.

Suggested change
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' ws: wss:; font-src 'self';"
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; font-src 'self';"

Copilot uses AI. Check for mistakes.
Comment on lines +54 to +61
? undefined // allow any when explicitly public (Docker/EC2)
: [`http://localhost:${port}`, `http://127.0.0.1:${port}`];

io = new Server(server, {
cors: {
origin: allowedOrigins,
methods: ['GET', 'POST'],
},
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

allowedOrigins is set to undefined when host_public is true and then passed through as cors.origin. That relies on Socket.IO/cors default handling of an explicit origin: undefined, which is ambiguous and can be interpreted differently across versions/config. To make the public mode behavior explicit and stable, either omit the cors option entirely when host_public is true, or set origin to an explicit permissive value (e.g., '*' or true, depending on desired credentials behavior).

Suggested change
? undefined // allow any when explicitly public (Docker/EC2)
: [`http://localhost:${port}`, `http://127.0.0.1:${port}`];
io = new Server(server, {
cors: {
origin: allowedOrigins,
methods: ['GET', 'POST'],
},
? undefined // legacy behavior: previously relied on origin: undefined
: [`http://localhost:${port}`, `http://127.0.0.1:${port}`];
const corsOptions = host_public
// Explicitly allow any origin when running in public mode
? { origin: true, methods: ['GET', 'POST'] }
// Restrict origins to local development URLs when not public
: { origin: allowedOrigins, methods: ['GET', 'POST'] };
io = new Server(server, {
cors: corsOptions,

Copilot uses AI. Check for mistakes.
@Z0mb13V1 Z0mb13V1 closed this Mar 3, 2026
@Z0mb13V1 Z0mb13V1 deleted the security/harden-mindserver-viewer branch March 3, 2026 18:35
@Z0mb13V1 Z0mb13V1 restored the security/harden-mindserver-viewer branch March 3, 2026 18:36
@Z0mb13V1 Z0mb13V1 deleted the security/harden-mindserver-viewer branch March 3, 2026 19:09
@Z0mb13V1 Z0mb13V1 restored the security/harden-mindserver-viewer branch March 3, 2026 20:40
@Z0mb13V1 Z0mb13V1 reopened this Mar 3, 2026
@Z0mb13V1 Z0mb13V1 marked this pull request as draft March 3, 2026 20:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants