Skip to content

Pnac#22

Closed
milan-zededa wants to merge 6 commits intomasterfrom
pnac
Closed

Pnac#22
milan-zededa wants to merge 6 commits intomasterfrom
pnac

Conversation

@milan-zededa
Copy link
Owner

Description

Provide a clear and concise description of the changes in this PR and
explain why they are necessary.

If the PR contains only one commit, you will see the commit message above:
fill free to use it under the description section here, if it is good enough.

For Backport PRs, a full description is optional, but please clearly state
the original PR number(s). Use the #{NUMBER} format for that, it makes it easier
to handle with the scripts later. For example:

Backport of lf-edge#1234, lf-edge#5678, #91011

Title of a backport PR must also follow the following format:

"[x.y-stable] Original PR title".

where x.y-stable is the name of the target stable branch, and
Original PR title is the title of the original PR.

For example, for a PR that backports a PR with title Fix the nasty bug to
branch 13.4-stable the title should be:
[13.4-stable] Fix the nasty bug.

PR dependencies

List all dependencies of this PR (when applicable, otherwise remove this
section).

How to test and validate this PR

Please describe how the changes in this PR can be validated or verified. For
example:

  • If your PR fixes a bug, outline the steps to confirm the issue is resolved.
  • If your PR introduces a new feature, explain how to test and validate it.

This will be used

  1. to provide test scenarios for the QA team
  2. by a reviewer to validate the changes in this PR.

The first is especially important, so, please make sure to provide as much
detail as possible.

If it's covered by an automated test, please mention it here.

Changelog notes

Text in this section will be used to generate the changelog entry for
release notes. The consumers of this are end users, not developers.
So, provide a clear and short description of what is changed in the PR from
the end user perspective. If it changes only tooling or some internal
implementation, put a note like "No user-facing changes" or "None".

PR Backports

For all current LTS branches, please state explicitly if this PR should be
backported or not. This section is used by our scripts to track the backports,
so, please, do not omit it.

Here is the list of current LTS branches (it should be always up to date):

  • 16.0-stable
  • 14.5-stable
  • 13.4-stable

For example, if this PR fixes a bug in a feature that was introduced in 14.5,
you can write:

- 16.0-stable: To be backported.
- 14.5-stable: No, as the feature is not available there.
- 13.4-stable: No, as the feature is not available there.

Also, to the PRs that should be backported into any stable branch, please
add a label stable.

Checklist

  • I've provided a proper description
  • I've added the proper documentation
  • I've tested my PR on amd64 device
  • I've tested my PR on arm64 device
  • I've written the test verification instructions
  • I've set the proper labels to this PR

