fix(sikkerhed): CalDAV lod en viewer skrive, og Web Push kunne ringe indad
Release / Build and publish image (push) Failing after 10m0s

To huller fundet ved en gennemgang efter dagens arbejde. Begge har en prøve, der
er set fejle før rettelsen.

CalDAV PUT afgjorde adgang på det projekt, klienten *nævnte i adressen*, og tog
derefter opgavens id fra den samme adresse. Intet bandt de to sammen. En viewer
på et delt projekt kunne pege på et projekt, de selv ejer — indbakken, som alle
har og alle må redigere — og lægge den delte opgaves id i filnavnet. Projekt-
tjekket gik godt på indbakken, og skrivningen landede på en opgave, de kun må
læse. GetTask lukkede det ikke: den svarer "må denne person se det", og det må en
viewer. `CanEdit` er spørgsmålet, og det er det samme, handleUpdateTask altid har
stillet — API-døren har spurgt hele tiden, CalDAV-døren spurgte ikke.

Opgaven skal nu også ligge, hvor adressen siger. Uden det kan en redaktør på to
projekter nå det ene gennem det andets adresse; intet eskaleres af det i sig
selv, men en adresse, der har lov at lyve om hvor noget bor, er sådan sagen
ovenfor kom ind.

Web Push blev leveret med en almindelig http.Client. Adressen kommer fra
browserens abonnement — altså fra den, der er logget ind — og serveren POSTer til
den på et skema, de selv vælger: sæt en påmindelse et minut ude, og jobbet går
igen hvert minut. Tilmeldingen afviser en privat adresse, og den kontrol er ikke
nok alene: den kører på URL'en som skrevet, og et navn, den godkender, kan svare
127.0.0.1 næste gang det slås op. Det er præcis den sætning, `internal/safedial`
selv står og advarer om. Leveringen går gennem safedial nu, som tjekker den
opslåede adresse ved forbindelsen og hvert viderestillingshop på samme måde.
Kontrollen ved tilmelding bliver, fordi den siger nej, mens nogen kigger på
skærmen.

Og en hærdning: /inbound/mail har fået et loft. Det er den svagere af de to
offentlige skriveveje — beaconet skriver en række for en fremmed, denne skriver
ingenting uden en 256-bit adresse, man skal kende i forvejen — men adressen deles
med mailservere og står på en indstillingsside, og når den først er kendt, var der
intet loft på, hvad der kunne laves med den. Tallet er sat generøst: modtageren er
mailserveren, én adresse der leverer det hele.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kristian Wind
2026-08-20 15:15:28 +02:00
co-authored by Claude Opus 5
parent 43f8c0ea85
commit cab4e703f5
5 changed files with 233 additions and 2 deletions
+15 -1
View File
@@ -32,6 +32,8 @@ import (
"net/url"
"strings"
"time"
"github.com/kristianwind/verdande/internal/safedial"
)
type Subscription struct {
@@ -101,7 +103,19 @@ func Send(ctx context.Context, sub Subscription, payload Payload, vapid VAPID) e
req.Header.Set("Urgency", "normal")
req.Header.Set("Authorization", "vapid t="+token+", k="+vapid.Public)
resp, err := (&http.Client{Timeout: 15 * time.Second}).Do(req)
// safedial, not a plain client. The endpoint comes from the browser's
// subscription, which is to say from whoever is signed in, and this server
// POSTs to it on a schedule they choose — set a reminder a minute out and the
// job goes again every minute.
//
// The subscribe handler already refuses a private address, and that check is
// not enough on its own: it runs on the URL as written, and a name it approves
// can answer 127.0.0.1 the next time it is looked up. safedial checks the
// *resolved* address at connect time, which is the only moment the address
// that will actually be used is known — and it checks every redirect hop the
// same way. The parse-time check stays, because it says no while somebody is
// looking at the screen.
resp, err := safedial.Client(15 * time.Second).Do(req)
if err != nil {
return err
}