- Shipped
- August 19, 2026 at 8:09 AM UTC
- Author
- Kamo
- Commit
- 19ddc01
Two halves of one incident. An account registered, verified an address at a disposable provider, took an auto-login session and created five organizations in three minutes and forty-two seconds — four of them byte-identical "Acme Corp" rows differing only in their domain. It produced ZERO rows in system_access_logs, so there is no IP and no user-agent on record for any of it. ── Enforcing the flag **************** - UserAuthenticationService: `u.is_fake = FALSE` goes into ACCOUNT_QUERY_PREFIX, not into a caller. All three login identifiers — username, account email, and the primary-mailbox re-lookup keyed by id — reuse that one projection, so one clause closes all three and a fourth identifier added later inherits it. A flagged account reads as non-existent and gets the generic "user does not exist or password is incorrect"; a message naming the flag would tell an abuser exactly what to change. - KSessionService.createSession: refuses outright. This is the narrowest waist in the system for "may this account act" — every *** is minted here, so one gate covers password login, enter-as, desktop SSO, device auth AND the register funnel's post-verification auto-login, which is how the account in question got a session without ever calling /login. Throws rather than returning null, because thirty callers assume a non-null id and would NPE somewhere else entirely; login and the auto-session path catch it and shape a clean answer. - FakeAccountGuard: two tiers on purpose. The single-account check is UNCACHED (it backs the hard gates, where a stale answer means a flagged account still gets in); the id sets are cached for 60s (they back read filtering, where the worst case is a synthetic org lingering in a listing for under a minute). Fails open — hiding a handful of rows is not worth 500ing an org listing — and does NOT cache a failure, so one blip does not become a minute of the flag silently not working. - OrgObjectStorageSweep: stops snapshotting synthetic orgs. This is where the money was: snapshots are written per org per day forever, and five abandoned orgs had already become ~9% of that table. An account that cannot log in does nothing about work the platform keeps doing on its behalf. - FakeAccountController: flag, unflag, list — behind MANAGE_ORGANIZATIONS rather than a new platform right, because kamo-internal's platformRightCoverage test asserts the right set against the console's rendered surfaces. Revokes live sessions on flagging, since the gates are on MINTING a session, not on using one; refuses to flag the System User, which would make the platform unable to enter any child org. ── The holes **************** - /register never read `***Token`. The register UI has always solved an Capcha challenge and posted the result; grep for the field across all Java returned nothing. The widget was in the browser and the endpoint was wide open. Now verified, and FAIL CLOSED — asymmetric with login on purpose: login can only verify a payload when one is present, because the mobile client sends none and hard-requiring it there would lock real users out. Registration has exactly one client and it already sends the token. - CapchaVerificationService returned TRUE when `capcha.service-url` was unset, so a config omission was indistinguishable from a solved challenge. Now rejects, behind `capcha.allow-unconfigured` (default false) for local runs. No production change — the property is set in kamowssecurity-config — but the latent bypass is gone. - POST /org had no rate limit of any kind. Now capped per ACCOUNT (not per IP — an IP key would punish everyone behind one corporate NAT). Deliberately generous: a real customer created three orgs in five and a half minutes, two with the same name and alias, while working out the wizard. A tight limit would have blocked them. Counts attempts, not successes, and fails open on a Redis outage. - The org alias had NO uniqueness check on this path — only the domain did, which is why four live orgs share "acme-corp" and why the creator varied only the domain each time: it was the one field the server would reject. Now a 409, scoped to the security provider because `(security_provider_id, alias)` is the pair login resolves a web-alias host on. - REGISTRATION / REGISTRATION_REJECTED / EMAIL_VERIFIED / ORG_CREATED are now written with IP and user-agent. Every detection rule keys off login events, so the whole register funnel sat outside the detection engine's field of view. Deliberately NOT done, having checked: login is not made ***-strict (breaks the mobile app, needs a coordinated release); disposable-email domains are not blocked (a real customer signs in from proton.me, and a naive "not a mainstream provider" rule blocks real people); no content-scoring auto-block (the strongest-looking behavioural signal — duplicate org names created minutes apart — matches a real customer exactly). Schema prerequisite already satisfied: KamoInitializer applied users.is_fake before the shared-lib push, verified as `boolean NOT NULL DEFAULT false` with its partial index, and re-ran idempotently without clearing the flag.