Release / Build and publish image (push) Failing after 9m54s
**H2 — SSRF gennem AI-udbyderens adresse.** `base_url` blev gemt uden kontrol og hentet med en almindelig klient, og udbyderens svar blev givet tilbage ordret. Det var en fungerende skanner af det interne net for enhver bruger: peg den på en vært, læs de første tre hundrede bytes af det, der svarede. På en homelab er det det meste af, hvad der er værd at nå. Nu gennem `safedial`, og svarets krop går i loggen frem for i svaret. Operatøren har stadig brug for beskeden — for en lokal model er den som regel selve problemet — så den er der, bare kun for den, der kører instansen. **M2 — Web Push havde tre.** Endepunktet blev gemt ukontrolleret og POSTet til på en tidsplan, den, der ringer, selv vælger: sæt en påmindelse et minut ude, og serveren henter adressen, hvert minut. Upserten flyttede desuden ejerskabet med `user_id = excluded.user_id`, så enhver, der kendte en andens endepunkt, kunne overtage det — offerets telefon ville få angriberens beskeder og ingen af sine egne. Og `DeletePushSubscription` havde ingen ejer-kontrol. **H3 — tre veje uden loft.** FTS-udtrykket byggede omkring halvtreds bytes pr. ord uden grænse; en søgning på en kvart megabyte blev til to megabyte MATCH-udtryk og seksogtyve sekunder inde i SQLite, mens den holdt på én af kun fire til otte forbindelser. Nogle få af dem samtidig, og databasen er utilgængelig for alle på instansen, logins inklusive. `filterquery.Parse` er kvadratisk — halvtreds tusind led er syv sekunders CPU og seksogfyrre gigabyte allokering i én forespørgsel — og havde hverken loft på antal eller på dybde. Og noteimporten læste hver fil i arkivet uden grænse: 64 MB komprimeret kan blive til seksogtres gigabyte skrevet ned på den disk, databasen ligger på. **M1 — `comment_id` blev taget på tro.** Rettighedskontrollen så på opgaven i adressen, mens filen blev hængt på den kommentar, formularen bad om. En viewer på et delt projekt kan liste dets kommentarer, så hun kunne læse et id og lægge en fil under en andens kommentar i et projekt, hun kun må læse. **M4 — fire hemmeligheder lå i klartekst i hver sikkerhedskopi.** TOTP-koden og AI-nøglen forsegles nu; begge konverterer sig selv næste gang de skrives. Kalenderfeedets og mailadressens tokens gør ikke — de slås op på deres præcise værdi og vises i fladen mere end én gang, hvilket en hash ikke kan. Det står nu skrevet i `SECURITY.md`, som påstod det modsatte. At rette det betyder, at adressen kun kan vises én gang, og det er en beslutning om, hvordan funktionen virker. Og en prøve på, at politikken overhovedet bliver sendt. Det var den, der gjorde XSS-en i nat til indsat HTML frem for kørt script, og der var ingen prøve, der rørte den — en foranstaltning, ingen prøve rører, kan forsvinde i en oprydning, uden at nogen opdager det. Målt på script-src alene: `style-src 'unsafe-inline'` står der med vilje, og en indsat stil kan gøre en side grim, ikke køre kode. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
47 lines
2.6 KiB
Markdown
47 lines
2.6 KiB
Markdown
# Security
|
|
|
|
## Reporting
|
|
|
|
Email **kristianwind@gmail.com** rather than opening an issue. Include what you
|
|
found, how to reproduce it, and what an attacker could do with it. You will get a
|
|
reply within a week.
|
|
|
|
## What verdande does about the obvious things
|
|
|
|
- **Passwords** are Argon2id with the parameters recorded in the hash, so they can
|
|
be raised later without locking anyone out.
|
|
- **Session, invite, reset and API tokens** are stored only as a SHA-256 hash. A
|
|
database dump contains nothing that can be presented.
|
|
- **The calendar feed address and the mail-to-task address are not hashed.** They
|
|
are looked up by their exact value, and they are shown in the interface more
|
|
than once, which a hash cannot do. Anybody holding a backup file therefore holds
|
|
those two URLs: the feed reads that person's tasks without a login, and the mail
|
|
address files tasks into their inbox. Rotating either from **Settings →
|
|
Integrations** invalidates the old one. This is written down rather than fixed
|
|
because fixing it means the address can only ever be shown once, which is a
|
|
decision about how the feature works and not only about how it is stored.
|
|
- **Mailbox passwords, OAuth tokens, TOTP secrets and AI keys** are sealed with
|
|
AES-GCM under a key kept outside the database — see `internal/secret`. That is
|
|
what makes a stray copy of the database less than a stray copy of everything:
|
|
the backup does not carry the key.
|
|
- **Sessions** are `httpOnly`, `SameSite=Lax`, and `__Host-` prefixed over HTTPS.
|
|
- **Uploads** are stored content-addressed by hash and served as
|
|
`application/octet-stream` with `nosniff` — with one exception, added so that a
|
|
note can show the pictures in it: a short allowlist of raster image types is
|
|
served inline as its own type. The list is named one format at a time rather
|
|
than matched on an `image/` prefix, because the prefix would let `image/svg+xml`
|
|
through, and an SVG is a document that can run script. `nosniff` stays, so the
|
|
browser must read the file as that image or not at all.
|
|
- **The filter language** compiles to parameterised SQL. No typed text is ever
|
|
concatenated into a statement.
|
|
- **Permission checks** are route middleware, and anything you may not see answers
|
|
404 rather than 403 — a 403 confirms the thing exists.
|
|
- **The CSP** is computed at startup by hashing the inline scripts in the built
|
|
page, rather than allowing `unsafe-inline`.
|
|
|
|
## What it does not do
|
|
|
|
verdande has no rate limiting beyond the login and password-reset endpoints, and
|
|
no audit log of reads. It is built to be run behind something — a Cloudflare
|
|
Tunnel, a reverse proxy, a VPN — by somebody who knows what they exposed.
|