Skip to main content
← Back to list
01Issue
BugOpenSwamp ClubPublic
AssigneesNone

Relationships

#1812 Identifier lookups are exact-match and case-sensitive across username, email and slug

Opened by keeb · 8/24/2026

Found by code scan. Hardening finding — the app currently stores these fields lowercased at write time, so nothing is observably broken today. The exposure is on the input side and on any future writer that does not normalise.

The sites

lib/infrastructure/better-auth-operative-queries.ts:42

const doc = await this.db.collection("user").findOne({ username });

Backs the invite-by-username branch. The value comes straight from a free-text form field with no normalisation beyond .trim(). A username typed with different capitalisation than stored yields Operative not found.

lib/infrastructure/better-auth-operative-queries.ts:94

const user = await this.db.collection("user").findOne({ email });

isEmailVerified — a miss here reports an operative as unverified, which is a fail-closed path that silently withholds whatever it gates.

lib/infrastructure/better-auth-collective-queries.ts:780

const doc = await this.db.collection("organization").findOne({ slug });

Collective lookup by slug. A mixed-case slug in a URL or an API call 404s.

lib/infrastructure/username-resolution.ts:62

await users.findOne({ username: name }) ?? await orgs.findOne({ slug: name });

Both halves exact.

lib/infrastructure/mongo-platform-invite-repository.ts:63

return this.invites.findOne({ email })...

No .toLowerCase() on the lookup, while lib/app/claim-platform-invites.ts:68 lowercases before comparing. The two ends of the same flow disagree about normalisation.

The asymmetry that makes this fragile

Write paths already normalise deliberately — lib/app/collective-commands.ts:226 lowercases the invited email before storing, with a comment explaining that a mixed-case invite would otherwise stay pending forever. claim-pending-invitations.ts:141 and accept-invite.tsx:98-99 lowercase too.

So the invariant "these fields are stored lowercase" is real but enforced only by convention at each writer, and the readers depend on it silently. One writer that skips the step — a new OAuth provider that hands back a capitalised address, an admin tool, a migration, a seed script — and these reads start missing with no error, just an empty result that every caller interprets as "does not exist."

Suggested fix

Either:

  1. Normalise at the read edge — lowercase in each of these lookups, or add $options: "i" where an index-backed exact match is not required — or
  2. Make the invariant enforceable rather than conventional: normalise on write in one place at the repository boundary, and add a case-insensitive collation or a stored normalised field with a unique index, so a non-conforming write cannot land.

Option 2 is the durable one. Option 1 alone leaves each new read site free to reintroduce the same bug.

Note that username is also matched exactly in findUserIdsByUsernames (:139) and in the $in shape at :145, which have the same property.

02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED

Open

8/24/2026, 11:57:56 PM

No activity in this phase yet.

03Sludge Pulse

Sign in to post a ripple.