source: Klonkt/test/ssrf-allowlist.test.js@ 29eb745

main
Last change on this file since 29eb745 was 29eb745, checked in by roboburr <roboburr@…>, 4 weeks ago

Doorslaggevend schuift mee, en de daemons hoeven geen TLS

Drie dingen van Bart (8-8).

  1. DOORSLAGGEVEND IS EEN STAND, GEEN MOMENT -- een fout in wat ik een uur eerder

bouwde. Ik berekende shaer:decisive een keer bij het doorsturen en bevroor het.
Bij vijf guardians staat er dan "je beslist niets", en zodra er een ja bij komt IS
elk van de anderen de doorslag. Precies de stille kant: het scherm zwijgt op het
moment dat het moet spreken. Na elke stem die het besluit open laat krijgen de
overgeblevenen nu een herziene stand, en alleen zij.

  1. DE VOLGPOORT MAG UITGEROLD (Barts akkoord). Zit in deze reeks.
  1. GEEN TLS VOOR DE TESTKUDDE. Barts vraag: waarom via https, gewoon localhost

::1. Terecht -- een certificaat en een publieke hostname toevoegen aan een doos
die ook andermans sites draait, om honderd neptestkinderen te bereiken, is de
verkeerde prijs.

Daarvoor mag de SSRF-poort gericht open: AP_ALLOW_HOSTS, een lijst van precieze
host:poort-paren. Geen "loopback is oke"-vlag. [::1]:3060 opent niet 127.0.0.1,
niet 3061, en niets in het interne netwerk -- want een brede vlag zet de
bescherming uit in een dev-omgeving, en dev-omgevingen worden productie. Leeg
tenzij een beheerder hem vult, en dat is de stand van elke productie-instance.

En het toetst iets dat we toch moesten kunnen: http MET HTTP-signatures, zonder
TLS. Een echt federatiegeval, tot nu toe nergens uitgeprobeerd.

ONDERWEG EEN LATENTE FOUT GEVONDEN. URL.hostname geeft een IPv6-literal met
blokhaken, en net.isIP herkent die vorm niet -- elk IPv6-adres viel door naar de
DNS-tak en strandde daar op ENOTFOUND. Geweigerd, maar per ongeluk en met de
verkeerde reden; isBlockedIp strippte die haken al en verwachtte dus input die
nooit aankwam. Nu wordt een IPv6-literal echt gecontroleerd.

EN EEN SLAAGTEST, want die ontbrak. Al mijn SSRF-toetsen controleerden alleen
weigeringen: de lijst helemaal negeren gaf nul rode toetsen, en de farm zou
stilletjes nooit werken. Nu bewijst er een dat wat op de lijst staat er ook
doorheen komt.

Suite 755/755. Drie mutaties gecontroleerd.

  • Property mode set to 100644
File size: 2.7 KB
Line 
1// De SSRF-poort mag gericht open voor een testkudde op de eigen machine
2// (shaer-6wt, Barts vraag 8-8: "waarom via https? gewoon localhost ::1").
3//
4// Wat hier bewaakt wordt is de SMALHEID. De bescherming bestaat omdat een
5// actor-URI van een vreemde komt; een brede "loopback mag"-vlag zou hem in een
6// dev-omgeving uitzetten, en dev-omgevingen worden productie. Een lijst van
7// precieze host:poort-paren opent precies wat erin staat en niets ernaast.
8//
9// Run: npm test
10import { test } from 'node:test';
11import assert from 'node:assert/strict';
12
13process.env.DATABASE_PATH = ':memory:';
14process.env.AP_ALLOW_HOSTS = '[::1]:3060';
15const AP = await import('../src/services/ActivityPubService.js');
16
17const faalt = async (url) => {
18 try { await AP.safeFetch(url); return null; }
19 catch (e) { return e.message; }
20};
21
22test('wat er WEL op staat komt langs de poort', async () => {
23 // De slaagtest, en zonder hem is de rest waardeloos: alleen weigeringen
24 // toetsen laat de suite groen terwijl er niets doorheen komt. Er draait hier
25 // niets op 3060, dus we verwachten een VERBINDINGSfout -- het bewijs is dat
26 // het geen ssrf-fout is.
27 const fout = await faalt('http://[::1]:3060/u/w001');
28 assert.ok(fout, 'er is wel degelijk een fout, want er luistert niets');
29 assert.ok(!String(fout).startsWith('ssrf-'), `door de poort, maar kreeg: ${fout}`);
30});
31
32test('een adres dat NIET op de lijst staat blijft geweigerd', async () => {
33 // Zelfde machine, andere poort. Zou dit doorlaten, dan is de lijst een vlag.
34 assert.equal(await faalt('http://[::1]:3061/u/w001'), 'ssrf-blocked-ip');
35});
36
37test('en 127.0.0.1 evenmin, ook al is het dezelfde machine', async () => {
38 // De lijst opent een HOST:POORT, geen begrip van "lokaal".
39 assert.equal(await faalt('http://127.0.0.1:3060/u/w001'), 'ssrf-blocked-ip');
40});
41
42test('het metadata-adres blijft dicht', async () => {
43 // Waar de bescherming voor bestaat.
44 assert.equal(await faalt('http://169.254.169.254/latest/meta-data/'), 'ssrf-blocked-ip');
45});
46
47test('zonder de omgevingsvariabele is er geen uitzondering', async () => {
48 // De lijst is leeg tenzij iemand hem expliciet vult. Dit is de stand waarin
49 // elke productie-instance draait.
50 const eerder = process.env.AP_ALLOW_HOSTS;
51 delete process.env.AP_ALLOW_HOSTS;
52 const mod = await import(`../src/services/ActivityPubService.js?leeg=${Date.now()}`);
53 let fout = null;
54 try { await mod.safeFetch('http://[::1]:3060/u/w001'); } catch (e) { fout = e.message; }
55 assert.equal(fout, 'ssrf-blocked-ip');
56 process.env.AP_ALLOW_HOSTS = eerder;
57});
58
59test('een ander schema blijft hoe dan ook geweigerd', async () => {
60 assert.equal(await faalt('file:///etc/passwd'), 'ssrf-bad-scheme');
61});
Note: See TracBrowser for help on using the repository browser.