fix(client): reserve the discovery @index account key (prevent user collision)
The special discovery-index account was keyed as "@index", which the consumer's normalizeUsername collapses to "index" — colliding with a real user named "index" (who could then hijack/tamper the global index document). Introduce a reserved-account namespace (a sentinel key unreachable by any typed username) so the index account can never collide with user input. Test proves a user named "index"/"@index" resolves to a DIFFERENT document than the reserved index account. 80 tests pass; tsc rc=0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@ import {
|
||||
resetConfig,
|
||||
setCurrentUser,
|
||||
} from "../src/polyfill";
|
||||
import { resetRegistryCache } from "../src/store-registry";
|
||||
import { resetRegistryCache, ensureAccount } from "../src/store-registry";
|
||||
import type { RegistrySession } from "../src/store-registry";
|
||||
|
||||
// discovery.ts submits to / reads from a global index owned by a RESERVED
|
||||
@@ -209,8 +209,34 @@ test("from: null makes an anonymous submission", async () => {
|
||||
expect(entries[0]!.from).toBeNull();
|
||||
});
|
||||
|
||||
test("INDEX_ACCOUNT is the reserved special account", () => {
|
||||
expect(INDEX_ACCOUNT).toBe("@index");
|
||||
test("INDEX_ACCOUNT lives in the reserved namespace (no typed username can equal it)", () => {
|
||||
// The index account occupies a key no user input can produce: it is prefixed
|
||||
// with a NUL control char, which a user cannot type into a username field and
|
||||
// which no `normalizeUser` output (a typeable value) contains. So it is
|
||||
// disjoint from the keys "index" / "@index" a hostile user would submit.
|
||||
expect(INDEX_ACCOUNT.startsWith("\u0000")).toBe(true); // unreachable-by-typing sentinel
|
||||
expect(INDEX_ACCOUNT).not.toBe("index");
|
||||
expect(INDEX_ACCOUNT).not.toBe("@index");
|
||||
});
|
||||
|
||||
test("a user named 'index'/'@index' does NOT resolve to the index account's document", async () => {
|
||||
// The discovery index lives on INDEX_ACCOUNT. A hostile (or unlucky) user who
|
||||
// registers as "index" or "@index" normalizes to key "index" — which must be
|
||||
// a DISJOINT key from the reserved index account, so they get their own
|
||||
// documents and cannot hijack / read-write the global index document.
|
||||
const indexRecord = await ensureAccount(INDEX_ACCOUNT);
|
||||
|
||||
// A real user "index" — same normalized form as "@index".
|
||||
const userIndex = await ensureAccount("index");
|
||||
expect(userIndex.docPublic).not.toBe(indexRecord.docPublic);
|
||||
expect(userIndex.docProtected).not.toBe(indexRecord.docProtected);
|
||||
expect(userIndex.docPrivate).not.toBe(indexRecord.docPrivate);
|
||||
|
||||
// "@index" must land on the SAME account as "index" (both normalize to
|
||||
// "index") — and still NOT on the reserved index account.
|
||||
const userAtIndex = await ensureAccount("@index");
|
||||
expect(userAtIndex.docPublic).toBe(userIndex.docPublic);
|
||||
expect(userAtIndex.docPublic).not.toBe(indexRecord.docPublic);
|
||||
});
|
||||
|
||||
test("watchIndex fires immediately then when a submission arrives", async () => {
|
||||
|
||||
Reference in New Issue
Block a user