security: add HTTP security headers to MindServer and bind browser viewer to localhost#717
Conversation
…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
There was a problem hiding this comment.
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.1instead 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 }); |
There was a problem hiding this comment.
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.
| 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';" |
There was a problem hiding this comment.
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.
| "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';" |
| ? 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'], | ||
| }, |
There was a problem hiding this comment.
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).
| ? 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, |
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:
X-Content-Type-OptionsnosniffContent-TypeX-Frame-OptionsDENYX-XSS-Protection1; mode=blockReferrer-Policystrict-origin-when-cross-originContent-Security-Policydefault-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' ws: wss:; font-src 'self';Socket.IO CORS Origin Restriction
The
Serverconstructor now receives acorsoption that restricts WebSocket connections based on thehost_publicparameter:host_public = false(default, local development): Onlyhttp://localhost:{port}andhttp://127.0.0.1:{port}are allowed as originshost_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 domainThis ensures that in the default local development mode, no external origin can open a Socket.IO connection to the MindServer.
2. Browser Viewer Localhost Binding (
src/agent/vision/browser_viewer.js)Adds
host: '127.0.0.1'to themineflayerViewer()call. Without this parameter, prismarine-viewer binds to0.0.0.0by 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.Docker compatibility: This does not break Docker deployments. Docker's
-p 3000:3000port 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:<script>tag loads and executes without restrictionBrowser Viewer Binding
By default,
prismarine-viewerbinds to0.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
host_public = true(cloud)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
mindserver.jslines 218/226/229 are unrelated — tab indentation +processglobal)http://localhost:8080http://localhost:3000withhost: '127.0.0.1'docker-compose up— viewer accessible via port mappinghost_public = true— no CORS restriction, external access worksRelated
develop