Repository navigation
Release v0.6.0 - Authentication on crudauth
Release Notes by the CRUDAdmin team
CRUDAdmin 0.6 moves the admin's authentication onto crudauth. Password checks, sessions, CSRF tokens and login lockout are now crudauth's; CRUDAdmin renders the pages around them. That closes the security gaps still open after 0.5.2:
- CSRF tokens were issued but never checked.
- The login form had no rate limiting.
X-Forwarded-Forwas trusted as-is.
On top of that, account management becomes superuser-only, admins can see and end sessions, and a set of audit-log and form bugs are fixed.
This release has breaking changes and signs every admin out once. Read Upgrading before deploying.
pip install -U crudadminSecurity fixes
This release fixes three reports that were in triage:
| Advisory | Report | Fix |
|---|---|---|
| GHSA-fv5q-2mg9-c62x | IP spoofing through X-Forwarded-For in session creation and login tracking |
The header is ignored unless trusted_proxy_hops says how many trusted proxies sit in front of the app |
| GHSA-4cwg-f726-9hfv | Rate limiting not enforced on the login endpoint | Login lockout per username and per IP |
| GHSA-7c4p-r94r-6r3j | Excessive authentication attempts | Same login lockout |
Client IP spoofing (GHSA-fv5q-2mg9-c62x)
CRUDAdmin 0.5 took the client address from the first X-Forwarded-For entry, which any client can set:
- A client could choose the IP recorded on its session and in the audit log.
- A value that wasn't an IP made every login from that client fail.
0.6 resolves the client address with crudauth's trusted-proxy rule:
- By default the header is ignored and the socket peer is used.
- Behind N reverse proxies, set
trusted_proxy_hops=N. The N-th entry from the right is read; values an attacker prepends sit further left and are never read.
No login rate limiting (GHSA-4cwg-f726-9hfv, GHSA-7c4p-r94r-6r3j)
0.5 shipped a rate limiter but never wired it into the login form, so password guessing was unlimited. 0.6 uses crudauth's login lockout:
- Trigger: five failures lock the username and the IP.
- Duration: one minute at first, doubling to at most five minutes.
- Reset: a successful login clears the counts.
Lockout counters are shared across workers with the redis and database session backends.
The cap is deliberately short. Anyone who knows an admin's username can trigger a lockout. With a five-minute cap, a lockout is an annoyance rather than an outage. allowed_ips keeps strangers away from the login page altogether. Tune the lockout with CRUDAdmin(lockout=LockoutConfig(...)).
CSRF tokens were never checked
0.5 issued a CSRF token at login but no route checked it; only SameSite=Strict cookies stood in the way. In 0.6:
- The header is required: every POST, PUT, PATCH and DELETE must carry the token in the
X-CSRF-Tokenheader. A cookie travels with cross-site requests but a custom header doesn't, which is what makes the check meaningful. admin.jssends it: a new static script adds the header to htmx requests andfetchcalls, and submits the plain create/update forms throughfetchso they carry it too.- Logout is a POST:
GET /logoutis gone.
Classification: OWASP A01:2021 (Broken Access Control) and A07:2021 (Identification and Authentication Failures).
What changed
Authentication on crudauth
- Login: crudauth's
authenticate_password, which counts failures toward the lockout and takes as long for an unknown username as for a wrong password. - Sessions: created by crudauth's
complete_loginand ended bycomplete_logout. Session ids and CSRF tokens are stored as HMACs keyed withSECRET_KEY, never raw. - Namespacing: cookies and storage keys are namespaced (
crudadmin_session,crudadmin:…), so the admin can share a browser and a Redis with a host app that also uses crudauth. - 0.5 password hashes keep working: they verify and are rehashed in crudauth's format at the next login.
- The login page: answers bad credentials with 401, a lockout with 429 and a cross-site login with 403. It shows only its own messages from the
errorquery parameter.
Permissions
- Superuser-only: admin users and the event log. New admins aren't superusers unless created as one; the initial admin is.
- Password confirmation: creating or editing an admin asks the superuser to confirm their password, which lasts five minutes. A stolen session alone can't add an admin or reset a password.
- Sessions follow account changes: a password change ends the admin's other sessions, and deactivating or demoting an admin ends all of theirs at once.
- The last active superuser can't be demoted or deactivated.
Sessions page
/management/sessions replaces the AdminSession table view:
- It shows each session's device, IP and times, never its id.
- Admins see and end their own sessions; superusers see and end everyone's.
admin.session_managerexposes crudauth's session manager to your own code.
Session backends
| Backend | Use it for |
|---|---|
memory (default) |
One process. Sessions and lockout counts are lost on restart and not shared between workers; a startup warning says so. |
redis |
Several workers or hosts. |
database |
Several workers without Redis. Sessions and lockout counters live in the admin database, in crudadmin_auth_store and crudadmin_auth_counters. |
memcachedis refused at startup.hybrid(Redis plus a database copy of each session) now meansredisand warns.
Audit log
- Failures are recorded as failures: an action is recorded as
successorfailurefrom its response. A failed action gets no audit rows. - Deletes are audited: a delete writes one audit row per deleted record, keyed by the model's primary key, including keys not named
id. Creates use the real primary key too. - One transaction: an event and its audit rows are written together and rolled back together if any write fails. A logging failure no longer leaves the database session unusable.
- Login events: logins, logouts, failed logins and lockouts are recorded through crudauth hooks, carrying the session's public handle.
- Custom
db_config:track_events=Truewith your owndb_configgets the event tables automatically.
Forms and lists
- Field types:
Optional[bool]and otherOptional[...]fields render as their real input type, soOptional[bool]is a checkbox. - Clearing a field: an empty input clears a nullable column on update.
- Password transformer: it keeps
Falseand0. - Admin edits: editing an admin user no longer fails. The update handler added
updated_atto the form data, whichAdminUserUpdaterejects. - Bulk delete: it works with UUID primary keys, and a malformed id gets a 422.
- Pagination: it keeps the active search.
- List errors: a list query that fails is an error page, not "0 items".
- Single page load: a form submitted through
admin.jsloads the next page once.
Upgrading from 0.5 in place
- Automatic upgrade:
initialize()adds the newis_activeandtoken_versioncolumns to an existingadmin_usertable. - Several workers at once: each column is added in its own transaction, and a worker that loses the race carries on.
- Tested: on SQLite, PostgreSQL 16 and MySQL 8.
Behavior changes
- Every admin is signed out once when 0.6 is deployed.
SECRET_KEYis required and used. It keys stored session ids and CSRF tokens; changing it signs everyone out.- Logout is a POST with the
X-CSRF-Tokenheader. session_backend="memcached"andmemcached_config=fail at startup.- New admins aren't superusers by default.
- Admin users and the event log are superuser-only.
- Errors inside admin pages return a 500 instead of a redirect to the login page.
Upgrading
-
Update.
pip install -U crudadmin. This addscrudauth[useragent]>=0.8.2,<0.9, which brings PyJWT. Thememcachedextra is gone. -
Close the stores. Call
admin.shutdown()afteryieldin your lifespan:@asynccontextmanager async def lifespan(app: FastAPI): await admin.initialize() yield await admin.shutdown()
-
Change Memcached settings. Replace
session_backend="memcached"with"redis", or with"database"to keep sessions in the admin database. -
Behind a reverse proxy, set
trusted_proxy_hopsto the number of proxies. -
Custom logout links must POST to
/logoutwith theX-CSRF-Tokenheader. Pages that extendbase/base.htmlget it fromadmin.js. -
Schema.
initialize()addsis_activeandtoken_versiontoadmin_user. If you manage the admin schema with Alembic instead:op.add_column( "admin_user", sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.true()), ) op.add_column( "admin_user", sa.Column("token_version", sa.Integer(), nullable=False, server_default="0"), )
-
Drop the old table.
admin_sessionis no longer used and can be dropped:DROP TABLE admin_session;
-
Removed modules. Code that imported them must move to crudauth or
admin.session_manager:crudadmin.session.manager,crudadmin.session.storage,crudadmin.session.backends;crudadmin.core.auth,crudadmin.core.rate_limiter;AdminUserService,log_auth_action.
RedisConfigandMemcachedConfigare still importable. -
Deprecations.
session_backend="hybrid"andtrack_sessions_in_dbstill work for this release, with aDeprecationWarning.
Tests
The suite has 320 tests, and ruff and mypy are clean. mypy now covers the tests too, through one configuration with the pydantic plugin. New coverage:
- Login: cookies, lockout, CSRF on logout and writes, session ids kept out of logs, and the event log.
- 0.5 compatibility: a 0.5 password hash logging in and being upgraded.
- Permissions: superuser-only areas, password confirmation and its lockout, sessions ended on password change and deactivation, and the last-superuser guard.
- Upgrades: the in-place upgrade of a 0.5 admin database, including two workers starting at once.
- Session backends: selection, Redis client settings, and
hybrid/memcachedhandling. - Forms, lists and audit log: through a real app with a non-
idprimary key and a UUID key.
The redirect and form-submission changes were also checked in a real browser: login, create, bulk delete, password confirmation, the Sessions page and logout.
What's Changed
- Move admin authentication onto crudauth: login, sessions, CSRF, lockout and trusted-proxy IPs
- Make account management superuser-only, with password confirmation for changes and a last-superuser guard
- Add the Sessions page and end sessions on password change or deactivation
- Support
memory,redisanddatabasesession backends; refusememcached, maphybridtoredis - Upgrade 0.5 admin databases in place, safely under concurrent startup
- Fix the audit log: status from the response, per-record delete rows, one transaction
- Fix forms and lists: optional field types, clearing fields, UUID bulk delete, pagination search, list errors, admin-user edits
- Use one mypy configuration for the package and the tests
Full Changelog: v0.5.2...v0.6.0