Skip to content

fix: drop passlib + security hardening — fixes #15#16

Merged
GeiserX merged 3 commits intomainfrom
fix/drop-passlib-bcrypt-compat
Apr 15, 2026
Merged

fix: drop passlib + security hardening — fixes #15#16
GeiserX merged 3 commits intomainfrom
fix/drop-passlib-bcrypt-compat

Conversation

@GeiserX
Copy link
Copy Markdown
Owner

@GeiserX GeiserX commented Apr 15, 2026

Summary

Fixes #15ValueError: password cannot be longer than 72 bytes on any signup/login.

Root Cause

passlib 1.7.4 (abandoned since 2020) runs an internal self-test (detect_wrap_bug) that sends a 255-byte test password to bcrypt.hashpw(). bcrypt >=5.0.0 strictly rejects passwords >72 bytes. This crashes on the first call regardless of actual password length — every new deployment is broken.

Fix: Drop passlib, use bcrypt directly

Replace passlib.hash.bcrypt with direct bcrypt.hashpw()/checkpw(). The $2b$ hash format is identical — existing password hashes remain valid, no migration needed.

Security Hardening (from automated review)

Category Fix
Worker XSS Escape all dynamic values in worker status page HTML (html.escape)
SSRF Validate worker URLs before proxying — reject loopback, link-local, localhost
Session cookie Auto-detect secure flag via CASHPILOT_SECURE_COOKIE / CASHPILOT_BASE_URL
Security headers Add middleware: X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy
Username validation Enforce 3-32 alphanumeric chars (a-z, 0-9, _, -) on registration
Password minimum Raised from 6 to 8 characters
CDN integrity Pin Chart.js v4.4.8 with SRI hash

Code Quality

  • Fiat conversion: exchange_rates.to_usd() now handles fiat currencies via Frankfurter rates (previously silently dropped non-USD fiat earnings)
  • Deprecated API: Replace datetime.utcnow() with datetime.now(datetime.UTC) across database.py and main.py
  • Inline imports: Move import re, from datetime import ... to top-level
  • __import__ hack: Replace with standard from app.collectors import make_collectors
  • DRY: Extract _get_verified_worker_url() shared helper for 3 proxy functions

Files Changed

  • app/auth.py — Drop passlib, direct bcrypt, secure cookie flag
  • app/main.py — Security headers, SSRF validation, username validation, password min, imports cleanup
  • app/worker_api.py — HTML escape all dynamic values
  • app/exchange_rates.py — Add fiat currency conversion
  • app/database.py — Replace deprecated datetime.utcnow()
  • app/templates/base.html — Chart.js SRI
  • requirements.txt — Remove passlib, keep bcrypt>=4.0
  • tests/test_auth.py (new) — 15 tests: password hashing, truncation, sessions

Test plan

  • 426 tests pass locally (15 new auth + 411 existing)
  • CI lint (ruff check + format)
  • CI tests pass
  • CodeRabbit review
  • Codecov check

Summary by CodeRabbit

Release Notes

  • New Features

    • Fiat currency conversions are now supported for exchange rate calculations.
  • Bug Fixes

    • Fixed earnings date calculations for improved accuracy across time zones.
    • Enhanced security protections against injection attacks and unauthorized access attempts.
    • Added security headers to all application responses.
    • Strengthened password requirements (minimum 8 characters) during registration.

passlib 1.7.4 (abandoned since 2020) is incompatible with bcrypt >=5.0.
Its internal detect_wrap_bug() self-test sends a 255-byte password to
bcrypt.hashpw(), which bcrypt 5.0+ strictly rejects with ValueError.
This crashes on the first call regardless of actual password length.

Replace passlib with direct bcrypt.hashpw()/checkpw(). The $2b$ hash
format is identical, so existing password hashes remain valid.
Also adds 15 auth tests (hashing, truncation, sessions).
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 15, 2026

Warning

Rate limit exceeded

@GeiserX has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 12 minutes and 25 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 12 minutes and 25 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6dbe08f2-15ab-4f8a-986c-4c1d9f914b84

📥 Commits

Reviewing files that changed from the base of the PR and between e370326 and 65278d6.

📒 Files selected for processing (1)
  • app/main.py
📝 Walkthrough

Walkthrough

This PR addresses a signup bug by switching password hashing to bcrypt with byte truncation, adds security headers middleware, validates worker URLs to mitigate SSRF, tightens registration validation, updates timezone awareness, supports fiat currency conversion, escapes HTML output, and includes comprehensive auth tests.

Changes

