diff --git a/.env b/.env index 656c0672..706d20f4 100644 --- a/.env +++ b/.env @@ -175,6 +175,19 @@ MICROSOFT_OAUTH_TENANT=common APP_DB_LOG_LEVEL=warning APP_CONTAINER_NAME=web +# WIDE ON PURPOSE, AND WORTH NARROWING. Loopback plus every private range, +# because a reverse proxy on the same Docker network or LAN has an address in +# one of them that cannot be known here — and a proxy that is NOT in this list +# breaks sign-in with OAuth, shares one login allowance between everybody and +# strips `secure` off cookies, none of which says why. +# +# The cost of the width: anything else with a private address that can reach +# plMail directly — another machine on the LAN, another container — is believed +# when it sends X-Forwarded-For, and can pose as any address it likes. That +# gets it past the PER-ADDRESS limits on the login form and the public booking +# page. It does not get it past the per-account login limit, which ignores the +# address. Once your proxy works, replace this with the proxy's own address or +# subnet; docs/install/reverse-proxy.md says how to find it. TRUSTED_PROXIES=127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 ###> lexik/jwt-authentication-bundle ### diff --git a/CHANGELOG.md b/CHANGELOG.md index 783c79df..514b33ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,18 @@ ran code, both of which let an outsider put misleading entries on a calendar unn server's verdict is believed when its name is on the same domain as the IMAP host. - **Mail in Spam or the bin is no longer read for events.** A message moved back out is read then. +**A mail's styles can no longer draw over plMail when you reply to it or print it.** Inline styles +are kept because in mail they are the design, and in the reading pane that is harmless: the message +is in a sandboxed frame. The quote in the reply composer and the print page are not framed, and a +mail using `position: fixed` could cover the composer with content of its own. Such declarations are +now removed, and both places clip what is inside them. Also from #34. + +**The default `TRUSTED_PROXIES` is documented for what it is.** It trusts every private address so a +reverse proxy works unconfigured, which also lets another machine on the LAN forge its address and +step around the per-address limits on login and booking. The per-account login limit is unaffected. +The default is unchanged — narrowing it blindly would break sign-in behind a proxy — and +[Reverse proxy](docs/install/reverse-proxy.md) now says how and when to narrow it. Also from #34. + Nothing already on a calendar is removed or changed by upgrading. One new nullable column, `event_source_link.hold_reason`. See [Invitations and events from mail](docs/features/calendar-invitations.md). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..f1c93ded --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,42 @@ +# Security + +## Reporting a vulnerability + +Please report it privately first: on GitHub, open the repository's **Security** tab and choose +**Report a vulnerability**. That opens a private advisory only the maintainer can read, and it is +where the fix and the disclosure are coordinated. + +If you would rather not use GitHub for it, open an ordinary issue that says only that you have a +security report and how to reach you — no details — and you will be contacted. + +What helps most: + +- which version or commit you looked at, and whether you ran it or read the code; +- what an attacker needs (an account, a place on the network, only the ability to send mail); +- what they get, in a sentence; +- the files and lines involved, if you know them. + +You will get an answer within a few days. plMail is maintained by one person in spare time, so a +fix can take longer than that; you will be told what the plan is either way. Reporters are credited +in the changelog unless they ask not to be. + +## What counts + +plMail is self-hosted, so the things worth reporting are the ones that cross a boundary an +installation relies on: + +- one user reading or changing another user's mail, calendar or settings; +- anything a **sender of mail** can make happen on the reader's side beyond showing them a + message — running script in the app, changing the calendar, reaching the reader's session; +- anything an unauthenticated visitor can do beyond the pages meant for them (login, shared + calendar links, booking pages, the demo door); +- secrets or tokens leaving the install. + +How the app is meant to hold those lines is written down in +[docs/internals/security-model.md](docs/internals/security-model.md). A report that shows one of +its claims to be false is exactly what this file is for. + +## Supported versions + +Fixes go into `main` and the next release. There are no maintained older branches: the answer to a +fixed vulnerability is upgrading. diff --git a/docs/de/install/reverse-proxy.md b/docs/de/install/reverse-proxy.md index f6bc95f3..aae7fa4d 100644 --- a/docs/de/install/reverse-proxy.md +++ b/docs/de/install/reverse-proxy.md @@ -1,4 +1,4 @@ - + # Hinter einem Reverse-Proxy Alles, was von außerhalb des eigenen Netzes erreichbar ist, will ein echtes Zertifikat davor, und @@ -103,6 +103,26 @@ Setzt du ihn zu weit — alles wird als vertrauenswürdig behandelt —, dann ka `X-Forwarded-For` fälschen und sich als beliebige Adresse ausgeben, was die Ratenbegrenzung pro Adresse aushebelt und einen frei wählbaren Wert in deine Logs schreibt. +**Die mitgelieferte Vorgabe liegt mit Absicht auf der weiten Seite.** Sie vertraut jeder privaten +Adresse, weil sich die Adresse des Proxys nicht im Voraus wissen lässt und ihr Fehlen die vier Dinge +oben kaputt macht. Der Preis: Jede andere Maschine in deinem LAN und jeder andere Container im +Docker-Netz, die plMail *am Proxy vorbei* erreichen, wird geglaubt, wenn sie `X-Forwarded-For` +schickt. Sie kann sich dann bei jeder Anfrage als neue Adresse ausgeben und die Grenzen umgehen, die +pro Adresse zählen: die 25 Anmeldeversuche pro 15 Minuten von einer Adresse und die Grenze der +öffentlichen Buchungsseite. Die Grenze von 20 Versuchen pro **Konto** schaut nicht auf die Adresse +und gilt weiter, ein einzelnes Konto lässt sich so also nicht schneller durchprobieren. + +Sobald der Proxy funktioniert, eng es auf den Proxy allein ein: + +```dotenv +# die Adresse des Proxys oder das Subnetz des Docker-Netzes, das er mit plMail teilt +TRUSTED_PROXIES=127.0.0.1,172.20.0.0/16 +``` + +`docker network inspect --format '{{(index .IPAM.Config 0).Subnet}}'` gibt das Subnetz eines +Docker-Netzes aus. Ist der Port von plMail nur für den Proxy und nicht ins LAN veröffentlicht, +kostet die weite Vorgabe nichts — es gibt sonst niemanden, dem geglaubt würde. + **Der typische Fehlerfall ist, das als OAuth-Problem zu diagnostizieren.** Eine Redirect-URI-Abweichung nennt die URI, und das `http://` darin ist der ganze Hinweis. @@ -256,6 +276,11 @@ eine Installation, die ihre eigene Konfiguration verwaltet, ist das das gewünsc bedeutet aber auch: Änderst du die Adresse in der Compose-Datei und erwartest, dass der Wert aus dem Einrichtungsbildschirm zählt, bearbeitest du die falsche Stelle, und umgekehrt. +**Die Vorgabe für `TRUSTED_PROXIES` glaubt deinem ganzen privaten Netz.** Deshalb funktioniert ein +Proxy ohne weiteres Zutun, und es heißt, dass eine Maschine im LAN, die plMail direkt erreicht, ihre +Adresse fälschen und die Ratenbegrenzung pro Adresse umgehen kann. Eng es ein, sobald der Proxy +läuft — siehe oben. + **Ein falsches `TRUSTED_PROXIES` sieht aus wie vier voneinander unabhängige Fehler.** OAuth-Abweichungen, ein von allen geteiltes Kontingent an Anmeldeversuchen, Cookies ohne `secure` und Freigabelinks mit falschem Schema sind ein und dieselbe Einstellung. diff --git a/docs/install/reverse-proxy.md b/docs/install/reverse-proxy.md index a375f0f8..335659d7 100644 --- a/docs/install/reverse-proxy.md +++ b/docs/install/reverse-proxy.md @@ -94,6 +94,26 @@ Get it wrong in the wide direction — trusting everything — and a client can and present itself as any address it likes, which defeats the per-address rate limits and puts a chosen value into your logs. +**The committed default is on the wide side of that, deliberately.** It trusts every private +address, because the proxy's own address cannot be known in advance and leaving it out breaks the +four things above. The price is that any other machine on your LAN, or any other container on the +Docker network, that can reach plMail *without* going through the proxy is believed when it sends +`X-Forwarded-For`. It can then pose as a new address for every request and step around the limits +that count per address: the 25-per-15-minutes cap on login attempts from one address, and the cap +on the public booking page. The limit of 20 attempts per **account** does not look at the address +and still holds, so one account cannot be guessed at faster this way. + +Once the proxy works, narrow it to the proxy alone: + +```dotenv +# the proxy's address, or the subnet of the Docker network it shares with plMail +TRUSTED_PROXIES=127.0.0.1,172.20.0.0/16 +``` + +`docker network inspect --format '{{(index .IPAM.Config 0).Subnet}}'` prints the subnet +of a Docker network. If plMail's port is published only to the proxy and not to the LAN, the wide +default costs nothing — there is nobody else to believe. + **The failure mode is diagnosing this as an OAuth problem.** A redirect-URI mismatch names the URI, and the `http://` in it is the entire clue. @@ -235,6 +255,10 @@ intended behaviour for a deployment that manages its own configuration — but i operator who changes the address in the Compose file and expects the setup screen's value to matter is editing the wrong one, and vice versa. +**The default `TRUSTED_PROXIES` believes your whole private network.** That is what makes a proxy +work out of the box, and it means a machine on the LAN that reaches plMail directly can forge its +address and dodge the per-address rate limits. Narrow it once the proxy is working — see above. + **A wrong `TRUSTED_PROXIES` looks like four unrelated bugs.** OAuth mismatches, everyone sharing a login-attempt allowance, cookies without `secure`, and share links with the wrong scheme are all one setting.