Skip to content

fix(dashboard): match non-directory public auth-gate entries exactly - #78966

Open
Gearhead1175 wants to merge 1 commit into
NousResearch:mainfrom
Gearhead1175:fix/dashboard-auth-public-prefix-exact-match
Open

Gearhead1175 wants to merge 1 commit into
NousResearch:mainfrom
Gearhead1175:fix/dashboard-auth-public-prefix-exact-match

Conversation

@Gearhead1175

Copy link
Copy Markdown

What does this PR do?

_path_is_public in hermes_cli/dashboard_auth/middleware.py:77 tests path == prefix or path.startswith(prefix) against every _GATE_PUBLIC_PREFIXES entry. The startswith arm makes the == arm redundant and turns each non-directory entry into a wildcard:

public entry also exempts
/login /loginX
/favicon.ico /favicon.icox
/api/auth/providers /api/auth/providersX
/auth/logout /auth/logout-all

Any request path that merely starts with a public entry bypasses the OAuth gate.

Directory entries were never affected — the trailing slash on /assets/ already stops it matching /assetsleak/, as the list's own comment at line 46 notes. The bug is confined to entries without one.

The data already encodes the distinction, so this honours it in the matcher: prefix semantics for /-terminated entries, exact match for everything else. That mirrors how PUBLIC_API_PATHS is handled a few lines up, where the docstring calls out the identical hazard ("adding /api/status doesn't accidentally expose /api/status/secret-extension").

Impact is hardening, not a live bypass. Comparing the registered route table against the public entries, no route currently sits under one of those prefixes, so nothing sensitive is reachable on main today. This matters when someone later adds a route — or a static file under web_dist — whose name extends a public entry; the allowlist is the boundary and it should mean what it says.

Filing publicly rather than via GHSA per SECURITY.md §3.2 — no §3.1 outcome is reachable, so this is hardening, not a vulnerability report.

Related Issue

No existing issue. Searched open/closed issues and PRs for _path_is_public, GATE_PUBLIC_PREFIXES, auth gate prefix, startswith auth bypass — closest hits are #61305, #35547, #27932, which touch this file but not this matcher.

Type of Change

  • 🔒 Security fix

Changes Made

  • hermes_cli/dashboard_auth/middleware.py_path_is_public matches /-terminated entries by prefix, all others exactly; docstring updated
  • tests/hermes_cli/test_dashboard_auth_middleware.py — 23 parametrised regression cases in three directions

How to Test

Reproduce on main:

from hermes_cli.dashboard_auth.middleware import _path_is_public
_path_is_public("/loginX")        # True  ← should be False
_path_is_public("/favicon.icox")  # True  ← should be False

With this branch both return False. The five sibling cases fail on the unfixed matcher and pass with it.

scripts/run_tests.sh tests/hermes_cli/test_dashboard_auth_middleware.py -q
# 37 passed

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix
  • I've run the suite via scripts/run_tests.sh and all tests pass
  • I've added tests for my changes
  • I've tested on my platform: CachyOS (Linux 7.1.5), Python 3.11.15

Documentation & Housekeeping

  • Documentation — N/A (docstring updated in place)
  • cli-config.yaml.example — N/A (no config keys)
  • CONTRIBUTING.md / AGENTS.md — N/A (no architecture change)
  • Cross-platform impact — N/A (pure string comparison, no path/OS semantics)
  • Tool descriptions/schemas — N/A

Screenshots / Logs

Full suite via scripts/run_tests.sh -q:

this branch:  2569 files, 24632 passed, 64 failed, 339.6s
clean main:   2569 files, 24706 passed, 61 failed, 334.1s

File-level diff of the failure sets: 22 files fail on main, 24 on this branch. The two extra — tests/test_tui_gateway_server.py and tests/tools/test_transcription_tools.pypass 563/563 when run in isolation on this branch, and both failed on wall-clock timing under 32-way parallelism (reader.join(timeout=0.5)TimeoutExpired; the tui file took 74.1s loaded vs 16.1s isolated). Nothing in the baseline set passed here in the other direction.

