Repository navigation
fix(plugin): extract plugin archives through os.Root - #32774
Open
Thanatos9404 wants to merge 1 commit into
Open
Thanatos9404 wants to merge 1 commit into
Thanatos9404 wants to merge 1 commit into
Conversation
The HTTP and local installers (TarGzExtractor) and the OCI installer each had their own copy of the tar extraction loop. Both built output paths with cleanJoin, which calls securejoin.SecureJoin, and then wrote through the os package. The joined path is only valid at the moment it is computed: the filepath-securejoin documentation notes that SecureJoin cannot protect against path components that change before the path is used. A symlink in the target directory pointing outside it was also rewritten into a path inside the target instead of being refused. Use a single extractTar for all three installers that opens the target directory with os.Root and creates every directory and file through it, so no entry can be written outside the target directory. The OCI copy of the loop is removed, which also means each file is closed as soon as it is written instead of when the function returns. cleanJoin becomes cleanPath. It keeps the existing checks for ':', '..' and absolute paths and returns a clean relative path instead of joining it. Files are created with the permission bits of the header mode, which is what os.OpenFile applied for ordinary modes. Behaviour for well formed plugin archives is unchanged. Changes for unusual input: a symlink in the target directory that leaves it, or any absolute symlink, now makes extraction fail instead of being rewritten; relative symlinks that stay inside are still followed. Header modes carrying Go's setuid, setgid or sticky FileMode bits no longer set them on the extracted file. Signed-off-by: Yashvardhan Thanvi <[email protected]>
2 of 3 tasks
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 this PR does / why we need it:
Plugin archives are extracted in two places:
TarGzExtractorininternal/plugin/installer/extractor.go, used by the HTTP and local installers, and a second copy of the same tar loop in the OCI installer. Both build each output path withcleanJoin, which callssecurejoin.SecureJoin, and then write with theospackage.That path is only right at the moment it is computed. The filepath-securejoin README describes the
SecureJoinAPI as "fundamentally unsafe" when path components can change between the join and the use, and recommends newer APIs for new code.SecureJoinalso deals with a symlink in the target directory that points outside it by rewriting it into a path inside the target, so extraction carries on and writes somewhere the archive did not name.Plugin installs writing outside their directory have come up before: GHSA-qq3j-xp49-j73f (plugin archive directory traversal, 2020) and GHSA-vmx8-mqv2-9gmg (plugin install writing outside the plugins directory through the version field), as have similar chart cases (GHSA-hr2v-4r36-88hr, GHSA-557j-xg8c-q2mm). All of those are fixed. This PR does not fix a known vulnerability. It removes the path joining from plugin extraction, so that an archive entry cannot be written outside the target directory by construction.
The change:
extractTarinextractor.go, built onos.Root, is used byTarGzExtractor.Extractand by the OCI installer. Every directory and file is created through the root. The OCI copy of the loop is removed, so each file is now closed as soon as it has been written instead of when the function returns.cleanJoinbecomescleanPath. It keeps the existing checks for:,..and absolute paths, with the same error messages, and returns the cleaned relative path instead of joining it.os.FileMode(header.Mode).Perm().Root.OpenFilerejects anything beyond permission bits, andos.OpenFilealready ignored the tar setuid, setgid, sticky and file type bits, so ordinary archives get exactly the modes they got before.os.Rootwas added in Go 1.24 andRoot.MkdirAllin Go 1.25;mainanddev-v3both declarego 1.26.0. #32773 does the same for chart expansion. Once both are merged nothing importsgithub.com/cyphar/filepath-securejoinany more, and I will add thego mod tidychange to whichever of the two is merged second (or send it separately).Special notes for your reviewer:
For well formed plugin archives nothing changes: same files and layout, directories 0755, file permissions from the header minus umask, symlink and other unsupported entry types still rejected with
unknown type, and the same errors for:,..and absolute paths. The behaviour changes, all for unusual input, are:path escapes from parent. Before,SecureJoinrewrote it into a path inside the target and extraction continued there.os.Rootdoes not follow absolute links. Relative symlinks that stay inside the target are still followed.FileModebits (1<<23, 1<<22, 1<<20) used to set those bits on the file throughos.OpenFile; now only the permission bits are used. This overlaps with Sanitize attacker-controlled file modes in plugin extraction #32249.connow fails withpath escapes from parentinstead of opening the device. (Names containing:were already rejected.)Related work: #31387 also removes the OCI copy of
extractTar; whichever lands first, I am happy to rebase. #32414, #32347 and #32398 fix the deferredClosein the OCI loop on their own.Tests:
internal/plugin/installer/extractor_test.goruns every case through bothTarGzExtractorand the OCI entry point. One table test feeds archives that try to leave the target (.., an absolute path, a symlink entry followed by a write through it) and targets that already contain symlinks pointing outside (directory symlink with an absolute and a relative target, a directory entry through such a symlink, and a file symlink); every case must fail and leave the outside directory empty. Another checks that a well formed archive (a./entry, explicit and implied directories, a pax global header, and modes 0644, 0755, 0600, 04755, 02755, 01755, 0100755, 0100644) extracts with the same content, owner permission bits and no special bits. A third checks that a relative symlink inside the target is still followed.TestCleanJoinbecomesTestCleanPathwith the same cases.On main, the 8 cases with an existing symlink pointing outside fail (4 per entry point): extraction succeeds and writes at the rewritten path. Every other new test passes on main and on this branch. I ran the package tests on Linux; they do not build on Windows on main either (
syscall.Umaskinhttp_installer_test.go).If applicable:
docs neededlabel should be applied if so)