Changeset b5a0268c in Klonkt for src/services/ap-inbox.js


Ignore:
Timestamp:
09/04/2026 11:07:47 AM (4 days ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
59f404a
Parents:
9041d35
Message:

De negatieve dereference-cache is geen censuurknop meer (shaer-qawr)

De cache onthield elke mislukte dereference op de note-URI alleen, en een
van de twee manieren om te mislukken is volledig door een vreemde te
sturen. Neem de echte note-URI van je slachtoffer, zet je eigen actor op
DEZELFDE host als beweerde afzender en wijs met inReplyTo naar een van
onze publieke notes: de fetch bij de bron slaagt, attributedTo is het
slachtoffer en niet jij, en die note-URI stond dertig minuten op de
zwarte lijst. Het echte doorgestuurde antwoord liep daar daarna op stuk.
Elke dertig minuten herhalen gaf onbeperkte, gerichte onderdrukking van
een specifiek antwoord, voor een verzoek per keer.

De sleutel is nu de hele bescherming, want de twee soorten mislukking
zeggen iets verschillends. Een TRANSPORTFOUT is een eigenschap van de
note zelf en geldt voor iedereen die hem doorstuurt: sleutel is de
note. Een attributedTo-MISMATCH zegt iets over DEZE doorstuurder:
sleutel is (note, beweerde actor). De leugenaar remt daarmee alleen
zichzelf af, en dat was de rem die de cache moest zijn.

Tweede punt uit dezelfde bead: query en fragment tellen niet meer mee in
de sleutel. Met een kale URL waren ?x=1 en ?x=2 losse ingangen, dus
varieren kostte niets en er was helemaal geen rem -- en met vijfhonderd
losse sleutels waren de echte ingangen eruit te duwen.

Bewust NIET gedaan: een rem per HOST. Wie een handvol niet-bestaande
URL's op een host laat mislukken zou daarmee die hele host het zwijgen
opleggen, en dat is een grotere versie van precies de fout die hier
gerepareerd wordt. Dat staat als waarschuwing bij de code.

Drie toetsen, elk met eigen tegenbewijs: met de sleutel terug op de note
alleen valt de censuurtoets en blijven de andere twee groen, en met de
URL-normalisatie eruit valt alleen de remtoets. Volle suite 1237.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/ap-inbox.js

    r9041d35 rb5a0268c  
    142142// dagenlang; zonder dit doet elke herhaling de fetch opnieuw, ook als die de
    143143// vorige twintig keer niets opleverde. Dempt meteen de scherpte van misbruik.
     144//
     145// DE SLEUTEL IS DE HELE BESCHERMING (shaer-qawr). Er zijn twee soorten
     146// mislukking en ze zeggen iets heel verschillends:
     147//
     148//   TRANSPORTFOUT -- de note is niet op te halen. Dat is een eigenschap van de
     149//   note zelf en geldt voor iedereen die hem doorstuurt, dus de objId alleen is
     150//   de goede sleutel.
     151//
     152//   attributedTo-MISMATCH -- de bron zegt dat iemand ANDERS de auteur is. Dat
     153//   zegt alles over de doorstuurder en niets over de note, dus die onthouden we
     154//   per (note, beweerde actor).
     155//
     156// Met een enkele sleutel voor allebei was dit een censuurknop: neem de echte
     157// note-URI van je slachtoffer, zet er je eigen actor op dezelfde host bij en
     158// wijs naar een van onze publieke notes. De fetch slaagt, de mismatch volgt, en
     159// die note-URI stond dertig minuten op de zwarte lijst -- waarna het ECHTE
     160// doorgestuurde antwoord erop stukliep. Elke dertig minuten herhalen gaf
     161// onbeperkte, gerichte onderdrukking van een specifiek antwoord, voor een
     162// verzoek per keer. Nu raakt de leugenaar alleen zijn eigen ingang.
     163//
     164// Query en fragment tellen niet mee. Ze horen zelden bij de identiteit van een
     165// note, en met een kale URL als sleutel waren ?x=1, ?x=2 enzovoort losse
     166// ingangen: dan is de rem geen rem, want varieren kost niets. Zelfde reden dat
     167// de host in kleine letters gaat.
     168//
     169// GEEN rem per HOST, hoe verleidelijk ook: wie een handvol niet-bestaande
     170// URL's op een host laat mislukken zou daarmee die HELE host het zwijgen
     171// opleggen. Dat is een grotere versie van precies de fout die hier gerepareerd
     172// wordt.
    144173const _derefMiss = new Map();
    145174const DEREF_MISS_MS = 30 * 60 * 1000;
    146 function derefRecentlyFailed(uri) {
    147   const t = _derefMiss.get(uri);
    148   if (t === undefined) return false;
    149   if (Date.now() - t > DEREF_MISS_MS) { _derefMiss.delete(uri); return false; }
    150   return true;
     175const derefKey = (uri, claimedActor) => {
     176  let basis = String(uri || '');
     177  try { const u = new URL(basis); basis = `${u.protocol}//${u.host.toLowerCase()}${u.pathname}`; }
     178  catch { /* onparseerbaar: de kale string is dan de sleutel */ }
     179  // Een NUL-teken als scheiding, als escape geschreven en niet als byte: het
     180  // kan in geen enkele URL staan, dus een actor-sleutel is nooit per ongeluk
     181  // als note-sleutel te lezen.
     182  return claimedActor ? `${basis}\u0000${claimedActor}` : basis;
     183};
     184function derefRecentlyFailed(uri, claimedActor) {
     185  for (const k of [derefKey(uri), derefKey(uri, claimedActor)]) {
     186    const t = _derefMiss.get(k);
     187    if (t === undefined) continue;
     188    if (Date.now() - t > DEREF_MISS_MS) { _derefMiss.delete(k); continue; }
     189    return true;
     190  }
     191  return false;
    151192}
    152 function noteDerefFailure(uri) {
     193function noteDerefFailure(uri, claimedActor) {
    153194  if (_derefMiss.size > 500) {   // simpele begrenzing: oudste helft eruit
    154195    const oud = [..._derefMiss.entries()].sort((a, b) => a[1] - b[1]).slice(0, 250);
    155196    for (const [k] of oud) _derefMiss.delete(k);
    156197  }
    157   _derefMiss.set(uri, Date.now());
     198  _derefMiss.set(derefKey(uri, claimedActor), Date.now());
    158199}
     200// Alleen voor de toets: de aanval speelt zich af in deze twee functies, en de
     201// weg erheen (dereferenceForwarded) eist https en een echte fetch. De
     202// dienstlaag exporteert ze niet, dus het uitvoeroppervlak blijft gelijk.
     203export const _derefCacheForTests = { derefRecentlyFailed, noteDerefFailure };
    159204
    160205async function dereferenceForwarded(act, claimedActor, type, slugParam) {
     
    184229    : null;
    185230  if (!knownNoteUri(parent)) return skipped('unknown inReplyTo', parent || '(none)');
    186   if (derefRecentlyFailed(objId)) return skipped('recent failure', objId);
     231  if (derefRecentlyFailed(objId, claimedActor)) return skipped('recent failure', objId);
    187232  // Onbetekend eerst; tekenen alleen als terugval. Anders kan een ander ons een
    188233  // ONDERTEKEND verzoek naar een adres van zijn keuze laten sturen -- dezelfde
     
    207252  if (attributed !== claimedActor) {
    208253    // Not a transport hiccup: the source itself says someone else wrote this.
    209     noteDerefFailure(objId);
     254    // Per (note, beweerde actor), nooit op de note alleen: dit zegt iets over
     255    // DEZE doorstuurder, en op de note alleen was het een censuurknop op de
     256    // note van een ander (shaer-qawr).
     257    noteDerefFailure(objId, claimedActor);
    210258    return skipped('attributedTo mismatch', `${objId} claims ${attributed || '(none)'}`);
    211259  }
Note: See TracChangeset for help on using the changeset viewer.