-
Notifications
You must be signed in to change notification settings - Fork 5
Harden local transcript explorer security #17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,8 +6,8 @@ services: | |
| USER_ID: 501 | ||
| container_name: claude-code-chat-explorer | ||
| restart: unless-stopped | ||
| ports: | ||
| - "127.0.0.1:9876:9876" # Localhost only | ||
| expose: | ||
| - "9876" | ||
| volumes: | ||
| # Mount at SAME absolute path so symlinks resolve correctly | ||
| - ${HOME}/.claude:${HOME}/.claude:ro | ||
|
|
@@ -23,6 +23,14 @@ services: | |
| # events through Docker Desktop's VirtioFS bridge on macOS/Windows. | ||
| - CHOKIDAR_USEPOLLING=1 | ||
| - CHOKIDAR_INTERVAL=2000 | ||
| # Native runs bind to loopback by default. Inside Docker the process | ||
| # must listen on the container interface; the published host port above | ||
| # remains restricted to 127.0.0.1. | ||
| - CHAT_EXPLORER_HOST=0.0.0.0 | ||
| - CHAT_EXPLORER_AUTH_TOKEN=${CHAT_EXPLORER_AUTH_TOKEN:-} | ||
|
|
||
| networks: | ||
| - chat-explorer-internal | ||
|
|
||
| # Security hardening | ||
| cap_drop: | ||
|
|
@@ -41,6 +49,39 @@ services: | |
| reservations: | ||
| memory: 256M | ||
|
|
||
| # This process publishes localhost but has no transcript/database mounts and | ||
| # uses only Node's core HTTP/TCP modules. The transcript-reading app above | ||
| # remains on an internal-only network with no outbound route. | ||
| chat-explorer-proxy: | ||
| build: | ||
| context: . | ||
| args: | ||
| USER_ID: 501 | ||
| container_name: claude-code-chat-explorer-proxy | ||
| restart: unless-stopped | ||
| command: ["node", "src/loopback-proxy.js"] | ||
| depends_on: | ||
| - chat-explorer | ||
| ports: | ||
| - "127.0.0.1:9876:9876" | ||
| environment: | ||
| - CHAT_EXPLORER_UPSTREAM_HOST=chat-explorer | ||
| - CHAT_EXPLORER_UPSTREAM_PORT=9876 | ||
| networks: | ||
| - chat-explorer-internal | ||
| - chat-explorer-ingress | ||
| cap_drop: | ||
| - ALL | ||
| security_opt: | ||
| - no-new-privileges:true | ||
| read_only: true | ||
| tmpfs: | ||
| - /tmp:noexec,nosuid,size=16m | ||
| deploy: | ||
| resources: | ||
| limits: | ||
| memory: 128M | ||
|
|
||
| # chat-explorer-test: | ||
| # build: | ||
| # context: . | ||
|
|
@@ -54,3 +95,8 @@ services: | |
|
|
||
| volumes: | ||
| chat-explorer-db: | ||
|
|
||
| networks: | ||
| chat-explorer-internal: | ||
| internal: true | ||
|
Comment on lines
+99
to
+101
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔍 App container is on an internal-only network with no outbound route, which disables URL-based session import
Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| chat-explorer-ingress: | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Live updates stop working in the dev container because the real-time connection is rejected
The real-time WebSocket connection is refused (
getAllowedWebSocketOriginsatsrc/chats-mobile.js:1516-1521) whenever the page is opened on a host port that differs from the container's internal port, which is exactly the case for the dev setup published on127.0.0.1:9877:9876.Impact: In the dev container, new conversations and messages never appear live; the real-time panel silently stops updating.
Origin allowlist keyed on internal port vs. published host port
The WebSocket
verifyClient(src/chats-mobile.js:1551-1562) callsauthorizeWebSocketUpgrade, which requiresinfo.originto be an exact member ofgetAllowedWebSocketOrigins(). That set is built fromthis.port, the port the app listens on inside the container (9876). The dev proxy publishes127.0.0.1:9877:9876(docker-compose.dev.yml:62-63) and forwards the browser'sOriginheader verbatim (src/loopback-proxy.js:28). A browser loadinghttp://localhost:9877sendsOrigin: http://localhost:9877, which is not in{http://localhost:9876, http://127.0.0.1:9876, http://[::1]:9876}, soisOriginAllowedreturns false and the upgrade is rejected with 403. The same breakage occurs for any user who follows the README "Change Port" instructions to remap the published port. Production works only because there the published port equals the internal port.Prompt for agents
Was this helpful? React with 👍 or 👎 to provide feedback.