fix: second tour adverse — mes correctifs avaient quatre trous, dont un qui les annulait
J'ai relancé un adversaire sur les correctifs du commit précédent, comme la règle l'exige. Il en a trouvé quatre. Le premier annulait la garde que je venais d'écrire. **Le registre de propriété était écrivable depuis la surface publiée.** `assertMayWrite` lit la propriété dans l'index de store de l'appelant — et `caps.open` marquait les documents de STRUCTURE (les trois stores, les inbox) comme « créés par moi ». Donc un porteur pouvait, par le `docs.sparqlUpdate` publié, insérer `contains "<n'importe quel document>"` dans son propre index et s'en déclarer propriétaire. Démontré : Bob écrit dans le document protégé d'Alice, et détourne l'inbox d'un de ses documents — exactement le vecteur que le commit précédent prétendait fermer. `open` classe désormais sans marquer : un document de structure n'est possédé par personne au sens de la paternité, donc les deux moitiés de la garde répondent non, ce qui est correct. **`inbox.post` acceptait n'importe quel NURI.** Déplacer `depositInto` hors de la surface ne suffisait pas : `post` atteint la même porte, qui saute les deux gardes par conception. Bob, ne détenant rien, écrivait quatre triplets dans le document d'Alice. En amont la confusion est impossible — `InboxPost` scelle vers une CLÉ d'inbox et le broker route par `inboxes: PubKey → RepoId` ; adresser un document n'est pas refusé, c'est inexprimable. Le shim tient maintenant un index des inbox, l'équivalent émulé de ce que le broker sait par construction, et `post` refuse ce qui n'en est pas une. **Le filtre de lecture fuyait encore par les clés dunder.** `DeepSignalSet` expose la collection brute sur `__raw__` / `__meta__` : `[...view]` rendait zéro élément pendant que `view.__raw__` rendait le Set complet, tous utilisateurs confondus. Mon en-tête affirmait qu'« une propriété simple ne porte aucun élément » — faux pour ce type. **Et il cassait des membres légitimes** : ma liste blanche couvrait la moitié des helpers d'itération, si bien que `toArray`, `reduce`, `first`, `take`, `drop`, `flatMap` levaient sur les données du porteur lui-même. Tous filtrés désormais ; le refus ne vaut que pour l'inconnu. **Deux tests réparés à la source plutôt qu'en affaiblissant les gardes.** Le faux `doc_create` de `inbox.test.ts` rendait une CONSTANTE — tous les documents créés étaient le même NURI, donc la garde de propriété n'avait rien à distinguer et deux tests lisaient l'inbox d'Alice sous l'identité de Bob sans que rien ne proteste. Et le harnais e2e utilisait un document ordinaire comme inbox. Enfin, mon propre cache d'inbox a reproduit la faute que la revue avait relevée ailleurs : un mémo qui survit à sa session. Rattaché à `resetRegistryCache`. 189 tests unitaires (six régressions de plus), e2e 40/40 et applicatif 10/10 — après un échec réseau non reproductible, relancé sans modification.
This commit is contained in:
@@ -22,6 +22,7 @@ import { test, expect, mock, afterAll } from "bun:test";
|
||||
import {
|
||||
createEntityDoc,
|
||||
resetRegistryCache,
|
||||
resolveWriteGraph,
|
||||
userInbox,
|
||||
} from "../src/shared-wallet/account-registry";
|
||||
import { documentInboxAddress, openDocumentInbox } from "../src/emulated-verifier/branch-registers";
|
||||
@@ -416,6 +417,44 @@ test("connecting a user that does not exist provisions nothing", async () => {
|
||||
expect(getCaps().isEnforcing()).toBe(false);
|
||||
});
|
||||
|
||||
// REGRESSION (second adversarial pass). `inbox.post` is a published door that skips both
|
||||
// guards by design — the deposit is the one write that legitimately crosses. It accepted
|
||||
// ANY NURI, so it wrote into a document its caller could not even read. Upstream the
|
||||
// confusion cannot arise: a deposit carries an inbox key, not a document reference.
|
||||
test("a deposit is addressed to an inbox, never to a document", async () => {
|
||||
inject();
|
||||
setCurrentUser("alice");
|
||||
const protDoc = await createEntityDoc("alice", "protected");
|
||||
await write(protDoc, SECRET, "alice's own");
|
||||
|
||||
setCurrentUser("bob");
|
||||
await expect(post(protDoc, { payload: { x: 1 }, ts: 1 })).rejects.toThrow(/not an inbox/i);
|
||||
|
||||
setCurrentUser("alice");
|
||||
expect(await readValues([protDoc], SECRET)).toEqual(["alice's own"]); // untouched
|
||||
});
|
||||
|
||||
// REGRESSION (second adversarial pass). The write guard reads ownership from the store
|
||||
// index — and the holder's own store document was marked "created by me", so it was
|
||||
// writable through the PUBLISHED `docs.sparqlUpdate`. One insert into it and you were
|
||||
// the owner of anything you cared to name.
|
||||
test("a holder cannot write into their own store index and forge ownership", async () => {
|
||||
inject();
|
||||
setCurrentUser("alice");
|
||||
const protDoc = await createEntityDoc("alice", "protected");
|
||||
await write(protDoc, SECRET, "alice's own");
|
||||
|
||||
setCurrentUser("bob");
|
||||
await createEntityDoc("bob", "protected"); // bob has his own stores
|
||||
const bobStore = await resolveWriteGraph("bob", "protected");
|
||||
await expect(
|
||||
sparqlUpdate(SESSION.sessionId, `INSERT DATA { <${SHIM}:index> <${SHIM}:contains> "${protDoc}" }`, bobStore, "forge"),
|
||||
).rejects.toThrow(/WRITE cap/i);
|
||||
// …and he is still refused the write itself — here by rule 1 (he cannot even reach
|
||||
// alice's protected document), which fires before the ownership guard. Both say no.
|
||||
await expect(write(protDoc, SECRET, "bob was here")).rejects.toThrow(/refused/i);
|
||||
});
|
||||
|
||||
// WRITING IS OWNERSHIP — the two regressions that replaced the old write guard.
|
||||
//
|
||||
// It used to ask "was this cap served to me by a public store?", which was wrong in both
|
||||
|
||||
Reference in New Issue
Block a user