Skip to content

Security: Phobetore/Evermind

SECURITY.md

Security policy

Reporting something

Report privately, through GitHub's private advisory form. It reaches the maintainer without the report being public first. Please do not open a normal issue for a vulnerability.

Tell me what you found, how to reproduce it, and what an attacker gets out of it. A working proof of concept is welcome but not required.

Evermind is maintained by one person. Expect an acknowledgement within a week, and a fix released as soon as it is written and tested rather than on a schedule I would end up missing. You will be credited in the release notes unless you would rather not be.

Which versions get fixes

The newest release, and nothing else. Evermind is young enough that there is no sensible long-term support story yet, and pretending otherwise would be worse than saying so.

What Evermind assumes about where it runs

These are design decisions rather than oversights, so a report about them will be closed as working-as-intended:

  • The access password is not authentication. It is one shared secret with no accounts, and over plain HTTP on a home network it travels in clear, so anyone who can watch that traffic can read the password and the session that follows it. It exists to keep the other people on your home Wi-Fi out of your conversations, and it is documented as a speed bump. How it works, and so what to expect from it, is described below.
  • Evermind is not built to face the open internet. It binds to localhost by default for that reason. Putting it on a public address is a choice you make yourself, and doing so safely means a reverse proxy, TLS and real authentication in front of it.
  • API keys are stored so the application can use them. They sit in the local database, readable by anyone who can already read the files on that machine. Anyone at that point has your conversations too.
  • The model is not sandboxed and is not trusted to be safe. Evermind sends it your text and renders what comes back. You picked the model; its behaviour is between you and it.

How the access password works

Each device types the password once and gets a session of its own, which lasts until it is ended. Sign out of this device and Sign out every device, both in Settings, end one session or all of them, and changing or removing the password ends them all, for good: an old password set again later lets none of them back in. A session never expires by itself: each opening of Evermind gives its cookie another 400 days, so a device that is lost stays signed in until every device is signed out or the password changes.

The database keeps of the password only a salted scrypt digest, slow to guess from on purpose, by which Evermind finds out at start that the password has changed; and of each session only an HMAC of its token keyed by the password. So reading the database does not hand anyone a way in. The password is compared after Unicode normalisation, so an accent typed on a phone keyboard is the same password as the one in .env however each wrote it. Wrong guesses are limited to 10 per 15 minutes for each address, and over IPv6 for each /64 network, since one device can take any address of its network.

That limit has a cost when devices share what it counts. Every device does when they all reach Evermind from one address, as behind Docker Desktop, or behind a reverse proxy that FORWARDED_ALLOW_IPS does not name (see below); and over IPv6, every device of one network does, since a home network is a single /64. Ten wrong passwords from anyone who can reach Evermind that way then lock all of them out of signing in for 15 minutes, the right password included, and a new burst of ten does it again as soon as those 15 minutes are over. Devices already signed in go on working. Restarting Evermind lifts the lock at once.

Requests from other sites

A website you visit can make your browser send requests to Evermind. This is what stands in its way:

  • Changes from another origin are refused, password or not. The browser names the page that sends a write in Origin, and Evermind refuses the write when that is not the address the request was sent to, its Host. A reverse proxy in front of Evermind must therefore pass the Host header through unchanged, or the interface's own changes are refused too. The scheme is not compared, so a proxy that ends HTTPS works.
  • Without a password, Evermind only answers to an address or localhost. A website can point a name of its own at your computer once its page is loaded (DNS rebinding), and the browser then treats that page as Evermind itself, able to read and change everything. So the API only answers when it is reached by an address (192.168.1.42, 127.0.0.1) or by localhost. To open it by a name, set a password.
  • With a password, the password protects the rest. Any name is accepted, so a proxy can serve Evermind under its own, but the session cookie belongs to the name each device signed in on: a page under another name has none, and would need the password.
  • Pictures are served only under the names Evermind gives them: 32 hexadecimal characters, then .png, .jpg or .webp. Any other name is a 404 before a file is looked for. A page can make your browser ask a password-less Evermind for a picture, and a name that is a Windows network path (\\host\share\x) would otherwise make Evermind connect to that host and offer it your Windows account's credentials.

Behind a reverse proxy

A proxy that ends HTTPS passes requests on to Evermind over plain HTTP, so Evermind only knows that a browser came over HTTPS, and from which address, if the proxy says so in X-Forwarded-Proto and X-Forwarded-For. Anyone can send those headers, so Evermind believes them only from the addresses listed in FORWARDED_ALLOW_IPS. Set it, in .env, to the address the proxy connects from, as long as nothing else connects from it (see below). The session cookie is then marked Secure, at sign-in and each time it is renewed, so the browser never sends it over plain HTTP, and wrong passwords are counted for each device, or over IPv6 for each network, rather than for the proxy. docs/DEVELOPING.md has an nginx example.

Anything else that reaches Evermind from that address is believed just as much, and can claim a new address for each guess, so name only an address that nothing but the proxy connects from. On the same computer that is not 127.0.0.1, which every program and browser there uses: the nginx example has its proxy connect from 127.0.0.2 instead, an address no browser connects from, though a program there could. With Docker, a proxy outside the container reaches it from the same address as everything else on that computer; one in its own container on Evermind's network can have a fixed address (ipv4_address in Compose), shown at the start of each of its requests' lines in docker compose logs. Where the proxy cannot have an address of its own, leave FORWARDED_ALLOW_IPS empty: both headers are then ignored, every device shares the limit on wrong passwords, and the cookie is not marked Secure, which HSTS below makes up for.

Turn on HSTS at the proxy as well (the Strict-Transport-Security header). A browser that has been there once then never uses plain HTTP for that name again, not for an address typed without https:// nor for an old http:// link. Without it, that first plain request can be answered by someone on the network instead of the proxy, with a page of their own that asks for the password.

Slow and malformed requests

Before the password or any route, Evermind holds at most 16 KB of a request's line and headers while it waits for the rest, and refuses the request (400) past that. It closes a connection that leaves it waiting more than 20 seconds, for the rest of a request's line and headers or for a body sent after its answer; uploads and replies are not timed. A request whose body turns out malformed once Evermind has started answering it, after the 401 of a locked route for instance, has its connection closed at once.

What is worth reporting

  • Anything that lets a request bypass the access password while it is set, or keep a session alive after it was ended or the password changed
  • Anything that lets a page from another origin change something in Evermind, or read anything from it
  • Anything that reads or writes data across conversations, personas or accounts that should not be reachable from where the request came from
  • Injection of any kind that escapes its context: SQL, template, path traversal into the data directory, or a card that executes something when imported
  • Anything in the published Docker image that runs with more privilege than it needs, or ships a credential
  • Dependency vulnerabilities that are actually reachable from Evermind's code paths, rather than a scanner's raw output

That last one matters. A CVE in a transitive dependency that no code path touches is noise, and I would rather spend the time on something real.

There aren't any published security advisories