Cohort / File(s) Summary
Authentication & Hashing
app/auth.py, requirements.txt, tests/test_auth.py
Switched password hashing from passlib.hash.bcrypt to direct bcrypt library with UTF-8 truncation to 72 bytes; updated cookie security handling to use runtime configuration (CASHPILOT_SECURE_COOKIE or infer from https in CASHPILOT_BASE_URL); removed passlib dependency and loosened bcrypt version constraint; added comprehensive test coverage for hash/verify round-trips, truncation behavior, and token handling.
API Security & Validation
app/main.py, app/worker_api.py
Added global security headers middleware (X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy); introduced worker URL validation (_validate_worker_url, _get_verified_worker_url) to reject loopback/link-local IPs and enforce http/https schemes, reducing SSRF risk; tightened registration validation with username regex (3-32 chars, a-zA-Z0-9_-) and increased minimum password length to 8; added HTML escaping (html.escape) to dynamic values in worker status page output.
Data Handling & Currency
app/database.py, app/exchange_rates.py
Replaced datetime.utcnow() with timezone-aware datetime.now(datetime.UTC) across earnings calculations, dashboard date aggregation, and daily chart windows; extended to_usd() to support fiat currency conversion by checking _fiat_rates when crypto rates unavailable.
Frontend Assets
app/templates/base.html
Pinned Chart.js CDN from unpinned v4 to v4.4.8 with Subresource Integrity hash and crossorigin="anonymous" attribute.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.71% 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 clearly summarizes the main changes: removing passlib dependency and implementing security hardening, which directly corresponds to the primary objectives stated in the PR.
Linked Issues check ✅ Passed The PR fully addresses issue #15 by replacing passlib with direct bcrypt calls, eliminating the 72-byte truncation error that prevented signup. All security hardening changes align with the broader security objectives.
Out of Scope Changes check ✅ Passed All changes directly support the bug fix (#15) and security hardening objectives. While broader than the minimal fix, datetime modernization, Chart.js pinning, and exchange rates enhancement are all reasonable code quality improvements within the scope of a comprehensive security-focused release.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/drop-passlib-bcrypt-compat

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

- Worker status page: escape all dynamic HTML (XSS fix)
- Session cookie: auto-detect secure flag via CASHPILOT_SECURE_COOKIE env
- Security headers middleware: X-Content-Type-Options, X-Frame-Options,
  Referrer-Policy, Permissions-Policy
- SSRF protection: validate worker URLs before proxying (reject loopback,
  link-local, localhost)
- Username validation: 3-32 alphanumeric chars on registration
- Password minimum raised from 6 to 8 characters
- exchange_rates.to_usd(): add fiat currency conversion via Frankfurter rates
- Replace datetime.utcnow() with datetime.now(datetime.UTC) (deprecated)
- Remove inline imports (re, datetime) — moved to top-level
- Replace __import__() with standard from-import
- Chart.js: pin v4.4.8 with SRI integrity hash
- DRY: extract _get_verified_worker_url() shared helper for proxy functions
@GeiserX GeiserX changed the title fix: drop passlib, use bcrypt directly — fixes #15 fix: drop passlib + security hardening — fixes #15 Apr 15, 2026
@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 15, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 15, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/main.py (1)

207-211: ⚠️ Potential issue | 🔴 Critical

Timezone-aware vs naive datetime comparison will raise TypeError.

cutoff is timezone-aware (datetime.now(datetime.UTC)), but last_heartbeat from the database is naive (SQLite's datetime('now') has no timezone). Comparing them at line 211 will raise TypeError: can't compare offset-naive and offset-aware datetimes.

Proposed fix
-        cutoff = datetime.now(datetime.UTC) - timedelta(seconds=STALE_WORKER_SECONDS)
+        cutoff = datetime.utcnow() - timedelta(seconds=STALE_WORKER_SECONDS)

Or make the parsed datetime timezone-aware:

                 last = datetime.fromisoformat(w["last_heartbeat"])
+                last = last.replace(tzinfo=datetime.UTC)
                 if last < cutoff:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/main.py` around lines 207 - 211, The comparison is failing because cutoff
is timezone-aware (cutoff = datetime.now(datetime.UTC)) while last parsed from
w["last_heartbeat"] is naive; update the parsing in the workers loop (where last
= datetime.fromisoformat(w["last_heartbeat"])) to produce a timezone-aware
datetime (e.g., attach datetime.UTC via replace(tzinfo=...) or parse with an
aware parser) so that last and cutoff use the same timezone awareness before
comparing, or alternatively make cutoff naive by using naive now() — adjust only
the branch around cutoff/last in the worker status check to keep both datetimes
consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@app/main.py`:
- Around line 207-211: The comparison is failing because cutoff is
timezone-aware (cutoff = datetime.now(datetime.UTC)) while last parsed from
w["last_heartbeat"] is naive; update the parsing in the workers loop (where last
= datetime.fromisoformat(w["last_heartbeat"])) to produce a timezone-aware
datetime (e.g., attach datetime.UTC via replace(tzinfo=...) or parse with an
aware parser) so that last and cutoff use the same timezone awareness before
comparing, or alternatively make cutoff naive by using naive now() — adjust only
the branch around cutoff/last in the worker status check to keep both datetimes
consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 83ccc6ea-4644-4f88-985c-deb3f7076d96

📥 Commits

Reviewing files that changed from the base of the PR and between 7a58e1d and e370326.

📒 Files selected for processing (8)
  • app/auth.py
  • app/database.py
  • app/exchange_rates.py
  • app/main.py
  • app/templates/base.html
  • app/worker_api.py
  • requirements.txt
  • tests/test_auth.py

CodeRabbit found that datetime.fromisoformat() returns naive datetimes
from SQLite, but cutoff uses datetime.now(UTC). Attach tzinfo to the
parsed heartbeat timestamp to avoid TypeError on comparison.
@GeiserX GeiserX merged commit b9b3d61 into main Apr 15, 2026
6 checks passed
@GeiserX GeiserX deleted the fix/drop-passlib-bcrypt-compat branch April 15, 2026 14:04
@GeiserX GeiserX mentioned this pull request Apr 15, 2026
2 tasks
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.

[Bug]: Cannot signup

1 participant