Changeset cec72bc in Klonkt for test/forwarded-activities.test.js


Ignore:
Timestamp:
08/06/2026 11:18:34 AM (5 weeks ago)
Author:
Claude (agent) <aiclaude@…>
Branches:
main
Children:
f4f0a6d
Parents:
434ba41
git-author:
Robin <roboburr@…> (08/06/2026 11:18:33 AM)
git-committer:
Claude (agent) <aiclaude@…> (08/06/2026 11:18:34 AM)
Message:

Dereference smaller, onbetekend eerst, en mislukkingen onthouden (shaer-drf)

De twee veiligheidsvervolgstappen die in de notities van twee GESLOTEN beads
stonden en daarmee onvindbaar waren. Eerst als eigen bead gezet, nu uitgevoerd.

  1. ALLEEN BIJ EEN OUDER DIE WE KENNEN. dereferenceForwarded haalde een object op zodra een geldig ondertekende activiteit een andere auteur claimde dan de ondertekenaar. Maar claimedActor en object.id zijn allebei door de aanvaller gekozen, en het host-anker eist alleen dat ze aan ELKAAR gelijk zijn -- dus kon iedereen met een werkende fediverse-actor ons naar elke https-URL sturen. Nu moet inReplyTo wijzen naar iets van ons: een eigen post, een eigen outbox-antwoord, een rij in ap_timeline of een bekende reactie in ap_interactions. Doorsturen bestaat juist omdát wij in de thread zitten, dus dat kost geen legitiem verkeer waarvan we de ouder kennen. Wat wel afvalt wordt apart gelogd (skipped: unknown inReplyTo), zodat zichtbaar is of de vernauwing echte inhoud kost.
  1. ONBETEKEND EERST. Die GET werd nog ondertekend namens ons. efe5633 haalde dat weg bij de sleutel-ophaal in fetchActor maar niet hier, dus de attributie-blootstelling die ik dacht te hebben weggenomen bestond op dit pad nog. Nu onbetekend, en alleen tekenen als dat niet volstaat.
  1. MISLUKKINGEN ONTHOUDEN. Mastodon herhaalt dagenlang; elke herhaling deed de fetch opnieuw. Een negatieve cache van een half uur, begrensd op 500 items.

Vier tests erbij. Een ervan viel eerst om doordat een eerdere test dezelfde note
had laten mislukken en de cache hem dus oversloeg -- dat was geen fout maar het
bewijs dat de cache werkt; de test heeft nu een eigen id, met die reden erbij.

Suite 458/458.

