Step-up-Passwortpruefungen ohne Rate-Limiting (Account-Loeschung, Ownership-Transfer) #156
Labels
No labels
priority/could
priority/must
priority/should
priority/wont
status/blocked
status/claimed
status/done-migrated
type/bug
type/feature
type/infra
type/tech-debt
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
robert/todo#156
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Story: Step-up-Passwortpruefungen ohne Rate-Limiting (Account-Loeschung, Listen-Ownership-Transfer)
As a Nutzer, dessen Session kompromittiert werden koennte,
I want to dass wiederholte falsche Versuche gegen
AuthorizeCurrentPasswordQuerygenauso gedrosseltwerden wie beim Login,
so that ein Angreifer mit gestohlener Session (z.B. Cookie-Diebstahl/XSS) das aktuelle Passwort nicht
unbegrenzt oft gegen
DeleteCurrentUserAccountCommandoderTransferTodoListOwnershipCommanderraten kann.Kontext:
AuthorizeCurrentPasswordQuery(eingefuehrt mit #59) hat aktuell kein Rate-Limiting, imGegensatz zu
LoginUserCommand. Niedrigere Schwere als die Login-Luecke (setzt eine bereits kompromittierteSession voraus), aber dieselbe
"login"-Policy sollte sinngemaess auch hier greifen. Vollstaendigdokumentiert in
docs/SECURITY_NOTES.mdunter "Known open risks" -> "[OPEN] Step-up password checks arenot rate-limited".
Acceptance criteria:
AuthorizeCurrentPasswordQuery-Aufrufe sind rate-limitiert, analog zur bestehenden"login"-Policy(gleiche Groessenordnung an Versuchen/Zeitfenster, sofern kein triftiger Grund fuer eine Abweichung
gefunden wird).
ein einzelner kompromittierter Account gezielt gedrosselt wird.
DeleteCurrentUserAccountCommandundTransferTodoListOwnershipCommandwerden nach der konfigurierten Schwelle mit 429 abgelehnt.Out of scope for this story:
Rate-Limiter hinter Traefik wirkungslos") - falls diese Story zuerst umgesetzt wird, gilt fuer den
IP-Anteil des Keys hier dieselbe Einschraenkung.
Open questions: (escalate to human if unanswered)
"login"-Preset orientieren, sofern der Backend Engineer keinen Grund fuer eine Abweichung findet.Claiming this issue. Plan: add an in-process rate limiter to AuthorizeCurrentPasswordQueryHandler (Checkly/Features/Authorization/), gating step-up password confirmation for any caller (currently DeleteCurrentUserAccountCommand and TransferTodoListOwnershipCommand). Partitioned by the current authenticated UserId rather than IP/username, since the acceptance criteria calls for keying off the already-established session rather than IP - matches the threat model (an attacker with a stolen session, not an unauthenticated brute-forcer). Same order of magnitude as the existing "login" policy: 5 attempts/minute, fixed window, no queueing.
Rejected attempts throw a new RateLimitExceededException mapped to HTTP 429 in ExceptionHandler.cs, mirroring the ASP.NET Core RateLimiter's own RejectionStatusCode for the "login" policy. Regression tests: handler-level test asserting the exception after exceeding the limit, plus a WebApi-level test exercising DeleteCurrentUserAccountCommand/TransferTodoListOwnershipCommand end to end.
Implemented and pushed to master (commits 0fd9998 feature, fa45db6 code-review fixes).
Scope delivered:
Code-review fixes (2 findings, both fixed):
Two lower-severity review findings were left as accepted, deliberate design choices rather than fixed: the rate-limit bucket is shared across both gated commands and consumed on successful attempts too (mirrors the existing "login" policy's own behavior on purpose), and the "5/min" threshold is duplicated as a literal against Program.cs's "login" policy rather than a shared constant (not worth an abstraction for two call sites).
Tests: AuthorizeCurrentPasswordQueryHandlerTests updated (existing tests now stub the new dependency; added a new test asserting RateLimitExceededException is thrown, and that the password is never checked, when the limiter rejects). New StepUpPasswordRateLimiterTests directly exercises the concrete limiter. New MeteredHandlerTests case for the "rate_limited" outcome tag. All non-Docker-dependent test projects (Common.Tests: 119/119, Checkly.WebApi.Tests: 50/50, plus the new/updated Checkly.Tests files) verified green locally. The DB-backed handler tests (Checkly.Tests.dll broadly) can't run in this sandbox (no Docker for Testcontainers) - left for real CI to verify, consistent with this repo's established sandbox limitation.
Docker note: step 6 of the loop (rebuild/restart the local review container) could not run this cycle -
docker infohung past its timeout in this environment (matches a known, previously-documented sandbox limitation: Docker Desktop does not start here). Noted explicitly rather than silently skipped.Closing as done.