Skip to content

fix(security): code-audit findings — container escape, event-loop stalls, authz, error surfacing#98

Merged
GeiserX merged 3 commits into
mainfrom
fix/web-audit-security-correctness
Jul 10, 2026
Merged

fix(security): code-audit findings — container escape, event-loop stalls, authz, error surfacing#98
GeiserX merged 3 commits into
mainfrom
fix/web-audit-security-correctness

Conversation

@GeiserX

@GeiserX GeiserX commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Fixes from a full-codebase 8-lens /review-code! audit (architecture, security, complexity, dead-code, error-handling, test-health, consistency, performance). Overall health 7/10 — strong bones (clean collector ABC, uniform DB layer, Fernet+bcrypt, DNS-rebinding-aware SSRF guard, parameterized SQL, CodeQL+Dependabot, 88% coverage); the risk concentrated in the UI↔worker Docker channel + a few real correctness bugs. This closes the high-value, safe findings; the large refactors are deferred (below).

Security

  • Container escape (HIGH) — the worker deploy-spec used a bypassable volume denylist (blocked /var/run/docker.sock but not the parent /var/run → mount it → socket → host root) and never validated network_mode. Now: realpath-normalized checks that block /var/run+/run+all sensitive roots, and a network_mode allowlist (host kept for mysterium, container: blocked). Verified against the catalog (only mysterium needs host; no service mounts a host path).
  • Owner-gate bypass/api/deploy/{slug} is owner-only but the same deploy via /api/workers/{id}/command was writer-only. Now owner-gated everywhere (+ a test_command_deploy_writer_denied regression test).
  • Unmasked secretmain.pys 9-key secret list vs database.SECRET_CONFIG_KEYSs 11 left remember_web/xsrf_token shown unmasked by /api/collectors/meta; now single-sourced from the DB list.
  • Stale sessions — a demoted/deleted user kept access until token expiry; now the per-user epoch is bumped on role-change + deletion.
  • Unauth worker mini-UI status page now requires the fleet key; sanitizeHint blocks javascript: hrefs.

Correctness (event-loop stalls)

  • _validate_worker_url did a blocking socket.getaddrinfo in async context — one slow-DNS worker stalled the entire UI event loop. Now threaded via asyncio.to_thread at the sole call site (validator kept sync → no test churn).
  • docker_available() unthreaded in 4 spots (blocked when Docker was degraded) → to_thread.
  • Fire-and-forget collection tasks now keep a reference + log exceptions (_spawn); record_collection_start moved inside the try; the hourly collector gather is bounded by a semaphore.

Consistency / error-surfacing

  • Both container-action route families now record both the health event and the metrics lifecycle (they diverged, so health/metrics could disagree by URL).
  • The previously-dead rates_stale() is wired into get_all() so the UI can flag stale FX.
  • Frontend stops fabricating $0.00 / eternal spinners / a false-healthy bell on fetch errors — surfaces the error, keeps last values; removes dead code (_breakdownCache, the ******** sentinel); consistent slug escaping.

Verification

ruff clean; full pytest 998 passed (updated the route-side-effect + deploy-gate tests to the new secure behavior, added the deploy-writer-denied test + a _login_attempts autouse reset fixture that removes a latent order-dependent flake).

Deferred — with rationale (need their own PR / your call)

  • Per-worker fleet keys (today one shared key authenticates both directions + maps to a writer role) and a proxy-aware first-run guard (the "private network only" check is proxy-blind → pre-auth owner takeover behind a reverse proxy) — both are auth-model changes touching deployment; high value, but they deserve dedicated design + your input on the trusted-proxy/setup-token approach.
  • main.py god-module extraction (fleet-proxy / dashboard / earnings-view) + build-enforcing the UI/worker Docker boundary + a shared contracts.py — structural refactors.
  • orchestrator.py/worker_api.py test coverage (38%/44% — the Docker-socket code) + true end-to-end (real-DB) tests — a focused test effort.
  • health_events growth — needs an uptime-aware redesign (a naive dedup would break the uptime% metric), not a simple change.