No dashboard/auth/web_server file appears in either failure set, and test_dashboard_auth_middleware.py passed (37✓) in the full run. The pre-existing failures are provider routing, media generation, sandbox backends, and doctor — all needing external services this environment lacks. Raw totals jitter between runs because of the timing-sensitive tests above, so the file-level diff is the meaningful comparison rather than the counts.

--EnA

`_path_is_public` tested `path == prefix or path.startswith(prefix)`
against every `_GATE_PUBLIC_PREFIXES` entry. The `startswith` arm makes
the `==` arm redundant and turns each non-directory entry into a
wildcard: `/login` also exempts `/loginX`, `/favicon.ico` also exempts
`/favicon.icox`, `/api/auth/providers` also exempts
`/api/auth/providersX`. Any request path that merely starts with such an
entry bypasses the OAuth gate.

Directory entries were never affected — the trailing slash on `/assets/`
already stops it matching `/assetsleak/`, as the list's own comment
notes. The bug is confined to entries without one.

The data already encodes the distinction, so honour it in the matcher:
prefix semantics for `/`-terminated entries, exact match for everything
else. This mirrors how `PUBLIC_API_PATHS` is handled a few lines up,
where the docstring calls out the identical hazard ("adding
`/api/status` doesn't accidentally expose
`/api/status/secret-extension`").

No route currently registered under a public entry's prefix carries
sensitive data, so this is hardening rather than a live bypass — but the
allowlist is the boundary, and it should mean what it says.

Adds regression coverage in three directions: exact entries stay public,
`/`-terminated entries keep matching children, and prefixed siblings
stay gated. The five sibling cases fail on the unfixed matcher.
@alt-glitch alt-glitch added type/security Security vulnerability or hardening comp/dashboard Web dashboard / control panel UI (dashboard/, landing) area/auth Authentication, OAuth, credential pools P3 Low — cosmetic, nice to have sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data labels Aug 5, 2026
@egilewski

Copy link
Copy Markdown
Contributor

looks mergeable

The patch constrains non-directory OAuth auth-gate allowlist entries to exact paths while preserving directory-prefix behavior. Focused regression coverage passed and no source-backed security findings remain.

Review setup: I reviewed a run-owned local rebase or patch replay against current GitHub main because the submitted branch is stale or conflicted; this does not mean the submitted branch itself merges cleanly.

Security evidence:

  • trust boundary: Unauthenticated requests on an auth-required dashboard pass through the auth gate. Its public-path decision can bypass session and bearer verification, so every allowlisted entry is part of the authentication boundary.
  • source/sink/invariant: The request path and two public-path collections feed the early public return. The shared API allowlist remains exact; only entries ending in / use prefix matching, while all other entries require equality, so prefixed siblings remain gated.
  • current-main reproduction: The previous matcher treated every configured entry as a prefix, making /loginX, /favicon.icox, and /api/auth/providersX public; slash-terminated asset prefixes did not expose /assetsleak/secret.js.
  • PR-head or patch-replay validation: Patch replay against current GitHub main verified exact entries, slash-terminated descendants, exact shared API entries, and negative prefixed siblings. The focused matcher regression groups passed.
  • positive/negative cases: Positive cases include /login, /auth/callback, /favicon.ico, /assets/index.css, /fonts/Collapse-Regular.woff2, and /api/mcp/oauth/callback/abc123. Negative cases include /loginX, /favicon.icox, /auth/logout-all, /api/auth/providersX, /assetsleak/secret.js, /api/config, /sessions, and /.
  • residual bypass search: Every public prefix and its route shape was reviewed. Non-directory auth/bootstrap and favicon entries are exact; slash-terminated entries are asset or callback subtrees; the shared API allowlist remains exact. No additional changed-path bypass was found.
  • reviewer validation: Focused matcher tests passed, and the reviewed source supports the clean conclusion.

Not checked:

  • Full dashboard auth middleware suite
  • Live reverse-proxy path normalization
  • CodeRabbit review

Signed: GPT-5.6-luna-max in Codex

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/dashboard Web dashboard / control panel UI (dashboard/, landing) P3 Low — cosmetic, nice to have sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants