# Task list generated from PRD: .plans/2026/03/09/01-security-fixes/prd.md # Generated by: /jp-task-list tasks: - id: "TASK-001" title: "Remove legacy HTML dashboard endpoint" description: | Remove the `dashboard()` route handler from `src/meshcore_hub/api/routes/dashboard.py` (lines ~367-536). This handler renders a standalone HTML page using f-string HTML with unescaped database content (stored XSS) and has no authentication. The JSON sub-routes (`/stats`, `/activity`, `/message-activity`, `/node-count`) must remain intact and unchanged. Specifically: 1. Delete the `dashboard()` async function and its `@router.get("")` decorator (the handler that returns HTMLResponse). 2. Remove the `HTMLResponse` import from `fastapi.responses` if it is no longer used by any remaining route. 3. Verify that `GET /api/v1/dashboard/stats`, `/activity`, `/message-activity`, and `/node-count` still function. requirements: - "REQ-001" - "REQ-006" dependencies: [] suggested_role: "python" acceptance_criteria: - "The `dashboard()` route handler is removed from `api/routes/dashboard.py`" - "`HTMLResponse` import is removed if no longer used" - "`GET /api/v1/dashboard/` returns 404 or 405" - "`GET /api/v1/dashboard/stats` returns valid JSON with authentication" - "`GET /api/v1/dashboard/activity` returns valid JSON with authentication" - "`GET /api/v1/dashboard/message-activity` returns valid JSON with authentication" - "`GET /api/v1/dashboard/node-count` returns valid JSON with authentication" estimated_complexity: "small" files_affected: - "src/meshcore_hub/api/routes/dashboard.py" - id: "TASK-002" title: "Replace API key comparisons with constant-time comparison" description: | Replace all Python `==` comparisons of API keys and credentials with `hmac.compare_digest()` to prevent timing side-channel attacks. In `src/meshcore_hub/api/auth.py`: 1. Add `import hmac` at the top of the file. 2. Line ~82 in `require_read`: replace `if token == read_key or token == admin_key:` with `if hmac.compare_digest(token, read_key) or hmac.compare_digest(token, admin_key):`. 3. Line ~127 in `require_admin`: replace `if token == admin_key:` with `if hmac.compare_digest(token, admin_key):`. In `src/meshcore_hub/api/metrics.py`: 1. Add `import hmac` at the top of the file. 2. Line ~57: replace `return username == "metrics" and password == read_key` with `return hmac.compare_digest(username, "metrics") and hmac.compare_digest(password, read_key)`. Note: `hmac.compare_digest()` requires both arguments to be strings (or both bytes). The existing code already works with strings, so no type conversion is needed. requirements: - "REQ-002" - "REQ-007" dependencies: [] suggested_role: "python" acceptance_criteria: - "All API key comparisons in `api/auth.py` use `hmac.compare_digest()`" - "All credential comparisons in `api/metrics.py` use `hmac.compare_digest()`" - "`hmac` is imported in both files" - "Valid API keys are accepted and invalid keys are rejected (no behavior change)" estimated_complexity: "small" files_affected: - "src/meshcore_hub/api/auth.py" - "src/meshcore_hub/api/metrics.py" - id: "TASK-003" title: "Add WEB_TRUSTED_PROXY_HOSTS configuration setting" description: | Add a `web_trusted_proxy_hosts` field to the web settings in `src/meshcore_hub/common/config.py`. 1. In the `WebSettings` class (or the relevant settings class containing web config), add: ```python web_trusted_proxy_hosts: str = Field(default="*", description="Comma-separated list of trusted proxy hosts or '*' for all") ``` 2. The field should accept a string value. The `ProxyHeadersMiddleware` in uvicorn accepts either `"*"` or a list of strings. If the value is `"*"`, pass it directly. Otherwise, split on commas and strip whitespace to produce a list. This task only adds the configuration field. The middleware integration and startup warning are in TASK-004. requirements: - "REQ-003" - "REQ-006" dependencies: [] suggested_role: "python" acceptance_criteria: - "A `web_trusted_proxy_hosts` setting exists in the configuration with default value `*`" - "The setting can be configured via the `WEB_TRUSTED_PROXY_HOSTS` environment variable" - "The setting accepts `*` or a comma-separated list of hostnames/IPs" estimated_complexity: "small" files_affected: - "src/meshcore_hub/common/config.py" - id: "TASK-004" title: "Integrate trusted proxy hosts into web app middleware and add startup warning" description: | Update `src/meshcore_hub/web/app.py` to use the new `WEB_TRUSTED_PROXY_HOSTS` setting and emit a startup warning when using the insecure default. 1. Find the `ProxyHeadersMiddleware` addition (line ~239): ```python app.add_middleware(ProxyHeadersMiddleware, trusted_hosts="*") ``` Replace the hardcoded `"*"` with the configured value. If the config value is `"*"`, pass `"*"`. Otherwise, split the comma-separated string into a list of strings. 2. Add a startup warning (in the app factory or lifespan) when `WEB_ADMIN_ENABLED=true` and `WEB_TRUSTED_PROXY_HOSTS` is `"*"`: ```python import logging logger = logging.getLogger(__name__) if settings.web_admin_enabled and settings.web_trusted_proxy_hosts == "*": logger.warning( "WEB_ADMIN_ENABLED is true but WEB_TRUSTED_PROXY_HOSTS is '*' (trust all). " "Consider restricting to your reverse proxy IP for production deployments." ) ``` 3. Verify that the `_is_authenticated_proxy_request` function still accepts `X-Forwarded-User`, `X-Auth-Request-User`, and `Authorization: Basic` headers — do not modify that function. requirements: - "REQ-003" - "REQ-006" - "REQ-007" dependencies: - "TASK-003" suggested_role: "python" acceptance_criteria: - "`ProxyHeadersMiddleware` uses the configured `trusted_hosts` value instead of hardcoded `*`" - "A warning is logged at startup when admin is enabled and trusted hosts is `*`" - "The warning recommends restricting trusted hosts to the proxy IP" - "`_is_authenticated_proxy_request` still accepts all three header types" - "Setting `WEB_TRUSTED_PROXY_HOSTS` to a specific IP restricts proxy header trust" estimated_complexity: "medium" files_affected: - "src/meshcore_hub/web/app.py" - id: "TASK-005" title: "Escape config JSON in template script block to prevent XSS breakout" description: | Prevent XSS via `` breakout in the config JSON template injection in `src/meshcore_hub/web/app.py`. In the `_build_config_json` function (or wherever `config_json` is prepared for the template, around line 183), after calling `json.dumps(config)`, escape `` sequences: ```python config_json = json.dumps(config).replace("", "<\\/") ``` This prevents a config value containing `` from breaking out of the `` does not execute JavaScript" - "The SPA application correctly parses the escaped config JSON" - "Normal config values without special characters render unchanged" estimated_complexity: "small" files_affected: - "src/meshcore_hub/web/app.py" - id: "TASK-006" title: "Fix stored XSS in admin node-tags page" description: | Sanitize API-sourced data in `src/meshcore_hub/web/static/js/spa/pages/admin/node-tags.js` to prevent stored XSS. Three locations need fixing: 1. **Line ~243** — `unsafeHTML()` with nodeName in copy-all confirmation: ```javascript
${unsafeHTML(t('common.copy_all_entity_description', { count: tags.length, entity: t('entities.tags').toLowerCase(), name: nodeName }))}
``` Replace `unsafeHTML()` with safe rendering. Either escape `nodeName` with `escapeHtml()` before passing to `t()`, or use `textContent`-based rendering. 2. **Line ~272** — `unsafeHTML()` with nodeName in delete-all confirmation: ```javascript${unsafeHTML(t('common.delete_all_entity_confirm', { count: tags.length, entity: t('entities.tags').toLowerCase(), name: nodeName }))}
``` Same fix as above. 3. **Line ~454** — `innerHTML` with tag key in delete confirmation: ```javascript container.querySelector('#delete_tag_confirm_message').innerHTML = confirmMsg; ``` where `confirmMsg` is built with `activeTagKey` interpolated into an HTML span. Replace `innerHTML` with `textContent`, or escape `activeTagKey` with `escapeHtml()` before interpolation. Import `escapeHtml` from `../components.js` if not already imported. The function escapes `<`, `>`, `&`, `"`, and `'` characters using DOM textContent. requirements: - "REQ-005" - "REQ-006" dependencies: [] suggested_role: "frontend" acceptance_criteria: - "Node names in node-tags.js are escaped before HTML rendering" - "Tag keys in node-tags.js are escaped before HTML rendering" - "All `unsafeHTML()` calls on API-sourced data are replaced with safe alternatives" - "All `innerHTML` assignments of API-sourced data are replaced with safe alternatives" - "A node name containing `