Skip to content

Security dependencies: patched releases, streaming archives, no loopback trust - #1

Merged
inkly merged 10 commits into
mainfrom
chore/security-deps
Sep 21, 2026
Merged

inkly merged 10 commits into
mainfrom
chore/security-deps

Conversation

@inkly

@inkly inkly commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator
  • CI builds with the latest Go 1.26 patch (go-version-file pinned the build to go1.26.0 and its stdlib fixes), and the module now requires Go 1.26.8.
  • jwt v4.5.2, websocket v1.5.3, logrus, xz and klauspost/compress on patched releases.
  • mholt/archiver/v3 (unmaintained, advisories without a fix) replaced by mholt/archives behind a streaming WriteArchive: entries are fed as the walk finds them, files are opened through os.Root and must still be the file the walk saw, write errors and cancellation are returned, symlinks inside the tree are stored as links.
  • The JWT() middleware that let any loopback request through is gone; nothing used it.
  • InternalRequestEditor lets generated API clients (the message bus one) send the internal secret, so the message bus can stop trusting loopback alone.

govulncheck: no reachable vulnerability left. Opened so the tests run on Linux.

setup-go read the version from go.mod, which pins exactly go 1.26.0, so CI ran without the standard library security fixes shipped in later 1.26 patches (crypto/tls, net/http, html/template, os, ...). Ask for 1.26 with check-latest so every run picks up the newest patch. The go directive in go.mod is left as is.
golang-jwt/jwt/v4 v4.5.0 -> v4.5.2 (GO-2024-3250, GO-2025-3553), golang-jwt/jwt/v5 v5.0.0 -> v5.3.1 (GO-2025-3553), gorilla/websocket v1.5.0 -> v1.5.3 (GO-2026-6278), sirupsen/logrus v1.9.0 -> v1.9.4. ulikunitz/xz is left alone: it only comes in through archiver/v3, which the next commit removes.
…r/v3

archiver/v3 is unmaintained and carries advisories with no fix (GO-2024-2698, GO-2025-3605); it also pulled in rardecode v1 and xz v0.5.11. Its successor mholt/archives replaces it.

GetCompressionAlgorithm keeps its formats and extensions (zip, tar, tar.gz, tar.bz2, tar.xz, tar.lz4, tar.sz) but now returns an archives.Archiver. AddFile no longer writes into an open archive: it appends the entries for a path to a slice, and the caller passes that slice to Archiver.Archive. No service calls these two functions; the CasaOS core has its own copy.

Entry names are unchanged: the last element of commonPath followed by the path under commonPath, directories included, commonPath itself not listed. Symlinks used to be followed (os.Stat), so a link could pull in files or whole directories from outside the tree; they are now stored as symlinks. Devices, pipes and sockets are still skipped.

A test writes a nested tree, with a symlink leaving it where the OS allows one, as zip and tar.gz and reads both back with the standard library.
JWT() let any request whose RealIP was ::1 or 127.0.0.1 through without a token. The services stopped trusting loopback on 2026-09-13 and now check external.IsInternalRequest with the per-boot secret, and none of them calls jwt.JWT any more (checked CasaOS, AppManagement, LocalStorage, UserService, Gateway and MessageBus). Remove it, with the two tests that only exercised it, so nobody can wire it back in. Validate, the key helpers and JWKSHandler stay.

echo-jwt and golang-jwt/jwt/v5 were only needed by this middleware and leave go.mod.
mholt/archives brought in klauspost/compress v1.18.0, which has an out-of-bounds read in its s2 package (GO-2026-5841, fixed in v1.18.7). Our code does not reach it today, but tar.sz goes through s2, so take the fixed release.
archiver/v3 named tar directory entries "dir/" (as tar.FileInfoHeader does); mholt/archives writes "dir". Add the slash in AddFile so tar and zip names stay exactly what they were, and make the test check the raw names.
WriteArchive(ctx, w, format, commonPath, paths) replaces AddFile, which nothing called. It walks each selected path and hands entries to ArchiveAsync as they are found, so the first bytes go out before the walk ends; ctx stops the walk and the writes. GetCompressionAlgorithm keeps its formats and extensions and now returns archives.ArchiverAsync, which all seven implement.

Names are unchanged: base(commonPath) plus the place under commonPath, directories with a trailing slash, no entry for a selected commonPath directory. A selected commonPath file is now archived instead of dropped.

A selected symlink is followed and its target archived under the link's name, as archiver/v3 did; symlinks inside the tree stay links. A missing or unreadable path, or an empty selection, is an error instead of an empty archive.

Files are opened through os.Root at their walk root, non-blocking, and must still be the regular file the walk saw (os.SameFile), so a file swapped for a symlink or another file between the walk and the read is an error, not a read of outside content. A file that shrank is an error too.

The output writer records the first write error, which archives drops when its deferred Close fails the final flush, and refuses writes after any error so a failed archive is left unterminated.
The walk moves at the pace of the client downloading, so on a live folder a
temporary file renamed or a cache emptied between a directory's listing and
the walk reaching it is routine; failing the whole download for it was
worse than archiver/v3, whose callers skipped it. The selected path itself
going missing is still an error, and so is a file that vanishes after its
header was written.

Metadata is read with Lstat through the root rather than DirEntry.Info:
Windows answers Info from the directory listing, and Lstat keeps the read
confined to the tree on every platform. The archiver's early-exit error is
wrapped with %w.
The clients oapi-codegen generates, the message bus one first of all, do not
go through this module's HTTP helpers and so never sent the secret: a
service could not have authenticated to the message bus the day it stops
trusting loopback alone. InternalRequestEditor goes to their
WithRequestEditorFn and adds the secret to loopback destinations only,
leaving any Authorization already set.
WriteArchive's containment relies on os.Root, whose trailing-slash symlink
escape was fixed in 1.26.5, and a build on 1.26.0 also carries every stdlib
fix since. With 1.26.8 as the floor, whatever builds a service that uses
this module switches to a toolchain that has them.
@inkly
inkly merged commit 6943d65 into main Sep 21, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant