Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 26b1848308ffb57c42a26f8fb51f9337e6ed001c)
+++ src/services/ActivityPubService.js	(revision fc41e043cd09fae43cf51adae8a1d3c81f00d619)
@@ -2181,12 +2181,19 @@
     // handshake (a ward and a co-guardian may both live here). Gather candidate
     // local slugs from the inbox owner, the `to` list, and the ward.
+    // MET localSlugOf en niet met slugFromActorUrl. Dat laatste knipt alleen de
+    // staart van een pad af, zonder naar de HOST te kijken -- en deze uri's
+    // komen uit `to` en uit de relatie, dus van de afzender. Een Offer gericht
+    // aan https://elders.example/ap/users/dev leverde zo de slug "dev" op, en
+    // die bestaat hier. Dan draait onze dev de afhandeling van een activiteit
+    // die nooit aan hem geadresseerd was. localSlugOf eist dat de uri met onze
+    // eigen basis begint en dat de site echt bestaat.
     const cand = new Set();
     if (slugParam) cand.add(slugParam);
     for (const t of (Array.isArray(act.to) ? act.to : (act.to ? [act.to] : []))) {
-      if (typeof t === 'string') { const s = slugFromActorUrl(t); if (s) cand.add(s); }
+      if (typeof t === 'string') { const s = localSlugOf(t); if (s) cand.add(s); }
     }
     if (type === 'Offer' || type === 'Undo') {
       const rel = type === 'Undo' ? Guardianship.parseUndoRelationship(act) : Guardianship.parseRelationship(act.object);
-      if (rel) { const s = slugFromActorUrl(rel.ward); if (s) cand.add(s); }
+      if (rel) { const s = localSlugOf(rel.ward); if (s) cand.add(s); }
     }
     let consumed = false;
@@ -2206,5 +2213,5 @@
     const noteIds = [];
     for (const u of objectUris) {
-      const s = slugFromActorUrl(u);        // one of our actors?
+      const s = localSlugOf(u);             // one of OURS -- host meegewogen
       if (s) { targetSlug = targetSlug || s; continue; }
       const pid = postIdFromNoteUrl(u, base); // one of our notes?
@@ -2234,5 +2241,9 @@
   if (type === 'Follow') {
     const who = typeof act.actor === 'string' ? act.actor : (act.actor && act.actor.id);
-    const slug = slugParam || slugFromActorUrl(typeof act.object === 'string' ? act.object : (act.object && act.object.id));
+    // slugParam is de eigenaar van een per-actor inbox; op de GEDEELDE inbox is
+    // die er niet en werd de slug uit act.object geraden. Zonder hostcontrole
+    // kon een Follow op andermans actor met dezelfde padstaart hier een volger
+    // opleveren.
+    const slug = slugParam || localSlugOf(typeof act.object === 'string' ? act.object : (act.object && act.object.id));
     if (!who || !slug) return 400;
     const remote = await fetchActor(who);
@@ -6364,5 +6375,5 @@
 // Which local site (if any) hosts this actor URI — used by the handshake to
 // apply the local side of a commit and to derive a ward's existing guardians.
-function localSlugOf(actorUri) {
+export function localSlugOf(actorUri) {
   const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
   if (!actorUri || !actorUri.startsWith(`${base}/ap/users/`)) return null;
Index: test/inbox-vreemde-slug.test.js
===================================================================
--- test/inbox-vreemde-slug.test.js	(revision fc41e043cd09fae43cf51adae8a1d3c81f00d619)
+++ test/inbox-vreemde-slug.test.js	(revision fc41e043cd09fae43cf51adae8a1d3c81f00d619)
@@ -0,0 +1,41 @@
+// Een slug is een LOKALE sleutel en mag niet uit een vreemde URI komen.
+//
+// Robins vraag (11-8): waar communiceren we nog op <slug>, want op een
+// AP-oppervlak mag dat niet. In de inbox werd hij geraden met
+// slugFromActorUrl -- die knipt de staart van een pad af zonder naar de host te
+// kijken. De uri's komen daar uit `to`, uit de relatie en uit act.object, dus
+// van de AFZENDER. Een activiteit gericht aan andermans actor met dezelfde
+// padstaart als een van onze sites kwam zo bij ONZE site terecht.
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+
+process.env.DATABASE_PATH = ':memory:';
+process.env.PUBLIC_BASE_URL = 'https://ons.test';
+
+const dbMod = await import('../src/config/database.js');
+const db = dbMod.default;
+dbMod.initializeDatabase();
+const AP = await import('../src/services/ActivityPubService.js');
+
+db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
+  .run('u1', 'u1', 'u1@t', 'x', 'god');
+db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s1', 'dev', 'Dev', 'u1');
+
+test('een vreemde actor met onze padstaart levert GEEN lokale slug', () => {
+  // Dit is de aanval in een regel: elders heet ook iemand /ap/users/dev.
+  assert.equal(AP.localSlugOf('https://elders.example/ap/users/dev'), null);
+});
+
+test('onze eigen actor levert hem wel', () => {
+  assert.equal(AP.localSlugOf('https://ons.test/ap/users/dev'), 'dev');
+});
+
+test('en een site die niet bestaat ook niet, ook al is de host de onze', () => {
+  // De staart is niet genoeg: hij moet ook echt een site zijn.
+  assert.equal(AP.localSlugOf('https://ons.test/ap/users/bestaatniet'), null);
+});
+
+test('een pad dat er alleen op lijkt telt niet', () => {
+  assert.equal(AP.localSlugOf('https://ons.test/elders/ap/users/dev'), null);
+  assert.equal(AP.localSlugOf('https://kwaad.test/ons.test/ap/users/dev'), null);
+});
