fix(nginx): stop the files alias resolving above the download root - #190
Open
junkerderprovinz wants to merge 1 commit into
Open
fix(nginx): stop the files alias resolving above the download root#190junkerderprovinz wants to merge 1 commit into
junkerderprovinz wants to merge 1 commit into
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
location SUBFOLDERfileshas no trailing slash while itsalias REPLACE_DOWNLOADS_PATH/has one. nginx appends the unmatched remainder of the URI to the alias verbatim, soGET /files../secret.txtresolves 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 = SUBFOLDERfilesreturning 301 to the slash form keeps a direct visit to/filesworking. The dashboard always requestsapi/files/with the slash, and the legacy fallback infooter.htmluses/files/, so the redirect only covers hand typed URLs and old bookmarks.The two files have to land together.
init-nginxdeletes this block whenSELKIES_FILE_TRANSFERScarries nodownloadorHARDEN_DESKTOPis true. Its old range keyed on the literalfiles {, 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 theSUBFOLDERsubstitution 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
PASSWORDset this was never reachable unauthenticated.auth_basicsits 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.confexactly asinit-nginxdoes and ran stock nginx 1.27.4 against it. Thefancyindexdirectives were dropped for the test since that module is not in stock nginx; it affects the directory listing, not file access.With
PASSWORDset, on both the plain and the ssl block:/files/public.txt/filesRendering was also checked for
SUBFOLDERvalues of/,/desktop/and/files/: after the hardening deletion the surviving locations match upstream exactly in each case, and the braces stay balanced.