Skip to content

fix: harden monitoring dashboard CORS and add optional auth - #109

Merged
SHA888 merged 7 commits into
mainfrom
fix/monitoring-dashboard-auth
Jul 31, 2026
Merged

fix: harden monitoring dashboard CORS and add optional auth#109
SHA888 merged 7 commits into
mainfrom
fix/monitoring-dashboard-auth

Conversation

@SHA888

@SHA888 SHA888 commented Jul 29, 2026

Copy link
Copy Markdown
Owner

What

Hardens MonitoringDashboard CORS and adds optional bearer-token authentication.

Why

The monitoring dashboard had two security issues flagged in the whole-repo review (2026-07-22):

  1. Permissive CORS with credentials (CWE-942): setup_cors() used wildcard * origin with allow_credentials=True, meaning any website could make credentialed requests to internal operational endpoints.
  2. No authentication: All API endpoints returned internal data with zero auth — a problem if the dashboard is ever exposed beyond localhost.

Changes

  • CORS: Default origins now derive from host:port instead of *. Binding to 0.0.0.0 logs a security warning. When * is explicitly passed, allow_credentials is forced False per the CORS spec.
  • Auth: New optional auth_token parameter. When set, all /api/* and /ws requests must present Authorization: Bearer <token>. WebSocket clients can pass ?token= as a query param. The HTML dashboard page (/) is not gated.
  • 14 new unit tests covering: auth accept/reject for all endpoints, dashboard page not gated, CORS defaults, wildcard warning, 0.0.0.0 warning, and constructor parameter storage.

Addresses the P1 TODO item: "Monitoring dashboard has no authentication and permissive CORS-with-credentials".

Summary by CodeRabbit

  • New Features
    • Added optional bearer-token authentication for dashboard API routes and WebSocket clients (via Authorization: Bearer <token> and Sec-WebSocket-Protocol / ?token=).
    • Added configurable cross-origin allowed origins and tightened defaults for publicly bound addresses.
  • Bug Fixes
    • Improved CORS/auth compatibility: preflight (OPTIONS) is supported while keeping the dashboard landing page ungated.
    • Enforced safer token handling (constant-time comparison) and rejected invalid auth_token values.
  • Tests
    • Expanded unit tests for constructor validation, CORS/auth behavior, WebSocket auth flows, and non-ASCII tokens.
  • Documentation
    • Updated the security monitoring dashboard checklist to reflect completion.

- Replace wildcard '*' CORS origin with host-specific defaults
- Warn when binding to 0.0.0.0 without auth
- Add optional auth_token parameter for bearer-token auth on API/WS
- Wildcard '*' origin disables allow_credentials per CORS spec
- 14 new unit tests for auth accept/reject, CORS, and constructor

Closes the monitoring dashboard auth+CORS TODO item (P1).
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@SHA888, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 42 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a2638cb9-f330-4965-966a-a358a1c98c5f

📥 Commits

Reviewing files that changed from the base of the PR and between b763ea0 and f2b6534.

📒 Files selected for processing (3)
  • TODO.md
  • evoseal/services/monitoring_dashboard.py
  • tests/unit/services/test_monitoring_dashboard.py
📝 Walkthrough

Walkthrough

Changes

MonitoringDashboard now supports optional bearer-token authentication for API and WebSocket routes, host-derived or custom CORS origins, wildcard credential restrictions, and bind-address warnings. The dashboard client forwards tokens for WebSocket and metrics requests. Unit tests and TODO tracking cover the security changes.

Monitoring Dashboard Security

Layer / File(s) Summary
Dashboard authentication and app wiring
evoseal/services/monitoring_dashboard.py, tests/unit/services/test_monitoring_dashboard.py
Adds optional auth_token handling, authentication middleware for API and WebSocket requests, constant-time comparison, and tests for authenticated, unauthorized, ungated, and non-ASCII token behavior.
CORS configuration and authenticated browser requests
evoseal/services/monitoring_dashboard.py, tests/unit/services/test_monitoring_dashboard.py
Derives default origins from the dashboard host and port, disables credentials for wildcard origins, warns for wildcard bind hosts, and forwards tokens through WebSocket and metrics requests.
Security validation and tracking
tests/unit/services/test_monitoring_dashboard.py, TODO.md
Validates constructor, CORS, default-origin, and bind-warning behavior, and marks the monitoring-dashboard security item complete.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately reflects the main change: monitoring dashboard CORS hardening plus optional authentication.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/monitoring-dashboard-auth

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
evoseal/services/monitoring_dashboard.py (2)

48-77: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Docstring warns against combining wildcard origins with auth_token, but nothing enforces it.

Lines 53-54 state to never combine allowed_origins=["*"] with auth_token, but a caller can still do exactly that with no warning or guard. Consider logging a warning (similar to the 0.0.0.0 and wildcard-CORS warnings already added) when both are set together, so the documented risk is actually surfaced at runtime.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@evoseal/services/monitoring_dashboard.py` around lines 48 - 77, Add a runtime
warning in the initializer after assigning allowed_origins when both auth_token
is set and allowed_origins explicitly contains "*". Use the existing logger
warning pattern, clearly identify the insecure wildcard-CORS/auth combination,
and leave other origin configurations unchanged.

99-113: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Use constant-time comparison for token checks.

auth_header == f"Bearer {self.auth_token}" and token != self.auth_token compare request-controlled values against a secret with standard Python equality/inequality, which can leak matching-prefix information through timing; use secrets.compare_digest here.

🔒 Proposed fix
+import secrets
+
 `@web.middleware`
 async def _auth_middleware(self, request: web.Request, handler):
     """Reject unauthenticated API/WebSocket requests when auth_token is set."""
     if self.auth_token and request.path != "/":
         auth_header = request.headers.get("Authorization", "")
-        if auth_header == f"Bearer {self.auth_token}":
+        if secrets.compare_digest(auth_header, f"Bearer {self.auth_token}"):
             pass
         elif request.path == "/ws":
             # WebSocket clients can pass ?token= as query param
             token = request.query.get("token", "")
-            if token != self.auth_token:
+            if not secrets.compare_digest(token, self.auth_token):
                 return web.json_response({"error": "Unauthorized"}, status=401)
         else:
             return web.json_response({"error": "Unauthorized"}, status=401)
     return await handler(request)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@evoseal/services/monitoring_dashboard.py` around lines 99 - 113, Update
_auth_middleware to use secrets.compare_digest for both the Authorization bearer
token and WebSocket query token comparisons against self.auth_token, preserving
the existing unauthorized responses and request-path behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@evoseal/services/monitoring_dashboard.py`:
- Around line 62-73: Update the security warning condition in the dashboard
setup to trigger for both wildcard bind addresses, "0.0.0.0" and "::". Keep the
existing warning message and default CORS origin handling unchanged.

---

Nitpick comments:
In `@evoseal/services/monitoring_dashboard.py`:
- Around line 48-77: Add a runtime warning in the initializer after assigning
allowed_origins when both auth_token is set and allowed_origins explicitly
contains "*". Use the existing logger warning pattern, clearly identify the
insecure wildcard-CORS/auth combination, and leave other origin configurations
unchanged.
- Around line 99-113: Update _auth_middleware to use secrets.compare_digest for
both the Authorization bearer token and WebSocket query token comparisons
against self.auth_token, preserving the existing unauthorized responses and
request-path behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 73db3fa7-8807-4991-abfe-40060a43528f

📥 Commits

Reviewing files that changed from the base of the PR and between 85e92c2 and d192a56.

📒 Files selected for processing (3)
  • TODO.md
  • evoseal/services/monitoring_dashboard.py
  • tests/unit/services/test_monitoring_dashboard.py

Comment thread evoseal/services/monitoring_dashboard.py Outdated
- Use hmac.compare_digest for constant-time token comparison (prevents
  timing attacks on bearer-token and WS query-token checks)
- Exempt OPTIONS requests from auth so CORS preflight works when
  auth_token is set
- Warn at runtime when allowed_origins=['*'] is combined with auth_token
  (documented guard rail now enforced)
- Extend bind-address warning to include '::' wildcard alongside 0.0.0.0
@SHA888

SHA888 commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

Addressed the review feedback. Here is the point-by-point verdict and changes:

1. Constant-time token comparison ✅ Fixed

  • auth_header == f"Bearer {self.auth_token}" and token != self.auth_token replaced with hmac.compare_digest in both places. Added import hmac.

2. OPTIONS preflight not exempted ✅ Fixed

  • Added if request.method == "OPTIONS": return await handler(request) early in _auth_middleware, before the auth check. CORS preflight requests now pass through to aiohttp_cors.

3. allowed_origins=["*"] + auth_token combo not enforced ✅ Fixed

  • Added a runtime logger.warning in __init__ when both are set, matching the existing warning pattern. The docstring guard rail is now surfaced at runtime.

4. logger import/definition ❌ Not a bug

  • logger = logging.getLogger(__name__) is already defined at module scope (line 19). No change needed.

Verification: ruff format --check ✅, ruff check ✅, pytest tests/unit/services/test_monitoring_dashboard.py — 17 passed (including 3 new tests for OPTIONS passthrough, :: bind warning, and wildcard+auth combo warning).

…p:// default

Address review feedback on PR #109:
- Change the  check from a warning
  to a ValueError, so operators cannot silently end up with credentialed
  cross-origin access wide open.
- Document that default allowed_origins use http:// and that explicit
  origins are needed when behind TLS termination or alternate hostnames.
- Update test to expect ValueError instead of warning.

Points assessed but not changed:
- WebSocket ?token= query param: standard pattern since browsers cannot
  set custom headers on WebSocket connections; no change needed.
- Dashboard page auth_token leakage: _generate_dashboard_html() never
  interpolates the token into HTML/JS; not a real issue.
@SHA888

SHA888 commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

Addressed review feedback (commit 0c20042):

Changed:

  1. ** + now raises ** instead of only logging a warning. Operators cannot silently end up with credentialed cross-origin access wide open. Updated test accordingly.

  2. Documented default limitation in the docstring — notes that explicit origins are needed when behind TLS termination or accessed via a different hostname than the bind host.

Assessed but not changed:
3. WebSocket ?token= query param — browsers cannot set custom headers on WebSocket connections (the WebSocket API only accepts a URL + optional protocols). The query param approach is the standard pattern. WS subprotocol injection is possible but adds complexity and still ends up in the URL. No change needed.

  1. Dashboard page leaking auth_token into embedded JS — verified that _generate_dashboard_html() never interpolates the token into HTML or JavaScript. The JS client opens a WebSocket to /ws with no token and does fetch('/api/metrics') with no auth header. The token is not leaked. (Note: this means the JS fallback fetch will get 401'd when auth is enabled — a functional limitation, not a security issue.)

- WebSocket auth now prefers Sec-WebSocket-Protocol header over query
  string to avoid leaking the bearer token in access/proxy logs.
  The HTML JS client sends the token via the protocol subprotocol
  and falls back to Authorization header for the HTTP API polling.
  The ?token= query param is still accepted as a fallback but
  documented as less secure.

- hmac.compare_digest replaced with _constant_compare that encodes
  both operands to UTF-8 bytes, preventing TypeError on non-ASCII
  tokens or malformed Authorization headers.

- Removed bare http://localhost (no port) from default CORS origins
  for 0.0.0.0/:: binds — it only matches port 80 and is dead config
  for typical non-80 dashboard ports.

- Added tests: WS auth via Sec-WebSocket-Protocol (accept + reject),
  non-ASCII token round-trip, _constant_compare unit test, and
  assertion that bare http://localhost is excluded from defaults.

Point 2 (wildcard bind warning vs error): left as a warning. Raising
would break legitimate behind-firewall deployments; the warning is
clear and actionable.

Point 5 (CORS per-origin defaults): verified aiohttp_cors behaves
identically with per-origin keys vs '*' for cors.add(route). No
change needed.
@SHA888

SHA888 commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

Addressed the review feedback. Here's my assessment of each point and what changed:

1. WebSocket token in query string — ✅ Fixed

The _auth_middleware now prefers Sec-WebSocket-Protocol header over the ?token= query param. The header value is parsed as a comma-separated list of subprotocols; entries starting with Bearer are treated as the auth token. The ?token= fallback is still accepted (for clients that can't set WS headers) but is documented as less secure since it leaks into access/proxy logs.

The embedded JavaScript client now reads the token from the page URL query param (/?token=xxx) and passes it via the Sec-WebSocket-Protocol header, so the token never appears in server WebSocket access logs. The HTTP API polling also sends the token via Authorization header.

2. Wildcard bind only logs warning — ⏭️ Intentionally unchanged

Raising an error on 0.0.0.0/:: would break legitimate behind-firewall deployments. The warning is clear and actionable ("Ensure auth_token is set or restrict access via firewall"). The existing test test_bind_all_warns confirms this is the intended behavior.

3. hmac.compare_digest non-ASCII TypeError — ✅ Fixed

Added _constant_compare(a, b) static method that encodes both operands to UTF-8 bytes before calling hmac.compare_digest, then catches UnicodeEncodeError/AttributeError and returns False instead of letting a TypeError bubble into a 500. Used for all auth comparisons (Authorization header, WS protocol token, query param token). New tests: test_non_ascii_token_does_not_500 (round-trip with über-sécret token) and test_constant_compare_non_ascii.

4. CORS bare http://localhost without port — ✅ Fixed

Removed the bare http://localhost entry from the default origins for 0.0.0.0/:: binds. Only http://localhost:<port> is kept. Updated test assertion confirms the bare entry is excluded.

5. CORS per-origin defaults behavior — ✅ Verified, no change needed

Confirmed aiohttp_cors.setup(defaults={origin: opts for origin in origins}) works identically to defaults={"*": opts} for cors.add(route) calls — each route gets CORS handling for each origin in the mapping. The OPTIONS preflight is handled by aiohttp_cors middleware regardless of whether defaults uses * or specific origins. The library's own docs show per-origin defaults as the standard pattern.

…y token

- Replace 'Bearer <token>' subprotocol with 'bearer.<token>' in JS client
  (spaces are invalid in Sec-WebSocket-Protocol per RFC 6455 token grammar;
  browsers threw SyntaxError before onerror/onclose attached, silently
  killing all live WebSocket updates when auth_token was set)
- Wrap WebSocket constructor in try/catch so malformed subprotocol doesn't
  hard-fail silently
- Update _auth_middleware to parse 'bearer.<token>' instead of 'Bearer <token>'
- Validate auth_token is not empty string in constructor (previously falsy
  check silently disabled auth for auth_token='')
- Update tests to use valid 'bearer.<token>' format
- Add test for legacy 'Bearer ' format rejection
- Add test for empty auth_token ValueError

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@evoseal/services/monitoring_dashboard.py`:
- Around line 635-644: Update the token-loading logic near the WebSocket setup
to read browser bearer tokens from location.hash rather than the query string,
parse the fragment’s token value, and remove it with history.replaceState after
reading. Preserve ?token= only for the documented non-browser WebSocket
fallback, and add a regression check asserting the generated client uses
location.hash instead of location.search.
- Around line 644-648: Update the browser WebSocket authentication flow around
the WebSocket constructor to encode auth_token as an ASCII-safe base64url
representation of its UTF-8 bytes before placing it in the bearer subprotocol.
Update the server-side WebSocket authentication handling to base64url-decode and
UTF-8-decode that value before constant-time comparison with auth_token,
preserving existing behavior for valid ASCII tokens. Add a regression test
covering a non-ASCII credential and successful WebSocket
connection/reconnection.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9a1ae841-2a10-48c5-be7c-5b64e3a17ba4

📥 Commits

Reviewing files that changed from the base of the PR and between 0c20042 and b763ea0.

📒 Files selected for processing (2)
  • evoseal/services/monitoring_dashboard.py
  • tests/unit/services/test_monitoring_dashboard.py

Comment on lines +635 to +644
// Read token from page URL query param (set by the operator).
// The token is sent via Sec-WebSocket-Protocol header so it does
// not appear in server/proxy access logs.
if (!authToken) {
const params = new URLSearchParams(window.location.search);
authToken = params.get('token');
}

try {
ws = new WebSocket(wsUrl, authToken ? ['bearer.' + authToken] : undefined);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not load the bearer token from the page query string.

Opening /?token=... sends the secret in the initial dashboard request and access logs, undermining the intended URL-safe WebSocket flow. Read it from #token=... instead, then remove the fragment with history.replaceState; retain ?token= only as the documented non-browser WebSocket fallback. Add a regression check that the generated client reads location.hash, not location.search. HTTP fragments are not sent in requests, and are stripped from Referer values. (w3.org)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@evoseal/services/monitoring_dashboard.py` around lines 635 - 644, Update the
token-loading logic near the WebSocket setup to read browser bearer tokens from
location.hash rather than the query string, parse the fragment’s token value,
and remove it with history.replaceState after reading. Preserve ?token= only for
the documented non-browser WebSocket fallback, and add a regression check
asserting the generated client uses location.hash instead of location.search.

Source: Coding guidelines

Comment on lines +644 to +648
ws = new WebSocket(wsUrl, authToken ? ['bearer.' + authToken] : undefined);
} catch (e) {
console.error('WebSocket constructor failed (bad subprotocol?):', e);
addLogEntry('WebSocket setup error: ' + e.message, 'error');
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C2 'new WebSocket|location\.(search|hash)|bearer\.' \
  evoseal/services/monitoring_dashboard.py
rg -n -C2 'non_ascii.*ws|ws_auth.*ascii|bearer\.' \
  tests/unit/services/test_monitoring_dashboard.py

Repository: SHA888/EVOSEAL

Length of output: 1720


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== monitoring dashboard outline/size =="
wc -l evoseal/services/monitoring_dashboard.py
ast-grep outline evoseal/services/monitoring_dashboard.py --depth 3 --view expanded | sed -n '1,220p' || true

echo "== relevant monitoring dashboard sections =="
sed -n '1,220p' evoseal/services/monitoring_dashboard.py
sed -n '580,670p' evoseal/services/monitoring_dashboard.py

echo "== test monitoring dashboard relevant sections =="
sed -n '130,210p' tests/unit/services/test_monitoring_dashboard.py

echo "== occurrences of auth_token/authToken/url token config =="
rg -n -C2 "auth_token|authToken|SEAL_AUTH_TOKEN|SECRETCODE|bearer\\.\\+|new WebSocket|Sec-WebSocket-Protocol|subprotocol|sec_websocket_token" evoseal tests

Repository: SHA888/EVOSEAL

Length of output: 35871


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import base64
import urllib.parse
import hmac

non_ascii = "über-sécret"
plain = "bearer." + non_ascii
b64 = base64.urlsafe_b64encode(non_ascii.encode("utf-8")).decode("ascii").rstrip("=")
proto_b64encoded_token = "bearer." + b64
decode_b64_token = base64.urlsafe_b64decode(proto_b64encoded_token[len("bearer."):] + b"==").decode("utf-8")
assert hmac.compare_digest(decode_b64_token.encode("utf-8"), non_ascii.encode("utf-8"))
print("plain_proto", plain)
print("base64url_proto", proto_b64encoded_token)
print("decoded_correct", decode_b64_token == non_ascii)
PY

sed -n '210,240p' tests/unit/services/test_monitoring_dashboard.py

Repository: SHA888/EVOSEAL

Length of output: 282


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import base64
import hmac

non_ascii = "über-sécret"
plain = "bearer." + non_ascii
b64 = base64.urlsafe_b64encode(non_ascii.encode("utf-8")).decode("ascii").rstrip("=")
proto_b64encoded_token = "bearer." + b64
decode_b64_token = base64.urlsafe_b64decode(proto_b64encoded_token[len("bearer."):] + b"==").decode("utf-8")
assert hmac.compare_digest(decode_b64_token.encode("utf-8"), non_ascii.encode("utf-8"))
print("plain_proto", plain)
print("base64url_proto", proto_b64encoded_token)
print("decoded_correct", decode_b64_token == non_ascii)
PY

sed -n '210,242p' tests/unit/services/test_monitoring_dashboard.py

Repository: SHA888/EVOSEAL

Length of output: 282


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 <<'PY'
from __future__ import annotations
import base64
import hmac

non_ascii = "über-sécret"
plain = "bearer." + non_ascii
b64 = base64.urlsafe_b64encode(non_ascii.encode("utf-8")).decode("ascii").rstrip("=")
proto_b64encoded_token = "bearer." + b64
encoded = proto_b64encoded_token[len("bearer."):]
padding_needed = (4 - len(encoded) % 4) % 4
decode_b64_token = base64.urlsafe_b64decode(encoded + "=" * padding_needed).decode("utf-8")
assert hmac.compare_digest(decode_b64_token.encode("utf-8"), non_ascii.encode("utf-8"))
print("plain_proto", repr(plain))
print("plain_proto_ascii", plain.isascii())
print("base64url_proto", repr(proto_b64encoded_token))
print("base64url_proto_ascii", proto_b64encoded_token.isascii())
print("decoded_correct", decode_b64_token == non_ascii)
PY

sed -n '210,242p' tests/unit/services/test_monitoring_dashboard.py

Repository: SHA888/EVOSEAL

Length of output: 1436


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 <<'PY'
import io
import http.client
from urllib.parse import urlencode
from aiohttp import web
import asyncio

loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
app = web.Application()
routes = web.RouteTableDef()

`@routes.get`("/")
async def dashboard_page(request):
    return web.Response(text='<html></html>', content_type="text/html")

`@routes.get`("/ws")
async def websocket_handler(request):
    ws = web.WebSocketResponse()
    try:
        await ws.prepare(request)
        await ws.close()
    except Exception as exc:
        return web.Response(text=f"WebSocket prepare raised {type(exc).__name__}: {exc}")
    return ws

app.router.add_routes(routes)

runner = web.WebAppRunner(app, port=18999)

`@asyncio.coroutine`
def run():
    yield from runner.setup()
    yield from startup_site()
    yield from asyncio.sleep(0.1)

asyncio.set_event_loop(loop)
loop.run_until_complete(run())

Repository: SHA888/EVOSEAL

Length of output: 359


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 <<'PY'
from pathlib import Path
from urllib.parse import urlparse

dashboard = Path("evoseal/services/monitoring_dashboard.py").read_text()
search = Path("tests/").glob("*/services/test_monitoring_dashboard.py")
files = next(iter(Path("tests").rglob("*test_monitoring_dashboard.py")), None)

