Skip to content

Commit

Permalink
Merge pull request #47 from hashicorp/IPL-4970-data-tfe-slug-checksum…
Browse files Browse the repository at this point in the history
…-changing-on-each-run

Restore original timestamps when unpacking
  • Loading branch information
brandonc authored Nov 1, 2023
2 parents fffa1ad + b9ee75a commit eb823de
Show file tree
Hide file tree
Showing 13 changed files with 629 additions and 91 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ require (
require (
github.com/go-test/deep v1.0.3 // indirect
golang.org/x/net v0.5.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.6.0 // indirect
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,7 @@ golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
28 changes: 28 additions & 0 deletions internal/unpackinfo/lchtimes_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//go:build darwin
// +build darwin

package unpackinfo

import (
"golang.org/x/sys/unix"
)

// Lchtimes modifies the access and modified timestamps on a target path
// This capability is only available on Linux and Darwin as of now.
func (i UnpackInfo) Lchtimes() error {
return unix.Lutimes(i.Path, []unix.Timeval{
{Sec: i.OriginalAccessTime.Unix(), Usec: int32(i.OriginalAccessTime.Nanosecond() / 1e6 % 1e6)},
{Sec: i.OriginalModTime.Unix(), Usec: int32(i.OriginalModTime.Nanosecond() / 1e6 % 1e6)}},
)
}

// CanMaintainSymlinkTimestamps determines whether is is possible to change
// timestamps on symlinks for the the current platform. For regular files
// and directories, attempts are made to restore permissions and timestamps
// after extraction. But for symbolic links, go's cross-platform
// packages (Chmod and Chtimes) are not capable of changing symlink info
// because those methods follow the symlinks. However, a platform-dependent option
// is provided for linux and darwin (see Lchtimes)
func CanMaintainSymlinkTimestamps() bool {
return true
}
28 changes: 28 additions & 0 deletions internal/unpackinfo/lchtimes_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//go:build linux
// +build linux

package unpackinfo

import (
"golang.org/x/sys/unix"
)

// Lchtimes modifies the access and modified timestamps on a target path
// This capability is only available on Linux and Darwin as of now.
func (i UnpackInfo) Lchtimes() error {
return unix.Lutimes(i.Path, []unix.Timeval{
{Sec: i.OriginalAccessTime.Unix(), Usec: int64(i.OriginalAccessTime.Nanosecond() / 1e6 % 1e6)},
{Sec: i.OriginalModTime.Unix(), Usec: int64(i.OriginalModTime.Nanosecond() / 1e6 % 1e6)}},
)
}

// CanMaintainSymlinkTimestamps determines whether is is possible to change
// timestamps on symlinks for the the current platform. For regular files
// and directories, attempts are made to restore permissions and timestamps
// after extraction. But for symbolic links, go's cross-platform
// packages (Chmod and Chtimes) are not capable of changing symlink info
// because those methods follow the symlinks. However, a platform-dependent option
// is provided for linux and darwin (see Lchtimes)
func CanMaintainSymlinkTimestamps() bool {
return true
}
25 changes: 25 additions & 0 deletions internal/unpackinfo/lchtimes_others.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//go:build !(linux || darwin)
// +build !linux,!darwin

package unpackinfo

import (
"errors"
)

// Lchtimes modifies the access and modified timestamps on a target path
// This capability is only available on Linux and Darwin as of now.
func (i UnpackInfo) Lchtimes() error {
return errors.New("Lchtimes is not supported on this platform")
}

// CanMaintainSymlinkTimestamps determines whether is is possible to change
// timestamps on symlinks for the the current platform. For regular files
// and directories, attempts are made to restore permissions and timestamps
// after extraction. But for symbolic links, go's cross-platform
// packages (Chmod and Chtimes) are not capable of changing symlink info
// because those methods follow the symlinks. However, a platform-dependent option
// is provided for linux and darwin (see Lchtimes)
func CanMaintainSymlinkTimestamps() bool {
return false
}
153 changes: 153 additions & 0 deletions internal/unpackinfo/unpackinfo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package unpackinfo

import (
"archive/tar"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"time"
)

// UnpackInfo stores information about the file (or directory, or symlink) being
// unpacked. UnpackInfo ensures certain malicious tar files are not unpacked.
// The information can be used later to restore the original permissions
// and timestamps based on the type of entry the info represents.
type UnpackInfo struct {
Path string
OriginalAccessTime time.Time
OriginalModTime time.Time
OriginalMode fs.FileMode
Typeflag byte
}

// NewUnpackInfo returns an UnpackInfo based on a destination root and a tar header.
// It will return an error if the header represents an illegal symlink extraction
// or if the entry type is not supported by go-slug.
func NewUnpackInfo(dst string, header *tar.Header) (UnpackInfo, error) {
// Get rid of absolute paths.
path := header.Name

if path[0] == '/' {
path = path[1:]
}
path = filepath.Join(dst, path)

// Check for paths outside our directory, they are forbidden
target := filepath.Clean(path)
if !strings.HasPrefix(target, dst) {
return UnpackInfo{}, errors.New("invalid filename, traversal with \"..\" outside of current directory")
}

// Ensure the destination is not through any symlinks. This prevents
// any files from being deployed through symlinks defined in the slug.
// There are malicious cases where this could be used to escape the
// slug's boundaries (zipslip), and any legitimate use is questionable
// and likely indicates a hand-crafted tar file, which we are not in
// the business of supporting here.
//
// The strategy is to Lstat each path component from dst up to the
// immediate parent directory of the file name in the tarball, checking
// the mode on each to ensure we wouldn't be passing through any
// symlinks.
currentPath := dst // Start at the root of the unpacked tarball.
components := strings.Split(header.Name, "/")

for i := 0; i < len(components)-1; i++ {
currentPath = filepath.Join(currentPath, components[i])
fi, err := os.Lstat(currentPath)
if os.IsNotExist(err) {
// Parent directory structure is incomplete. Technically this
// means from here upward cannot be a symlink, so we cancel the
// remaining path tests.
break
}
if err != nil {
return UnpackInfo{}, fmt.Errorf("failed to evaluate path %q: %w", header.Name, err)
}
if fi.Mode()&fs.ModeSymlink != 0 {
return UnpackInfo{}, fmt.Errorf("cannot extract %q through symlink", header.Name)
}
}

result := UnpackInfo{
Path: path,
OriginalAccessTime: header.AccessTime,
OriginalModTime: header.ModTime,
OriginalMode: header.FileInfo().Mode(),
Typeflag: header.Typeflag,
}

if !result.IsDirectory() && !result.IsSymlink() && !result.IsRegular() && !result.IsTypeX() {
return UnpackInfo{}, fmt.Errorf("failed creating %q, unsupported file type %c", path, result.Typeflag)
}

return result, nil
}

// IsSymlink describes whether the file being unpacked is a symlink
func (i UnpackInfo) IsSymlink() bool {
return i.Typeflag == tar.TypeSymlink
}

// IsDirectory describes whether the file being unpacked is a directory
func (i UnpackInfo) IsDirectory() bool {
return i.Typeflag == tar.TypeDir
}

// IsTypeX describes whether the file being unpacked is a special TypeXHeader that can
// be ignored by go-slug
func (i UnpackInfo) IsTypeX() bool {
return i.Typeflag == tar.TypeXGlobalHeader || i.Typeflag == tar.TypeXHeader
}

// IsRegular describes whether the file being unpacked is a regular file
func (i UnpackInfo) IsRegular() bool {
return i.Typeflag == tar.TypeReg || i.Typeflag == tar.TypeRegA
}

// RestoreInfo changes the file mode and timestamps for the given UnpackInfo data
func (i UnpackInfo) RestoreInfo() error {
switch {
case i.IsDirectory():
return i.restoreDirectory()
case i.IsSymlink():
if CanMaintainSymlinkTimestamps() {
return i.restoreSymlink()
}
return nil
default: // Normal file
return i.restoreNormal()
}
}

func (i UnpackInfo) restoreDirectory() error {
if err := os.Chmod(i.Path, i.OriginalMode); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed setting permissions on directory %q: %w", i.Path, err)
}

if err := os.Chtimes(i.Path, i.OriginalAccessTime, i.OriginalModTime); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed setting times on directory %q: %w", i.Path, err)
}
return nil
}

func (i UnpackInfo) restoreSymlink() error {
if err := i.Lchtimes(); err != nil {
return fmt.Errorf("failed setting times on symlink %q: %w", i.Path, err)
}
return nil
}

func (i UnpackInfo) restoreNormal() error {
if err := os.Chmod(i.Path, i.OriginalMode); err != nil {
return fmt.Errorf("failed setting permissions on %q: %w", i.Path, err)
}

if err := os.Chtimes(i.Path, i.OriginalAccessTime, i.OriginalModTime); err != nil {
return fmt.Errorf("failed setting times on %q: %w", i.Path, err)
}
return nil
}
Loading

0 comments on commit eb823de

Please sign in to comment.