Summary by CodeRabbit

  • New Features

    • Service dashboards now display recent crash counts and flag services as unstable.
    • Exchange-rate responses now include whether cached rates are stale.
  • Security

    • Worker deployment via the generic worker command now requires owner access.
    • Worker status pages are now authenticated, and worker URL/proxy verification is strengthened.
    • Deployment safety checks are tighter for container specs (including network mode and volume mount paths).
    • Role changes and account deletions now invalidate existing active sessions.
  • Improvements

    • Metrics now warn that /metrics is unauthenticated.
    • Frontend refresh/saving behaviors are more resilient (safer hint rendering, preserved figures on API failures, and “Retry” flows).

GeiserX added 2 commits July 10, 2026 15:41
Ported from improvements made in CashPilot-Desktop (the Go port), applied where
the web app didn't already have them:

- dashboard: surface crashes_7d + an "unstable" flag (>=3 crashes in the 7-day
  health window) on each deployed service row, and render a distinct "unstable"
  badge (worst tone + explicit label + restarts/crashes in the tooltip) so a
  crash-looping earner is legible at a glance, not just via a lower score number.
  Reuses the existing get_health_scores data (already computes crashes/restarts).
- metrics: log a startup WARNING when Prometheus /metrics is enabled — it is
  served unauthenticated and exposes earnings/health, so keep it behind a proxy
  and off untrusted networks.

Tests: two new deployed-row tests (unstable at/above vs below the threshold).
ruff clean; full suite 997 passed.
…op stalls, authz, error surfacing

From a full-codebase 8-lens audit. Security + correctness fixes:

Security:
- worker_api: replace the bypassable deploy-spec volume DENYLIST with a
  realpath-normalized check that blocks /var/run + /run parents (closing the
  /var/run:/x -> docker.sock -> host-root escape) and adds a network_mode
  allowlist (permits host for mysterium, blocks container:<id>). Authenticate the
  previously-unauthenticated worker mini-UI status page.
- main: deploy via /api/workers/{id}/command is now owner-gated (was writer — an
  owner-gate bypass of /api/deploy). Single-source the secret-config-key set from
  database.SECRET_CONFIG_KEYS (the shorter local list left remember_web/xsrf_token
  unmasked in /api/collectors/meta).
- auth/routers: invalidate outstanding sessions on user role-change and deletion
  (a demoted/deleted user kept access until the token expired) via the existing
  per-user epoch mechanism.
- app.js: sanitizeHint rejects non-http(s)/mailto hrefs (blocks javascript:).

Correctness (event-loop stalls under async):
- main: _validate_worker_url's DNS resolution now runs via asyncio.to_thread at
  its sole call site (a slow-DNS worker no longer stalls the whole UI event loop);
  the validator stays sync so its existing tests are unaffected.
- worker_api: 4 unthreaded docker_available() calls now use asyncio.to_thread
  (they blocked exactly when Docker was degraded).
- main: fire-and-forget collection tasks keep a reference + log exceptions
  (_spawn); record_collection_start moved inside the try; the hourly collector
  gather is bounded by a semaphore (anti-storm as the catalog grows).

Consistency / error-surfacing:
- main: both container-action route families now record BOTH the health event and
  the metrics lifecycle (they had diverged).
- exchange_rates: wire the previously-dead rates_stale() into get_all() so the UI
  can flag stale FX.
- app.js: stop fabricating $0.00 / eternal spinners / a false-healthy bell on
  fetch errors — surface the error, keep last values; remove dead code.

Tests: negative-role test for deploy-via-command (owner-only); updated the route
side-effect + deploy-gate tests to the new behavior; added a _login_attempts
autouse reset fixture (removes a latent order-dependent flake).

