Authelia Remote-* headers are trusted from any source — LAN clients can impersonate any user/group #37

Open
opened 2026-10-26 12:28:00 -04:00 by jared · 0 comments
Owner

Problem

authenticateSSO (server.js) and makeAuthenticatePage (lib/pageauth.js) take identity and authorization straight from the Remote-User, Remote-Name, Remote-Email and Remote-Groups request headers, without checking where the request came from.

Through NPM this is fine, because the Authelia auth_request overwrites those headers. But the server listens on 0.0.0.0:8080, so any host on the LAN can connect directly and supply the headers itself. Choosing the group gets it through the admin/employee check, and it is then treated as whichever user it named, admin included. Since Pulse runs commands on every worker, this amounts to running commands across the fleet without a login.

A side effect is that upsertUser writes whatever the request claims (name, email, groups) into the users table.

Found by a read-only check on 2026-10-26. A direct request to :8080 carrying Remote-User and a non-matching group got the 403 group message instead of the 401 "Not authenticated" message. So the identity header was accepted, and only the group check stopped it. From the internet it is not reachable: NPM sends the request to Authelia first.

Fix

Do what Tinker Tickets does (see tinker_tickets#94 and AuthMiddleware::enforceTrustedProxy):

  • Add a TRUSTED_PROXIES setting in the env, e.g. TRUSTED_PROXIES=10.10.10.27 for NPM.
  • In both middlewares, if req.socket.remoteAddress, after stripping the ::ffff: prefix, is not in that list, reject any request that carries Remote-* headers. Never fall back to trusting them.
  • Fail closed: if TRUSTED_PROXIES is unset, refuse the headers and log a startup warning. The "empty = off" default is what #94 had to document around.
  • Optionally, also bind to the LAN interface and/or firewall :8080 so only NPM and the workers can reach it.

Related: the WebSocket has no authentication at all (separate issue).

## Problem `authenticateSSO` (server.js) and `makeAuthenticatePage` (lib/pageauth.js) take identity and authorization straight from the `Remote-User`, `Remote-Name`, `Remote-Email` and `Remote-Groups` request headers, without checking where the request came from. Through NPM this is fine, because the Authelia `auth_request` overwrites those headers. But the server listens on `0.0.0.0:8080`, so any host on the LAN can connect directly and supply the headers itself. Choosing the group gets it through the `admin`/`employee` check, and it is then treated as whichever user it named, admin included. Since Pulse runs commands on every worker, this amounts to running commands across the fleet without a login. A side effect is that `upsertUser` writes whatever the request claims (name, email, groups) into the `users` table. **Found by** a read-only check on 2026-10-26. A direct request to `:8080` carrying `Remote-User` and a non-matching group got the 403 group message instead of the 401 "Not authenticated" message. So the identity header was accepted, and only the group check stopped it. From the internet it is not reachable: NPM sends the request to Authelia first. ## Fix Do what Tinker Tickets does (see tinker_tickets#94 and `AuthMiddleware::enforceTrustedProxy`): - Add a `TRUSTED_PROXIES` setting in the env, e.g. `TRUSTED_PROXIES=10.10.10.27` for NPM. - In both middlewares, if `req.socket.remoteAddress`, after stripping the `::ffff:` prefix, is not in that list, reject any request that carries `Remote-*` headers. Never fall back to trusting them. - **Fail closed:** if `TRUSTED_PROXIES` is unset, refuse the headers and log a startup warning. The "empty = off" default is what #94 had to document around. - Optionally, also bind to the LAN interface and/or firewall `:8080` so only NPM and the workers can reach it. Related: the WebSocket has no authentication at all (separate issue).
jared added the bugbackendpriority/high labels 2026-10-26 12:28:00 -04:00
Sign in to join this conversation.