For backport PRs (remove it if it's not a backport):

  • I've added a reference link to the original PR
  • PR's title follows the template

And the last but not least:

  • I've checked the boxes above, or I've provided a good reason why I didn't
    check them.

Please, check the boxes above after submitting the PR in interactive mode.

Signed-off-by: Milan Lenco <milan@zededa.com>
Signed-off-by: Milan Lenco <milan@zededa.com>
Update EVE-API to include the recently added SCEP
(Simple Certificate Enrollment Protocol) and PNAC (802.1x) support.

Signed-off-by: Milan Lenco <milan@zededa.com>
Signed-off-by: Milan Lenco <milan@zededa.com>
Signed-off-by: Milan Lenco <milan@zededa.com>
Signed-off-by: Milan Lenco <milan@zededa.com>
}
tarReader := tar.NewReader(gzf)
for {
header, err := tarReader.Next()

Check failure

Code scanning / CodeQL

Arbitrary file access during archive extraction ("Zip Slip") High

Unsanitized archive entry, which may contain '..', is used in a
file system operation
.
Unsanitized archive entry, which may contain '..', is used in a
file system operation
.
Unsanitized archive entry, which may contain '..', is used in a
file system operation
.

Copilot Autofix

AI 28 days ago

In general, to fix Zip Slip–style issues, you must ensure that paths derived from archive entries cannot escape a designated extraction root. This is typically done by (1) normalizing the archive entry path, (2) rejecting any path that is absolute, contains .. path elements, or otherwise resolves outside the intended base directory, and (3) only passing validated, safe paths to filesystem operations like os.Mkdir and os.Create.

For this code, the simplest and least invasive fix is to update resolvePath so that it both normalizes and validates the input path and returns an error if it detects traversal or an absolute path. Since all uses of header.Name flow through resolvePath, tightening its behavior will secure every filesystem operation using newPath. A robust approach is:

  • Convert the incoming curPath (e.g., header.Name) to a slash‑normalized form and strip any leading / so that archive entries that start at the filesystem root are rejected or made relative.
  • Use filepath.Clean on the path after replacing the matched Location prefix with Destination, and explicitly reject any path that contains .. path components or that is absolute.
  • Optionally, if you also want to ensure extraction stays within a well‑defined base directory, derive a base directory from Destination and then verify that the cleaned newPath is still within that base using filepath.Abs and strings.HasPrefix.

Because we cannot alter callers outside this file, the best single change is to modify resolvePath to:

  • Return os.ErrNotExist (or another error) if curPath contains .. path elements or attempts to be absolute.
  • Only return the mapped newPath after running filepath.Clean and rechecking that it does not contain .. and isn’t absolute.
    This preserves existing semantics for valid paths (they still map Location to Destination) while preventing malicious header.Name values from producing unsafe newPath values. No new imports are needed; filepath and strings are already imported.
Suggested changeset 1
evetest/utils/tar.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/evetest/utils/tar.go b/evetest/utils/tar.go
--- a/evetest/utils/tar.go
+++ b/evetest/utils/tar.go
@@ -75,12 +75,36 @@
 }
 
 func resolvePath(curPath string, paths []FileToSave) (string, error) {
+	// Normalize to OS-specific path separators.
+	normalized := filepath.FromSlash(curPath)
+	// Disallow absolute paths from the archive.
+	if filepath.IsAbs(normalized) {
+		return "", os.ErrNotExist
+	}
+	// Clean the path and reject any traversal using "..".
+	cleaned := filepath.Clean(normalized)
+	// After filepath.Clean, any traversal will include ".." as a path element.
+	if cleaned == ".." || strings.HasPrefix(cleaned, ".."+string(os.PathSeparator)) {
+		return "", os.ErrNotExist
+	}
+
 	if paths == nil {
-		return curPath, nil
+		return cleaned, nil
 	}
+
 	for _, el := range paths {
-		if strings.HasPrefix(curPath, el.Location) {
-			return strings.Replace(curPath, el.Location, el.Destination, 1), nil
+		if strings.HasPrefix(cleaned, el.Location) {
+			mapped := strings.Replace(cleaned, el.Location, el.Destination, 1)
+			// Normalize and clean the mapped path as well.
+			mapped = filepath.Clean(filepath.FromSlash(mapped))
+			// Reject any absolute or traversing result.
+			if filepath.IsAbs(mapped) {
+				return "", os.ErrNotExist
+			}
+			if mapped == ".." || strings.HasPrefix(mapped, ".."+string(os.PathSeparator)) {
+				return "", os.ErrNotExist
+			}
+			return mapped, nil
 		}
 	}
 	return "", os.ErrNotExist
EOF
@@ -75,12 +75,36 @@
}

func resolvePath(curPath string, paths []FileToSave) (string, error) {
// Normalize to OS-specific path separators.
normalized := filepath.FromSlash(curPath)
// Disallow absolute paths from the archive.
if filepath.IsAbs(normalized) {
return "", os.ErrNotExist
}
// Clean the path and reject any traversal using "..".
cleaned := filepath.Clean(normalized)
// After filepath.Clean, any traversal will include ".." as a path element.
if cleaned == ".." || strings.HasPrefix(cleaned, ".."+string(os.PathSeparator)) {
return "", os.ErrNotExist
}

if paths == nil {
return curPath, nil
return cleaned, nil
}

for _, el := range paths {
if strings.HasPrefix(curPath, el.Location) {
return strings.Replace(curPath, el.Location, el.Destination, 1), nil
if strings.HasPrefix(cleaned, el.Location) {
mapped := strings.Replace(cleaned, el.Location, el.Destination, 1)
// Normalize and clean the mapped path as well.
mapped = filepath.Clean(filepath.FromSlash(mapped))
// Reject any absolute or traversing result.
if filepath.IsAbs(mapped) {
return "", os.ErrNotExist
}
if mapped == ".." || strings.HasPrefix(mapped, ".."+string(os.PathSeparator)) {
return "", os.ErrNotExist
}
return mapped, nil
}
}
return "", os.ErrNotExist
Copilot is powered by AI and may make mistakes. Always verify output.
}
tarReader := tar.NewReader(u)
for {
header, err := tarReader.Next()

Check failure

Code scanning / CodeQL

Arbitrary file access during archive extraction ("Zip Slip") High

Unsanitized archive entry, which may contain '..', is used in a
file system operation
.
Unsanitized archive entry, which may contain '..', is used in a
file system operation
.
Unsanitized archive entry, which may contain '..', is used in a
file system operation
.
Unsanitized archive entry, which may contain '..', is used in a
file system operation
.
Unsanitized archive entry, which may contain '..', is used in a
file system operation
.
Unsanitized archive entry, which may contain '..', is used in a
file system operation
.

Copilot Autofix

AI 28 days ago

In general, the fix is to normalize archive entry paths and ensure they do not escape the intended destination directory before using them in any filesystem operation. That means rejecting paths containing .. path elements, absolute paths, and anything that, after cleaning and joining with destination, does not remain within destination. Symlink targets (Linkname) should be treated similarly if you want to prevent links to arbitrary locations.

The best way to fix this function without changing existing behavior is:

  1. Introduce a small helper inside ExtractFromTar that, given an entry path, returns a safe absolute path under destination or an error if the path is unsafe. This helper should:
    • Use filepath.Clean to normalize the entry path.
    • Reject empty, absolute, or traversal paths (those starting with .. or containing .. as a path element).
    • Join the cleaned name with destination, get filepath.Abs for both, and ensure the resulting path has destAbs as its prefix (rel does not start with .. and is not absolute).
  2. Replace the simple pathBuilder := func(oldPath string) string { return path.Join(destination, oldPath) } with this validated builder that returns (string, error).
  3. Update all uses:
    • For directories and regular files: compute fullPath, err := pathBuilder(header.Name) and use fullPath in MkdirAll, Lstat, Remove, OpenFile, etc. If pathBuilder returns an error, abort extraction with a wrapped error mentioning the entry name.
    • For symlinks: compute linkPath from header.Name as above. For the symlink target, either:
      • Keep current behavior (allow arbitrary targets) but at least keep the link itself within destination by validating only header.Name, or
      • Stricter: also pass header.Linkname through the same validation so links cannot point outside destination. Given the vulnerability class, validating both is safer and still consistent with the idea of keeping extraction confined.
  4. Use filepath rather than path for actual OS paths, since this is filesystem-related code.

All changes are within evetest/utils/tar.go, in the ExtractFromTar function region. No new imports are needed, because filepath and strings are already imported.

Suggested changeset 1
evetest/utils/tar.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/evetest/utils/tar.go b/evetest/utils/tar.go
--- a/evetest/utils/tar.go
+++ b/evetest/utils/tar.go
@@ -155,10 +155,38 @@
 
 // ExtractFromTar extracts files from a tar reader into the destination directory
 func ExtractFromTar(u io.Reader, destination string) error {
-	// path inside tar is relative
-	pathBuilder := func(oldPath string) string {
-		return path.Join(destination, oldPath)
+	// path inside tar is relative; ensure paths stay within destination to avoid Zip Slip
+	destAbs, err := filepath.Abs(destination)
+	if err != nil {
+		return fmt.Errorf("ExtractFromTar: cannot get absolute destination: %w", err)
 	}
+	pathBuilder := func(entryPath string) (string, error) {
+		// Normalize entry path and reject absolute or traversing paths
+		cleanName := filepath.Clean(entryPath)
+		if cleanName == "." || cleanName == "" {
+			return "", fmt.Errorf("invalid entry path %q", entryPath)
+		}
+		if filepath.IsAbs(cleanName) {
+			return "", fmt.Errorf("absolute paths are not allowed in tar entries: %q", entryPath)
+		}
+		// Prevent paths that start with ".." after cleaning
+		if strings.HasPrefix(cleanName, ".."+string(filepath.Separator)) || cleanName == ".." {
+			return "", fmt.Errorf("path traversal is not allowed in tar entries: %q", entryPath)
+		}
+		fullPath := filepath.Join(destAbs, cleanName)
+		fullAbs, err := filepath.Abs(fullPath)
+		if err != nil {
+			return "", fmt.Errorf("cannot get absolute path for %q: %w", entryPath, err)
+		}
+		rel, err := filepath.Rel(destAbs, fullAbs)
+		if err != nil {
+			return "", fmt.Errorf("cannot get relative path for %q: %w", entryPath, err)
+		}
+		if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) || filepath.IsAbs(rel) {
+			return "", fmt.Errorf("entry path escapes destination: %q", entryPath)
+		}
+		return fullAbs, nil
+	}
 	tarReader := tar.NewReader(u)
 	for {
 		header, err := tarReader.Next()
@@ -170,17 +197,25 @@
 		}
 		switch header.Typeflag {
 		case tar.TypeDir:
-			if err := os.MkdirAll(pathBuilder(header.Name), os.FileMode(header.Mode)); err != nil {
+			dirPath, err := pathBuilder(header.Name)
+			if err != nil {
+				return fmt.Errorf("ExtractFromTar: invalid directory path %q: %w", header.Name, err)
+			}
+			if err := os.MkdirAll(dirPath, os.FileMode(header.Mode)); err != nil {
 				return fmt.Errorf("ExtractFromTar: Mkdir() failed: %w", err)
 			}
 		case tar.TypeReg:
-			if _, err := os.Lstat(pathBuilder(header.Name)); err == nil {
-				err = os.Remove(pathBuilder(header.Name))
+			filePath, err := pathBuilder(header.Name)
+			if err != nil {
+				return fmt.Errorf("ExtractFromTar: invalid file path %q: %w", header.Name, err)
+			}
+			if _, err := os.Lstat(filePath); err == nil {
+				err = os.Remove(filePath)
 				if err != nil {
 					return fmt.Errorf("ExtractFromTar: cannot remove old file: %w", err)
 				}
 			}
-			outFile, err := os.OpenFile(pathBuilder(header.Name), os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
+			outFile, err := os.OpenFile(filePath, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
 			if err != nil {
 				return fmt.Errorf("ExtractFromTar: OpenFile() failed: %w", err)
 			}
@@ -197,15 +225,23 @@
 				return fmt.Errorf("ExtractFromTar: outFile.Close() failed: %w", err)
 			}
 		case tar.TypeLink, tar.TypeSymlink:
-			if _, err := os.Lstat(pathBuilder(header.Name)); err == nil {
-				err = os.Remove(pathBuilder(header.Name))
+			linkPath, err := pathBuilder(header.Name)
+			if err != nil {
+				return fmt.Errorf("ExtractFromTar: invalid symlink path %q: %w", header.Name, err)
+			}
+			targetPath, err := pathBuilder(header.Linkname)
+			if err != nil {
+				return fmt.Errorf("ExtractFromTar: invalid symlink target %q: %w", header.Linkname, err)
+			}
+			if _, err := os.Lstat(linkPath); err == nil {
+				err = os.Remove(linkPath)
 				if err != nil {
 					return fmt.Errorf("ExtractFromTar: cannot remove old symlink: %w", err)
 				}
 			}
-			if err := os.Symlink(pathBuilder(header.Linkname), pathBuilder(header.Name)); err != nil {
+			if err := os.Symlink(targetPath, linkPath); err != nil {
 				return fmt.Errorf("ExtractFromTar: Symlink(%s, %s) failed: %w",
-					pathBuilder(header.Name), pathBuilder(header.Linkname), err)
+					linkPath, targetPath, err)
 			}
 		default:
 			return fmt.Errorf(
EOF
Copilot is powered by AI and may make mistakes. Always verify output.
return fmt.Errorf("ExtractFromTar: cannot remove old symlink: %w", err)
}
}
if err := os.Symlink(pathBuilder(header.Linkname), pathBuilder(header.Name)); err != nil {

Check failure

Code scanning / CodeQL

Arbitrary file write extracting an archive containing symbolic links High

Unresolved path from an archive header, which may point outside the archive root, is used in
symlink creation
.

Copilot Autofix

AI 28 days ago

In general, fixing this requires validating and constraining all paths derived from tar headers before using them on the filesystem, and for symlinks specifically, ensuring that the final resolved target (after following any existing symlinks) remains within the extraction root. That means: (1) prevent absolute paths and .. traversal from escaping destination, and (2) for symlink creation, check that the symlink’s effective target is still under destination using filepath.EvalSymlinks plus a Rel-based check.

For this code, the minimal, behavior-preserving fix is:

  1. Introduce a helper isRel(candidate, root string) bool that:

    • Rejects absolute candidate.
    • Resolves candidate relative to root using filepath.EvalSymlinks(filepath.Join(root, candidate)).
    • Computes filepath.Rel(root, resolved) and ensures the result does not start with ...
      This matches the “GOOD” pattern from the background.
  2. Use filepath (already imported) instead of path for filesystem paths, since this code works with the local filesystem, not URL paths. In particular:

    • Replace pathBuilder := func(oldPath string) string { return path.Join(destination, oldPath) }
      with filepath.Join.
    • This avoids subtle cross-platform issues and uses the right separator semantics.
  3. Before creating a symlink (and for safety also before creating files or directories), ensure that the target path is within destination:

    • Compute dstPath := pathBuilder(header.Name) and verify isRel(header.Name, destination); if not, return an error.
    • For symlinks: verify both the link name and its target:
      • if !isRel(header.Name, destination) || !isRel(header.Linkname, destination) { return error }
        This ensures that neither the symlink itself nor the effective resolved target can escape destination.
  4. Keep all other logic (removal of old entries, size limiting, mode handling) unchanged.

All required pieces are contained in evetest/utils/tar.go: we only add a small helper function and adjust the ExtractFromTar implementation to use it and to use filepath.Join instead of path.Join. No new imports are needed because filepath and strings are already imported.

Suggested changeset 1
evetest/utils/tar.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/evetest/utils/tar.go b/evetest/utils/tar.go
--- a/evetest/utils/tar.go
+++ b/evetest/utils/tar.go
@@ -18,6 +18,25 @@
 	Destination string
 }
 
+// isRel checks whether the given candidate path, interpreted as relative to root,
+// resolves (after following all symlinks) to a location within root.
+func isRel(candidate, root string) bool {
+	if filepath.IsAbs(candidate) {
+		return false
+	}
+	// Resolve the candidate relative to the root, following any existing symlinks.
+	realpath, err := filepath.EvalSymlinks(filepath.Join(root, candidate))
+	if err != nil {
+		return false
+	}
+	relpath, err := filepath.Rel(root, realpath)
+	if err != nil {
+		return false
+	}
+	relpath = filepath.Clean(relpath)
+	return relpath != ".." && !strings.HasPrefix(relpath, ".."+string(filepath.Separator))
+}
+
 // MaxDecompressedContentSize is the maximum size of a file that can be written to disk after decompression.
 // This is to prevent a DoS attack by unpacking a compressed file that is too big to be decompressed.
 const MaxDecompressedContentSize = 1024 * 1024 * 1024 // 1 GB
@@ -157,7 +176,7 @@
 func ExtractFromTar(u io.Reader, destination string) error {
 	// path inside tar is relative
 	pathBuilder := func(oldPath string) string {
-		return path.Join(destination, oldPath)
+		return filepath.Join(destination, oldPath)
 	}
 	tarReader := tar.NewReader(u)
 	for {
@@ -170,10 +189,16 @@
 		}
 		switch header.Typeflag {
 		case tar.TypeDir:
+			if !isRel(header.Name, destination) {
+				return fmt.Errorf("ExtractFromTar: directory path escapes destination: %s", header.Name)
+			}
 			if err := os.MkdirAll(pathBuilder(header.Name), os.FileMode(header.Mode)); err != nil {
 				return fmt.Errorf("ExtractFromTar: Mkdir() failed: %w", err)
 			}
 		case tar.TypeReg:
+			if !isRel(header.Name, destination) {
+				return fmt.Errorf("ExtractFromTar: file path escapes destination: %s", header.Name)
+			}
 			if _, err := os.Lstat(pathBuilder(header.Name)); err == nil {
 				err = os.Remove(pathBuilder(header.Name))
 				if err != nil {
@@ -197,6 +218,13 @@
 				return fmt.Errorf("ExtractFromTar: outFile.Close() failed: %w", err)
 			}
 		case tar.TypeLink, tar.TypeSymlink:
+			// Ensure both the link itself and its target resolve within destination.
+			if !isRel(header.Name, destination) {
+				return fmt.Errorf("ExtractFromTar: symlink path escapes destination: %s", header.Name)
+			}
+			if !isRel(header.Linkname, destination) {
+				return fmt.Errorf("ExtractFromTar: symlink target escapes destination: %s", header.Linkname)
+			}
 			if _, err := os.Lstat(pathBuilder(header.Name)); err == nil {
 				err = os.Remove(pathBuilder(header.Name))
 				if err != nil {
EOF
@@ -18,6 +18,25 @@
Destination string
}

// isRel checks whether the given candidate path, interpreted as relative to root,
// resolves (after following all symlinks) to a location within root.
func isRel(candidate, root string) bool {
if filepath.IsAbs(candidate) {
return false
}
// Resolve the candidate relative to the root, following any existing symlinks.
realpath, err := filepath.EvalSymlinks(filepath.Join(root, candidate))
if err != nil {
return false
}
relpath, err := filepath.Rel(root, realpath)
if err != nil {
return false
}
relpath = filepath.Clean(relpath)
return relpath != ".." && !strings.HasPrefix(relpath, ".."+string(filepath.Separator))
}

// MaxDecompressedContentSize is the maximum size of a file that can be written to disk after decompression.
// This is to prevent a DoS attack by unpacking a compressed file that is too big to be decompressed.
const MaxDecompressedContentSize = 1024 * 1024 * 1024 // 1 GB
@@ -157,7 +176,7 @@
func ExtractFromTar(u io.Reader, destination string) error {
// path inside tar is relative
pathBuilder := func(oldPath string) string {
return path.Join(destination, oldPath)
return filepath.Join(destination, oldPath)
}
tarReader := tar.NewReader(u)
for {
@@ -170,10 +189,16 @@
}
switch header.Typeflag {
case tar.TypeDir:
if !isRel(header.Name, destination) {
return fmt.Errorf("ExtractFromTar: directory path escapes destination: %s", header.Name)
}
if err := os.MkdirAll(pathBuilder(header.Name), os.FileMode(header.Mode)); err != nil {
return fmt.Errorf("ExtractFromTar: Mkdir() failed: %w", err)
}
case tar.TypeReg:
if !isRel(header.Name, destination) {
return fmt.Errorf("ExtractFromTar: file path escapes destination: %s", header.Name)
}
if _, err := os.Lstat(pathBuilder(header.Name)); err == nil {
err = os.Remove(pathBuilder(header.Name))
if err != nil {
@@ -197,6 +218,13 @@
return fmt.Errorf("ExtractFromTar: outFile.Close() failed: %w", err)
}
case tar.TypeLink, tar.TypeSymlink:
// Ensure both the link itself and its target resolve within destination.
if !isRel(header.Name, destination) {
return fmt.Errorf("ExtractFromTar: symlink path escapes destination: %s", header.Name)
}
if !isRel(header.Linkname, destination) {
return fmt.Errorf("ExtractFromTar: symlink target escapes destination: %s", header.Linkname)
}
if _, err := os.Lstat(pathBuilder(header.Name)); err == nil {
err = os.Remove(pathBuilder(header.Name))
if err != nil {
Copilot is powered by AI and may make mistakes. Always verify output.
return fmt.Errorf("ExtractFromTar: cannot remove old symlink: %w", err)
}
}
if err := os.Symlink(pathBuilder(header.Linkname), pathBuilder(header.Name)); err != nil {

Check failure

Code scanning / CodeQL

Arbitrary file write extracting an archive containing symbolic links High

Unresolved path from an archive header, which may point outside the archive root, is used in
symlink creation
.

Copilot Autofix

AI 28 days ago

In general, to fix this class of issues you must ensure that any filesystem path derived from archive headers is validated before use. For symlinks, both the path of the link itself and the link target must be checked. This validation should be done on resolved paths (using filepath.EvalSymlinks or equivalent) to account for any pre‑existing symlinks inside the extraction directory. Only if the fully resolved paths are still within the intended extraction root should the link be created.

For this specific function, we should:

  1. Introduce a helper isWithinDestination(candidate, destination string) bool that:
    • Rejects absolute candidate paths.
    • Joins destination and candidate.
    • Calls filepath.EvalSymlinks on the joined path; if that fails, returns false.
    • Uses filepath.Rel from destination to the resolved path and ensures the result does not start with .. (after filepath.Clean).
  2. Before creating symlinks in the tar.TypeLink, tar.TypeSymlink case, call this helper on both header.Name (the link path) and header.Linkname (the target). If either fails the check, return an error instead of creating the symlink.
  3. When calling os.Symlink, use the validated, joined values rather than recomputing them ad hoc, so we don’t accidentally diverge from what we checked.
  4. Keep other behavior (directory creation, regular file extraction, decompression limit) unchanged.

All changes are within evetest/utils/tar.go, inside ExtractFromTar. We only need to add the small helper inside that function (or as a nested closure) using existing imports; path/filepath is already imported, so no new dependencies are required.

Suggested changeset 1
evetest/utils/tar.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/evetest/utils/tar.go b/evetest/utils/tar.go
--- a/evetest/utils/tar.go
+++ b/evetest/utils/tar.go
@@ -159,6 +159,24 @@
 	pathBuilder := func(oldPath string) string {
 		return path.Join(destination, oldPath)
 	}
+	// isWithinDestination checks that a path from the archive, when resolved,
+	// stays within the extraction destination. It also resolves any existing
+	// symlinks under the destination.
+	isWithinDestination := func(candidate string) bool {
+		if filepath.IsAbs(candidate) {
+			return false
+		}
+		joined := pathBuilder(candidate)
+		realPath, err := filepath.EvalSymlinks(joined)
+		if err != nil {
+			return false
+		}
+		rel, err := filepath.Rel(destination, realPath)
+		if err != nil {
+			return false
+		}
+		return !strings.HasPrefix(filepath.Clean(rel), "..")
+	}
 	tarReader := tar.NewReader(u)
 	for {
 		header, err := tarReader.Next()
@@ -197,15 +215,22 @@
 				return fmt.Errorf("ExtractFromTar: outFile.Close() failed: %w", err)
 			}
 		case tar.TypeLink, tar.TypeSymlink:
-			if _, err := os.Lstat(pathBuilder(header.Name)); err == nil {
-				err = os.Remove(pathBuilder(header.Name))
+			// Validate that both the link path and its target stay within destination,
+			// resolving any existing symlinks under destination.
+			if !isWithinDestination(header.Name) || !isWithinDestination(header.Linkname) {
+				return fmt.Errorf("ExtractFromTar: symlink %q -> %q escapes destination %q", header.Name, header.Linkname, destination)
+			}
+			linkPath := pathBuilder(header.Name)
+			targetPath := pathBuilder(header.Linkname)
+			if _, err := os.Lstat(linkPath); err == nil {
+				err = os.Remove(linkPath)
 				if err != nil {
 					return fmt.Errorf("ExtractFromTar: cannot remove old symlink: %w", err)
 				}
 			}
-			if err := os.Symlink(pathBuilder(header.Linkname), pathBuilder(header.Name)); err != nil {
+			if err := os.Symlink(targetPath, linkPath); err != nil {
 				return fmt.Errorf("ExtractFromTar: Symlink(%s, %s) failed: %w",
-					pathBuilder(header.Name), pathBuilder(header.Linkname), err)
+					linkPath, targetPath, err)
 			}
 		default:
 			return fmt.Errorf(
EOF
@@ -159,6 +159,24 @@
pathBuilder := func(oldPath string) string {
return path.Join(destination, oldPath)
}
// isWithinDestination checks that a path from the archive, when resolved,
// stays within the extraction destination. It also resolves any existing
// symlinks under the destination.
isWithinDestination := func(candidate string) bool {
if filepath.IsAbs(candidate) {
return false
}
joined := pathBuilder(candidate)
realPath, err := filepath.EvalSymlinks(joined)
if err != nil {
return false
}
rel, err := filepath.Rel(destination, realPath)
if err != nil {
return false
}
return !strings.HasPrefix(filepath.Clean(rel), "..")
}
tarReader := tar.NewReader(u)
for {
header, err := tarReader.Next()
@@ -197,15 +215,22 @@
return fmt.Errorf("ExtractFromTar: outFile.Close() failed: %w", err)
}
case tar.TypeLink, tar.TypeSymlink:
if _, err := os.Lstat(pathBuilder(header.Name)); err == nil {
err = os.Remove(pathBuilder(header.Name))
// Validate that both the link path and its target stay within destination,
// resolving any existing symlinks under destination.
if !isWithinDestination(header.Name) || !isWithinDestination(header.Linkname) {
return fmt.Errorf("ExtractFromTar: symlink %q -> %q escapes destination %q", header.Name, header.Linkname, destination)
}
linkPath := pathBuilder(header.Name)
targetPath := pathBuilder(header.Linkname)
if _, err := os.Lstat(linkPath); err == nil {
err = os.Remove(linkPath)
if err != nil {
return fmt.Errorf("ExtractFromTar: cannot remove old symlink: %w", err)
}
}
if err := os.Symlink(pathBuilder(header.Linkname), pathBuilder(header.Name)); err != nil {
if err := os.Symlink(targetPath, linkPath); err != nil {
return fmt.Errorf("ExtractFromTar: Symlink(%s, %s) failed: %w",
pathBuilder(header.Name), pathBuilder(header.Linkname), err)
linkPath, targetPath, err)
}
default:
return fmt.Errorf(
Copilot is powered by AI and may make mistakes. Always verify output.
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