Verified: ruff clean; full pytest 998 passed.
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 025dc5f4-78ba-4bef-b543-7bcd61daec85

📥 Commits

Reviewing files that changed from the base of the PR and between 25a7973 and d316f4a.

📒 Files selected for processing (1)
  • app/main.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/main.py

📝 Walkthrough

Walkthrough

This PR updates background collection, service health and lifecycle reporting, worker security, authentication invalidation, metrics warnings, frontend resilience, input sanitization, and related test coverage.

Changes

Collection and service operations

Layer / File(s) Summary
Bounded collection and tracked tasks
app/main.py
Background collection tasks are tracked, exceptions are logged, and collectors run under a shared concurrency limit.
Service health payloads and lifecycle events
app/main.py, app/exchange_rates.py, tests/test_main_deploy_routes.py, tests/test_main_routes.py
Service and rate responses expose health state and staleness, lifecycle actions record health events before metrics, and route tests cover instability thresholds and event mocks.

Worker security and proxy execution

Layer / File(s) Summary
Worker availability and deployment validation
app/worker_api.py
Docker checks run off the event loop, the status page requires API-key verification, and deployment network and volume validation is expanded.
Async worker proxy verification and authorization
app/main.py, tests/test_coverage_gaps.py, tests/test_main_deploy_routes.py
Worker URL verification becomes asynchronous, and deploy commands require owner authorization while other commands require writer authorization.

Authentication and observability safeguards

Layer / File(s) Summary
Credential, session, and metrics safeguards
app/database.py, app/routers/users.py, app/metrics.py, app/main.py, tests/conftest.py
Credential mismatch warnings, session invalidation, metrics exposure logging, collector secret metadata, and login-state test isolation are updated.

Frontend resilience and dashboard rendering

Layer / File(s) Summary
Dashboard refresh and health presentation
app/static/js/app.js
Failed refreshes preserve existing values or show retry states, health badges consume crash metadata, and alert availability is rendered explicitly.
Escaped actions and credential updates
app/static/js/app.js
Action slugs are escaped, non-empty credential values are saved, and unused claim state is removed.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.30% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main security, authz, and event-loop hardening changes in the PR.
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/web-audit-security-correctness

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.

@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.66667% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.11%. Comparing base (d82fefb) to head (d316f4a).

Files with missing lines Patch % Lines
app/main.py 95.12% 2 Missing ⚠️
app/database.py 0.00% 1 Missing ⚠️
app/metrics.py 66.66% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main      #98      +/-   ##
==========================================
+ Coverage   92.09%   92.11%   +0.01%     
==========================================
  Files          30       30              
  Lines        3085     3118      +33     