print("dashboard_path_exists", Path("evoseal/services/monitoring_dashboard.py").exists())
print("dashboard_path", Path("evoseal/services/monitoring_dashboard.py").absolute())
print("dashboard_contains_new_websocket", "new WebSocket(wsUrl, authToken ? ['bearer.' + authToken] : undefined)" in dashboard)
print("dashboard_contains_restricted_ws_auth", "--restricted-ws-auth" in dashboard)
print("dashboard_route_ws", "self.app.router.add_get(\"/ws\", self.websocket_handler)" in dashboard)
print("server_authorization_header", "Bearer {self.auth_token}" in dashboard)
print("server_auth_from_protocol", "token_from_protocol" in dashboard)
print("test_file", files)
if files:
    txt = files.read_text()
    print("test_contains_ws_non_ascii", "test_ws_auth_via_sec_protocol_header" in txt and "auth_token='über-sécret'" in txt)
    print("test_contains_non_ascii_ws_subprotocol", "test_non_ascii_token_does_not_500" in txt and "Sec-WebSocket-Protocol" in txt and "über-sécret" in txt)
PY

sed -n '305,365p' tests/unit/services/test_monitoring_dashboard.py 2>/dev/null || true

Repository: SHA888/EVOSEAL

Length of output: 2235


Encode browser WebSocket credentials into a valid subprotocol token.

