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
  • src/services/ActivityPubService.js

    r434ba41 rcec72bc  
    16871687 *     geclaimde actor, dan blijft het een weigering. Geen twijfelgeval opslaan.
    16881688 */
     1689/** Kennen we deze note? Een eigen post, een eigen outbox-antwoord, een
     1690 *  gecachete post in de tijdlijn, of een reactie die al in een thread van ons
     1691 *  staat. Alle vier zijn een geldige reden dat iemand ons een antwoord daarop
     1692 *  doorstuurt; iets anders is dat niet. */
     1693function knownNoteUri(uri) {
     1694  if (!uri || typeof uri !== 'string') return false;
     1695  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
     1696  try {
     1697    if (base && uri.startsWith(`${base}/ap/notes/`)) {
     1698      const seg = decodeURIComponent(uri.slice(`${base}/ap/notes/`.length).split(/[?#]/)[0]);
     1699      if (db.prepare('SELECT 1 FROM ap_outbox WHERE id = ?').get(seg)) return true;
     1700      if (db.prepare('SELECT 1 FROM posts WHERE id = ?').get(seg)) return true;
     1701    }
     1702    if (db.prepare('SELECT 1 FROM ap_timeline WHERE id = ? LIMIT 1').get(uri)) return true;
     1703    if (db.prepare('SELECT 1 FROM ap_interactions WHERE object_uri = ? LIMIT 1').get(uri)) return true;
     1704  } catch { /* bij twijfel niet ophalen */ }
     1705  return false;
     1706}
     1707
     1708// Mislukte dereferences kort onthouden. Mastodon herhaalt een bezorging
     1709// dagenlang; zonder dit doet elke herhaling de fetch opnieuw, ook als die de
     1710// vorige twintig keer niets opleverde. Dempt meteen de scherpte van misbruik.
     1711const _derefMiss = new Map();
     1712const DEREF_MISS_MS = 30 * 60 * 1000;
     1713function derefRecentlyFailed(uri) {
     1714  const t = _derefMiss.get(uri);
     1715  if (t === undefined) return false;
     1716  if (Date.now() - t > DEREF_MISS_MS) { _derefMiss.delete(uri); return false; }
     1717  return true;
     1718}
     1719function noteDerefFailure(uri) {
     1720  if (_derefMiss.size > 500) {   // simpele begrenzing: oudste helft eruit
     1721    const oud = [..._derefMiss.entries()].sort((a, b) => a[1] - b[1]).slice(0, 250);
     1722    for (const [k] of oud) _derefMiss.delete(k);
     1723  }
     1724  _derefMiss.set(uri, Date.now());
     1725}
     1726
    16891727async function dereferenceForwarded(act, claimedActor, type, slugParam) {
    16901728  if (type !== 'Create' && type !== 'Update') return null;
     
    16951733    if (new URL(objId).host !== new URL(claimedActor).host) return null;   // ankereis
    16961734  } catch { return null; }
    1697   // Ondertekend als de ontvangende site wanneer we die kennen; anders anoniem,
    1698   // wat voor een publieke note volstaat. Beide gaan door safeFetch.
    1699   const fetched = await signedGetJson(slugParam, objId).catch(() => null);
    1700   if (!fetched || fetched.id !== objId) return null;   // ook: geen omleiding naar iets anders
    1701   const attributed = typeof fetched.attributedTo === 'string'
     1735  // Alleen dereferencen als het object beweert een antwoord te zijn op iets van
     1736  // ONS (shaer-drf). Zonder die eis zijn claimedActor en object.id allebei door
     1737  // de aanvaller gekozen en eist het host-anker alleen dat ze aan elkaar gelijk
     1738  // zijn -- dan kan iedereen met een werkende actor ons naar elke URL sturen.
     1739  // Doorsturen bestaat juist omdát wij in de thread zitten, dus deze eis kost
     1740  // niets aan legitiem verkeer waarvan we de ouder kennen.
     1741  const parent = typeof o === 'object' && o
     1742    ? (typeof o.inReplyTo === 'string' ? o.inReplyTo : (o.inReplyTo && o.inReplyTo.id))
     1743    : null;
     1744  if (!knownNoteUri(parent)) {
     1745    console.log('[AP] inbox forwarded, skipped (unknown inReplyTo):', claimedActor, parent || '(none)');
     1746    return null;
     1747  }
     1748  if (derefRecentlyFailed(objId)) return null;
     1749  // Onbetekend eerst; tekenen alleen als terugval. Anders kan een ander ons een
     1750  // ONDERTEKEND verzoek naar een adres van zijn keuze laten sturen -- dezelfde
     1751  // reden als bij fetchActor sinds efe5633.
     1752  let fetched = await apGetJson(objId).catch(() => null);
     1753  if ((!fetched || fetched.id !== objId) && slugParam) {
     1754    fetched = await signedGetJson(slugParam, objId).catch(() => null);
     1755  }
     1756  const attributed = fetched && (typeof fetched.attributedTo === 'string'
    17021757    ? fetched.attributedTo
    1703     : (fetched.attributedTo && fetched.attributedTo.id);
    1704   if (attributed !== claimedActor) return null;
     1758    : (fetched.attributedTo && fetched.attributedTo.id));
     1759  if (!fetched || fetched.id !== objId || attributed !== claimedActor) {
     1760    noteDerefFailure(objId);
     1761    return null;
     1762  }
    17051763  return fetched;
    17061764}
Note: See TracChangeset for help on using the changeset viewer.