==========================================
+ Hits         2841     2872      +31     
- Misses        244      246       +2     
Files with missing lines Coverage Δ
app/exchange_rates.py 100.00% <ø> (ø)
app/routers/users.py 100.00% <100.00%> (ø)
app/database.py 87.02% <0.00%> (ø)
app/metrics.py 82.26% <66.66%> (-0.24%) ⬇️
app/main.py 92.79% <95.12%> (+0.09%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@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

🤖 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 `@app/main.py`:
- Around line 1746-1758: api_worker_command lacks the health and container
lifecycle bookkeeping performed by other management routes. In
api_worker_command, mirror the corresponding record_health_event and
metrics.record_container_lifecycle calls used by /api/services handlers,
covering the proxied command’s success and failure/lifecycle outcomes;
alternatively, explicitly document this endpoint as a raw proxy if that behavior
is intentional.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b1bfcd97-5229-4ef8-83fa-0bbf3fc9ba4b

📥 Commits

Reviewing files that changed from the base of the PR and between d82fefb and 25a7973.

📒 Files selected for processing (11)
  • app/database.py
  • app/exchange_rates.py
  • app/main.py
  • app/metrics.py
  • app/routers/users.py
  • app/static/js/app.js
  • app/worker_api.py
  • tests/conftest.py
  • tests/test_coverage_gaps.py
  • tests/test_main_deploy_routes.py
  • tests/test_main_routes.py

Comment thread app/main.py
Comment on lines 1746 to 1758
@app.post("/api/workers/{worker_id}/command")
async def api_worker_command(request: Request, worker_id: int, body: WorkerCommand) -> dict[str, Any]:
"""Send a command to a worker by proxying to its REST API."""
_require_writer(request)
# Deploy is owner-gated everywhere else (see /api/deploy/{slug}); a writer
# must not be able to bypass that gate by sending command="deploy" here.
if body.command == "deploy":
_require_owner(request)
else:
_require_writer(request)

worker = await database.get_worker(worker_id)
url, headers = _get_verified_worker_url(worker)
url, headers = await _get_verified_worker_url(worker)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if the UI frontend calls /api/workers/{id}/command or only slug-specific routes
rg -n "workers/.*command\|/api/services/.*\(restart\|stop\|start\|remove\)" app/static/js/ --type=js

Repository: GeiserX/CashPilot

Length of output: 155


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate api_worker_command and related lifecycle recording paths
rg -n "api_worker_command|record_health_event|record_container_lifecycle|unstable" app/main.py app -g '!app/static/**'

# Show the relevant section around api_worker_command
sed -n '1720,1795p' app/main.py

# Find frontend / API usages of worker command versus slug-specific routes
rg -n "/api/workers/.*/command|api/workers/.*/command|/api/services/.*/(restart|stop|start|remove)|api/services/.*/(restart|stop|start|remove)" app -g '!app/static/**'

Repository: GeiserX/CashPilot

Length of output: 6914


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether worker-side container endpoints already record lifecycle/health data
rg -n "api/containers/.*/(deploy|stop|restart|start)|record_health_event|record_container_lifecycle|unstable" app/worker_api.py app/main.py app -g '!app/static/**'

# Show the relevant worker API section
sed -n '1,260p' app/worker_api.py

# Show the slug-specific routes for comparison
sed -n '760,830p' app/main.py
sed -n '1038,1098p' app/main.py

Repository: GeiserX/CashPilot

Length of output: 19873


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for any in-repo callers/docs of the raw worker command route
rg -n "/api/workers/\{worker_id\}/command|api_worker_command|workers/.*/command" .

# Show the frontend container-action calls, if any, for comparison
rg -n "/api/services/\{slug\}/(restart|stop|start)|/api/(restart|stop|remove)/\{slug\}|worker_id=" app/static app/templates app/main.py

Repository: GeiserX/CashPilot

Length of output: 2446


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the tests for /api/workers/{id}/command to see intended behavior
sed -n '500,670p' tests/test_main_deploy_routes.py
sed -n '660,730p' tests/test_coverage_gaps.py

# Show the route helper it uses, if any
rg -n "def _proxy_worker_command|_proxy_worker_deploy|record_health_event|record_container_lifecycle" app/main.py

Repository: GeiserX/CashPilot

Length of output: 10839


api_worker_command still skips health/lifecycle bookkeeping. The UI uses /api/services/*, so this only affects direct callers of /api/workers/{id}/command; if it should behave like the other management routes, mirror their record_health_event and metrics.record_container_lifecycle calls, or document it as a raw proxy.

🤖 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 `@app/main.py` around lines 1746 - 1758, api_worker_command lacks the health
and container lifecycle bookkeeping performed by other management routes. In
api_worker_command, mirror the corresponding record_health_event and
metrics.record_container_lifecycle calls used by /api/services handlers,
covering the proxied command’s success and failure/lifecycle outcomes;
alternatively, explicitly document this endpoint as a raw proxy if that behavior
is intentional.

@GeiserX GeiserX merged commit 3e10aa4 into main Jul 10, 2026
7 checks passed
@GeiserX GeiserX deleted the fix/web-audit-security-correctness branch July 10, 2026 16:39
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