auth_token supports non-ASCII values, but bearer. + über-sécret is not a valid RFC 6455 WebSocket subprotocol value; the browser constructor throws, this catch returns without reconnecting, and real-time updates fail on non-ASCII credentials. Encode auth tokens as an ASCII-only transport such as base64url-encoded UTF-8, decode server-side before constant-time comparison, and add a non-ASCII browser WebSocket regression test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@evoseal/services/monitoring_dashboard.py` around lines 644 - 648, Update the
browser WebSocket authentication flow around the WebSocket constructor to encode
auth_token as an ASCII-safe base64url representation of its UTF-8 bytes before
placing it in the bearer subprotocol. Update the server-side WebSocket
authentication handling to base64url-decode and UTF-8-decode that value before
constant-time comparison with auth_token, preserving existing behavior for valid
ASCII tokens. Add a regression test covering a non-ASCII credential and
successful WebSocket connection/reconnection.

Source: Coding guidelines

- Bracket IPv6 literals in default CORS origin (http://[::1]:8081)
- Compute allow_credentials per-origin (wildcard=False, explicit=True)
- Echo accepted bearer subprotocol in WebSocket handler (RFC 6455 §4.2.2)
- Validate auth_token charset against HTTP token grammar (RFC 7230 §3.2.6)
- Update non-ASCII token test to expect ValueError (also invalid tchar)
- Add tests for IPv6 origins, per-origin CORS, WS subprotocol echo, token charset
@SHA888

SHA888 commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

Addressed all four review points:

1. IPv6 default-origin bracketing — Added helper using . Now produces instead of the invalid . Tests: , , .

**2. Per-origin ** — Replaced the single shared with per-origin computation in . Wildcard gets ; explicit origins keep . Test: .

3. WebSocket subprotocol echo — now reads the header, extracts the subprotocol, and passes it to so browsers complete the handshake per RFC 6455 §4.2.2. Tests: , .

4. Token charset validation — Added regex check (RFC 7230 §3.2.6 tchar) in . Tokens containing , =, whitespace, or non-ASCII characters now raise at construction time, preventing silent fallback to the leaky query-param path. Existing updated to (non-ASCII is also invalid tchar). New tests: , , .

All 32 tests pass. Lint clean.

@SHA888

SHA888 commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

Addressed all four review points:

1. IPv6 default-origin bracketing — Added _bracket_ipv6() helper using ipaddress.ip_address(). Now host="::1" produces http://[::1]:8081 instead of the invalid http://::1:8081. Tests: test_ipv6_loopback_origin_is_bracketed, test_ipv6_custom_origin_is_bracketed, test_ipv4_origin_not_double_bracketed.

2. Per-origin allow_credentials — Replaced the single shared default_opts with per-origin computation in setup_cors(). Wildcard * gets allow_credentials=False; explicit origins keep allow_credentials=True. Test: test_mixed_wildcard_and_explicit_origins_credentials_per_origin.

3. WebSocket subprotocol echowebsocket_handler now reads the Sec-WebSocket-Protocol header, extracts the bearer.<token> subprotocol, and passes it to WebSocketResponse(protocols=...) so browsers complete the handshake per RFC 6455 S4.2.2. Tests: test_ws_echoes_bearer_subprotocol, test_ws_no_subprotocol_when_no_auth.

4. Token charset validation — Added _is_http_token() regex check (RFC 7230 S3.2.6 tchar) in __init__. Tokens containing /, =, whitespace, or non-ASCII characters now raise ValueError at construction time, preventing silent fallback to the leaky query-param path. Existing test_non_ascii_token_does_not_500 updated to test_non_ascii_token_rejected_at_init (non-ASCII is also invalid tchar). New tests: test_invalid_auth_token_with_slash_raises, test_invalid_auth_token_with_space_raises, test_valid_urlsafe_token_accepted.

All 32 tests pass. Lint clean.

# Conflicts:
#	TODO.md
@SHA888
SHA888 force-pushed the fix/monitoring-dashboard-auth branch from 21ebe0a to f2b6534 Compare July 31, 2026 06:33
@SHA888
SHA888 merged commit 5aa70f1 into main Jul 31, 2026
9 checks passed
@SHA888
SHA888 deleted the fix/monitoring-dashboard-auth branch July 31, 2026 06:38
SHA888 added a commit that referenced this pull request Jul 31, 2026
Rewrite docs/api/index.md to document the real public API surface:
- CLI commands (init, config, pipeline, start, doctor, estimate-cost, etc.)
- Core Python API (EvolutionPipeline, EvolutionConfig)
- Safety layer (SafetyIntegration, CheckpointManager, EditScopeValidator, etc.)
- Phase 3 services (ContinuousEvolutionService, TrainingManager)
- Monitoring dashboard quick reference with auth/CORS info

The previous version documented fabricated classes (EVOSEAL.evolve(),
OpenAIModel, AnthropicModel) that don't exist in the codebase.

Also fix stale CORS and auth info in docs/API_REFERENCE.md:
- Auth: document optional bearer token (added in PR #109)
- CORS: origins now default to host:port, not wildcard *

Addresses TODO.md item: Improve API reference
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