Co-Authored-By: Claude Opus 5 <noreply@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • test/forwarded-activities.test.js

    r434ba41 rcec72bc  
    3636let opgehaald = [];
    3737const echteFetch = globalThis.fetch;
    38 globalThis.fetch = async (url, opts) => {
     38globalThis.fetch = async (url, opts = {}) => {
    3939  const u = String(url);
    40   opgehaald.push(u);
     40  opgehaald.push({ url: u, ondertekend: !!(opts.headers && (opts.headers.Signature || opts.headers.signature)) });
    4141  if (u === NOTE_ID && bron) {
    4242    return new Response(JSON.stringify(bron), { status: 200, headers: { 'content-type': 'application/activity+json' } });
     
    4444  return new Response('not found', { status: 404 });
    4545};
     46
     47// Een note die WIJ kennen: doorsturen gebeurt omdat wij in de thread zitten, en
     48// sinds shaer-drf dereferencen we alleen als inReplyTo daarheen wijst.
     49const ONZE_NOTE = 'https://203.0.113.10/notes/van-ons';
     50db.prepare(`INSERT OR IGNORE INTO ap_timeline (id, slug, author_uri, author_name, content, created_at)
     51            VALUES (?,?,?,?,?,?)`).run(ONZE_NOTE, 'me', AUTEUR, 'Anna', '<p>x</p>', '2026-08-06 09:00:00');
    4652
    4753/** Een doorgestuurde Create: ondertekend door de doorstuurder, geschreven door de auteur. */
     
    5157    actor: AUTEUR,
    5258    object: objectOverride !== undefined ? objectOverride : {
    53       id: NOTE_ID, type: 'Note', attributedTo: AUTEUR,
     59      id: NOTE_ID, type: 'Note', attributedTo: AUTEUR, inReplyTo: ONZE_NOTE,
    5460      content: '<p>BEZORGDE versie</p>', to: ['https://www.w3.org/ns/activitystreams#Public'],
    5561    },
     
    6571  const status = await AP.handleInbox(req(act), 'me', alsDoorstuurder);
    6672  assert.notEqual(status, 401, 'een doorgestuurde reactie hoort niet meer geweigerd te worden');
    67   assert.ok(opgehaald.includes(NOTE_ID), 'het object hoort bij de bron opgehaald te zijn');
     73  assert.ok(opgehaald.some((v) => v.url === NOTE_ID), 'het object hoort bij de bron opgehaald te zijn');
    6874  // Wat telt: de OPGEHAALDE inhoud wordt gebruikt, niet wat de doorstuurder gaf.
    6975  assert.match(act.object.content, /ECHTE versie/);
     
    7581  // is de waarheid; de payload wordt weggegooid.
    7682  bron = { id: NOTE_ID, type: 'Note', attributedTo: AUTEUR, content: '<p>ECHTE versie</p>' };
    77   const act = doorgestuurd({ id: NOTE_ID, type: 'Note', attributedTo: AUTEUR, content: '<p>KOOP MIJN MUNTEN</p>' });
     83  const act = doorgestuurd({ id: NOTE_ID, type: 'Note', attributedTo: AUTEUR, inReplyTo: ONZE_NOTE, content: '<p>KOOP MIJN MUNTEN</p>' });
    7884  await AP.handleInbox(req(act), 'me', alsDoorstuurder);
    7985  assert.doesNotMatch(act.object.content, /MUNTEN/);
     
    8389  // Zonder deze ankereis wijst een doorsturer je naar een host die hij zelf
    8490  // beheert, waar attributedTo alles kan beweren.
    85   bron = { id: 'https://203.0.113.66/notes/1', type: 'Note', attributedTo: AUTEUR, content: '<p>x</p>' };
    86   const act = doorgestuurd({ id: 'https://203.0.113.66/notes/1', type: 'Note', attributedTo: AUTEUR, content: '<p>x</p>' });
     91  bron = { id: 'https://203.0.113.66/notes/1', type: 'Note', attributedTo: AUTEUR, inReplyTo: ONZE_NOTE, content: '<p>x</p>' };
     92  const act = doorgestuurd({ id: 'https://203.0.113.66/notes/1', type: 'Note', attributedTo: AUTEUR, inReplyTo: ONZE_NOTE, content: '<p>x</p>' });
    8793  const status = await AP.handleInbox(req(act), 'me', alsDoorstuurder);
    8894  assert.equal(status, 401);
     
    116122  const status = await AP.handleInbox({ body: doorgestuurd(), headers: {}, ip: '203.0.113.9' }, 'me', null);
    117123  assert.equal(status, 401);
    118   assert.equal(opgehaald.includes(NOTE_ID), false, 'er hoort niet eens opgehaald te worden');
     124  assert.equal(opgehaald.some((v) => v.url === NOTE_ID), false, 'er hoort niet eens opgehaald te worden');
    119125});
    120126
     
    127133});
    128134
     135test('een antwoord op iets dat we NIET kennen wordt niet opgehaald', async () => {
     136  // De vernauwing uit shaer-drf. Zonder deze eis zijn claimedActor en object.id
     137  // allebei door de aanvaller gekozen en eist het host-anker alleen dat ze aan
     138  // elkaar gelijk zijn -- dan kan iedereen met een werkende actor ons naar elke
     139  // URL sturen.
     140  bron = { id: NOTE_ID, type: 'Note', attributedTo: AUTEUR, content: '<p>x</p>' };
     141  opgehaald = [];
     142  const act = doorgestuurd({
     143    id: NOTE_ID, type: 'Note', attributedTo: AUTEUR,
     144    inReplyTo: 'https://203.0.113.10/notes/kennen-we-niet', content: '<p>x</p>',
     145  });
     146  const status = await AP.handleInbox(req(act), 'me', alsDoorstuurder);
     147  assert.equal(status, 401);
     148  assert.equal(opgehaald.some((v) => v.url === NOTE_ID), false, 'er hoort niet eens opgehaald te worden');
     149});
     150
     151test('zonder inReplyTo wordt er niets opgehaald', async () => {
     152  bron = { id: NOTE_ID, type: 'Note', attributedTo: AUTEUR, content: '<p>x</p>' };
     153  opgehaald = [];
     154  const act = doorgestuurd({ id: NOTE_ID, type: 'Note', attributedTo: AUTEUR, content: '<p>x</p>' });
     155  assert.equal(await AP.handleInbox(req(act), 'me', alsDoorstuurder), 401);
     156  assert.equal(opgehaald.some((v) => v.url === NOTE_ID), false);
     157});
     158
     159test('de dereference haalt ONBETEKEND op als dat volstaat', async () => {
     160  // Anders kan een ander ons een ondertekend verzoek naar een adres van zijn
     161  // keuze laten sturen, met onze identiteit eronder.
     162  //
     163  // Eigen id: NOTE_ID is in een eerdere test mislukt en zit dus in de negatieve
     164  // cache -- die zou deze poging overslaan. Dat de test daarop stukliep, is het
     165  // bewijs dat de cache doet wat hij moet.
     166  const VERS = 'https://203.0.113.10/notes/vers';
     167  const stubOrig = globalThis.fetch;
     168  globalThis.fetch = async (url, opts = {}) => {
     169    if (String(url) === VERS) {
     170      opgehaald.push({ url: VERS, ondertekend: !!(opts.headers && (opts.headers.Signature || opts.headers.signature)) });
     171      return new Response(JSON.stringify({ id: VERS, type: 'Note', attributedTo: AUTEUR, content: '<p>x</p>' }),
     172        { status: 200, headers: { 'content-type': 'application/activity+json' } });
     173    }
     174    return stubOrig(url, opts);
     175  };
     176  opgehaald = [];
     177  const act = doorgestuurd({ id: VERS, type: 'Note', attributedTo: AUTEUR, inReplyTo: ONZE_NOTE, content: '<p>y</p>' });
     178  const status = await AP.handleInbox(req(act), 'me', alsDoorstuurder);
     179  globalThis.fetch = stubOrig;
     180  assert.notEqual(status, 401);
     181  const pogingen = opgehaald.filter((v) => v.url === VERS);
     182  assert.equal(pogingen.length, 1, 'één poging');
     183  assert.equal(pogingen[0].ondertekend, false, 'en die was onbetekend');
     184});
     185
     186test('een mislukte poging wordt onthouden, zodat een retry hem niet herhaalt', async () => {
     187  // Mastodon herhaalt dagenlang; zonder cache doet elke herhaling de fetch
     188  // opnieuw. De tweede bezorging hoort geen tweede fetch op te leveren.
     189  const MIS = 'https://203.0.113.10/notes/mislukt';
     190  bron = null;   // de stub geeft 404
     191  const act = () => doorgestuurd({ id: MIS, type: 'Note', attributedTo: AUTEUR, inReplyTo: ONZE_NOTE, content: '<p>x</p>' });
     192  opgehaald = [];
     193  assert.equal(await AP.handleInbox(req(act()), 'me', alsDoorstuurder), 401);
     194  const na1 = opgehaald.filter((v) => v.url === MIS).length;
     195  assert.ok(na1 >= 1, 'de eerste poging haalt wel op');
     196  opgehaald = [];
     197  assert.equal(await AP.handleInbox(req(act()), 'me', alsDoorstuurder), 401);
     198  assert.equal(opgehaald.filter((v) => v.url === MIS).length, 0, 'de tweede niet meer');
     199});
     200
    129201test.after(() => { globalThis.fetch = echteFetch; });
Note: See TracChangeset for help on using the changeset viewer.