Skip to content

fix(nginx): stop the files alias resolving above the download root - #190

Open
junkerderprovinz wants to merge 1 commit into
linuxserver:masterfrom
junkerderprovinz:fix/files-alias-path-traversal
Open

fix(nginx): stop the files alias resolving above the download root#190
junkerderprovinz wants to merge 1 commit into
linuxserver:masterfrom
junkerderprovinz:fix/files-alias-path-traversal

Conversation

@junkerderprovinz

Copy link
Copy Markdown
Contributor

What

location SUBFOLDERfiles has no trailing slash while its alias REPLACE_DOWNLOADS_PATH/ has one. nginx appends the unmatched remainder of the URI to the alias verbatim, so GET /files../secret.txt resolves to <downloads>/../secret.txt, one level above the download root. Both server blocks carry it, the plain one and the ssl one.

Fix

A trailing slash on the location makes nginx strip the matched prefix instead of concatenating, so the remainder can no longer start above the root. An exact match location = SUBFOLDERfiles returning 301 to the slash form keeps a direct visit to /files working. The dashboard always requests api/files/ with the slash, and the legacy fallback in footer.html uses /files/, so the redirect only covers hand typed URLs and old bookmarks.

The two files have to land together. init-nginx deletes this block when SELKIES_FILE_TRANSFERS carries no download or HARDEN_DESKTOP is true. Its old range keyed on the literal files {, which the new opening line is not, so with only the config change a hardened container keeps serving the download root. The deletion now runs before the SUBFOLDER substitution and addresses the template token directly, one range per block. That ordering is deliberate: SUBFOLDER=/files/ is legal, and a pattern matching user supplied text would delete the dashboard root along with it.

Scope

With PASSWORD set this was never reachable unauthenticated. auth_basic sits at server level and is inherited by this location, so the traversal answered 401 like everything else. It exposed files only on instances running without a password, where the desktop itself is already open. Defence in depth rather than an urgent hole, and I sent a note to security@linuxserver.io first rather than opening with this.

Verified

Rendered default.conf exactly as init-nginx does and ran stock nginx 1.27.4 against it. The fancyindex directives were dropped for the test since that module is not in stock nginx; it affects the directory listing, not file access.

With PASSWORD set, on both the plain and the ssl block:

traversal, logged in traversal, no credentials /files/public.txt /files
upstream 200 with contents 401 200 301
patched 404 401 200 301
patched, hardened 404 401 404 404

Rendering was also checked for SUBFOLDER values of /, /desktop/ and /files/: after the hardening deletion the surviving locations match upstream exactly in each case, and the braces stay balanced.

The location lacked a trailing slash while its alias had one, so nginx appended
the remainder of the URI to the alias verbatim. A request for /files../x
therefore resolved to <downloads>/../x, one level above the download root, and
served whatever nginx could read there.

Adding the slash to the location makes nginx strip the matched prefix instead of
concatenating, which closes it. An exact-match redirect keeps /files without a
slash working, so a direct visit still lands on the listing. The dashboard always
requests api/files/ with the slash, so the redirect only covers typed URLs and
old bookmarks.

The two files have to land together. init-nginx deletes this block when downloads
are off or HARDEN_DESKTOP is set, and its old range keyed on the literal
"files {", which the new opening line no longer is. Left alone, a hardened
container would have kept serving the download root. The deletion now runs before
the SUBFOLDER substitution and addresses the template token directly, one range
per block, so it cannot be steered by a user supplied subfolder: SUBFOLDER=/files/
is legal, and a looser pattern would have deleted the dashboard root with it.

Verified with a plain nginx against the rendered config, on both server blocks
and with PASSWORD set. Upstream answers the traversal 200 with the file contents;
patched it answers 404; the download and the redirect are unchanged. With
hardening the endpoint is gone entirely, 404 for both, matching upstream
behaviour in that mode. Rendering was also checked for SUBFOLDER of /, /desktop/
and /files/: the surviving blocks match upstream exactly in each case.

Scope worth stating plainly: with PASSWORD set this was never reachable
unauthenticated. auth_basic sits at server level and is inherited by this
location, so the traversal answered 401 without credentials, which I measured
too. It exposed files only on instances running without a password, where the
desktop is already open. Defence in depth rather than an urgent hole.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

2 participants