Skip to content

dashboard server: hardening + better default UX#4

Open
ThinkOffApp wants to merge 1 commit intomainfrom
feat/dashboard-server-hardening
Open

dashboard server: hardening + better default UX#4
ThinkOffApp wants to merge 1 commit intomainfrom
feat/dashboard-server-hardening

Conversation

@ThinkOffApp
Copy link
Copy Markdown
Owner

Summary

Four small improvements to tools/su2_dashboard_server.py based on a review of the live setup:

  1. Block directory listings + dev-artefact paths. Default Python http.server exposes the repo root: hitting http://localhost:8001/ previously rendered the contents of .git/, .venv/, build/, etc. New: list_directory() returns 404, and a denylist in send_head() (shared by GET and HEAD) refuses any path under .git/, .venv/, build/, __pycache__/, dotfile-prefixed components, and .. traversals. Verified with curl: /.git/config and /build/source.sh now 404; /tools/su2_dashboard.html still 200.
  2. Redirect / to /dashboard so a bare host:port URL lands on the dashboard instead of 404'ing.
  3. Always-on hardening headers: X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: no-referrer on every response. Conservative set; the vanilla-JS dashboard uses no inline eval / no remote scripts so unaffected. Useful if the bind ever moves off 127.0.0.1.
  4. Startup readability check on --root. exists() / is_dir() both pass on a symlink whose target is gone (e.g. results/ pointing at an unmounted external drive). Now also calls os.scandir(root) so the operator sees an immediate startup failure rather than silent empty data later.

No behaviour change for the dashboard's API endpoints (/events, /chat, /thread_control, /thread_telemetry, /results/* JSON serving). Auth / CORS handling unchanged.

Cleanup also included

Grid/eigen-3.4.0.tar.bz2 and Grid/eigen-3.4.0.tar.bz2.1 were committed accidentally (4MB total). bootstrap.sh downloads them fresh on every run, so removing them from git is uncontroversial. Happy to split into a separate commit / PR if preferred.

Test plan

  • curl -sSI http://127.0.0.1:8001/.git/config → 404
  • curl -sSI http://127.0.0.1:8001/build/source.sh → 404
  • curl -sSI http://127.0.0.1:8001/tools/su2_dashboard.html → 200
  • curl -s http://127.0.0.1:8001/ → 302 to /dashboard
  • Response headers include X-Content-Type-Options: nosniff etc.
  • Startup with --root pointing at a dangling symlink (T705 mount removed) → fails fast with Root is not readable.
  • EventSource (/events) still streams — should be unaffected since SSE was never gated by these checks.

🤖 Generated with Claude Code

Three small improvements to tools/su2_dashboard_server.py based on the
review of the live setup:

1. Block directory listings + dev-artefact paths

   Default Python http.server exposes the repo root: hitting
   http://localhost:8001/ used to print the contents of .git/, .venv/,
   build/, etc. Override list_directory() to refuse, and add a denylist
   in send_head() (shared by GET and HEAD) that rejects any path under
   .git/, .venv/, build/, __pycache__/, dotfile-prefixed components, and
   .. traversals. The dashboard only legitimately needs paths under
   tools/ and results/, both still allowed.

2. Redirect / -> /dashboard

   Pasting a bare host:port URL now lands on the dashboard instead of
   404'ing on the disabled listing.

3. Always-on hardening headers

   X-Content-Type-Options: nosniff, X-Frame-Options: DENY,
   Referrer-Policy: no-referrer applied to every response. Conservative
   set; the vanilla-JS dashboard uses no inline eval / no remote
   scripts so it is unaffected. Useful if the bind ever moves off
   127.0.0.1.

4. Startup readability check on --root

   exists() / is_dir() can both pass on a symlink whose target is gone
   (e.g. results/ pointing at an unmounted external drive). Try a real
   os.scandir read so the operator gets an immediate startup failure
   instead of silent empty data later.

No behaviour change for the dashboard's actual API endpoints
(/events, /chat, /thread_control, /thread_telemetry, /results/* JSON
serving). All authentication / CORS handling unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cd45c56118

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +103 to +106
def _path_is_denied(self, path):
if path.startswith("/"):
path = path[1:]
if not path:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Decode URL paths before denylist checks

The new denylist is bypassable because _path_is_denied evaluates parsed.path without URL-decoding it first. A request such as /%2egit/config (or encoded .. segments) will pass this check, but SimpleHTTPRequestHandler later decodes/normalizes the path in file resolution, so .git and traversal-like targets can still be reached despite the hardening logic. Normalize and decode the path before checking components/prefixes so encoded dotfiles and dot-segments are rejected consistently.

Useful? React with 👍 / 👎.

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.

1 participant