Skip to content

Commit

Permalink
Merge branch 'main' into becca/findnew-remove
Browse files Browse the repository at this point in the history
  • Loading branch information
RebeccaMahany authored Apr 25, 2024
2 parents 8b366ba + 20342a0 commit 67ade2a
Show file tree
Hide file tree
Showing 10 changed files with 309 additions and 48 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# Note that multistage builds can leverage the tag applied (at build
# time) to this container

FROM --platform=linux/amd64 golang:1.20 AS golauncherbuild
FROM --platform=linux/amd64 golang:1.21 AS golauncherbuild
LABEL maintainer="engineering@kolide.co"

# fake data or not?
Expand Down
8 changes: 8 additions & 0 deletions ee/agent/storage/sqlite/keyvalue_store_sqlite.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"embed"
"errors"
"fmt"
"math"
"os"
"path"
"path/filepath"
Expand Down Expand Up @@ -254,6 +255,13 @@ VALUES %s
ON CONFLICT (name) DO UPDATE SET value=excluded.value;`
valueStr := strings.TrimRight(strings.Repeat("(?, ?),", len(kvPairs)), ",")

// make sure we don't go over max int size
// this is driven codeql code scanning
// https://codeql.github.com/codeql-query-help/go/go-allocation-size-overflow/
if len(kvPairs) > math.MaxInt/2 {
return nil, errors.New("too many key-value pairs")
}

// Build value args; save key names at the same time to determine which keys to prune later
valueArgs := make([]any, 2*len(kvPairs))
keyNames := make([]any, len(kvPairs))
Expand Down
5 changes: 4 additions & 1 deletion ee/tables/tablehelpers/run_as_user_posix.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ func WithUid(uid string) ExecOps {
return fmt.Errorf("looking up user with uid %s: %w", uid, err)
}

if currentUser.Uid != "0" && currentUser.Uid != runningUser.Uid {
// If the current user is the user to run as, then early return to avoid needing NoSetGroups.
if currentUser.Uid == runningUser.Uid {
return nil
} else if currentUser.Uid != "0" {
return fmt.Errorf("current user %s is not root and can't start process for other user %s", currentUser.Uid, uid)
}

Expand Down
90 changes: 86 additions & 4 deletions ee/tuf/library_manager.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package tuf

import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"fmt"
"io"
"io/fs"
"log/slog"
"net/http"
"os"
Expand All @@ -16,7 +19,6 @@ import (
"time"

"github.com/Masterminds/semver"
"github.com/kolide/kit/fsutil"
"github.com/kolide/launcher/pkg/backoff"
"github.com/kolide/launcher/pkg/traces"
"github.com/theupdateframework/go-tuf/data"
Expand Down Expand Up @@ -207,9 +209,8 @@ func (ulm *updateLibraryManager) moveVerifiedUpdate(binary autoupdatableBinary,
}
}()

// Untar the archive. Note that `UntarBundle` calls `filepath.Dir(destination)`, so the inclusion of `binary`
// here doesn't matter as it's immediately stripped off.
if err := fsutil.UntarBundle(filepath.Join(stagedVersionedDirectory, string(binary)), stagedUpdate); err != nil {
// Untar the archive.
if err := untar(stagedVersionedDirectory, stagedUpdate); err != nil {
return fmt.Errorf("could not untar update to %s: %w", stagedVersionedDirectory, err)
}

Expand Down Expand Up @@ -249,6 +250,87 @@ func (ulm *updateLibraryManager) moveVerifiedUpdate(binary autoupdatableBinary,
return nil
}

// untar extracts the archive `source` to the given `destinationDir`. It sanitizes
// extract paths and file permissions.
func untar(destinationDir string, source string) error {
f, err := os.Open(source)
if err != nil {
return fmt.Errorf("opening source: %w", err)
}
defer f.Close()

gzr, err := gzip.NewReader(f)
if err != nil {
return fmt.Errorf("creating gzip reader from %s: %w", source, err)
}
defer gzr.Close()

tr := tar.NewReader(gzr)
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("reading tar file: %w", err)
}

if err := sanitizeExtractPath(destinationDir, header.Name); err != nil {
return fmt.Errorf("checking filename: %w", err)
}

destPath := filepath.Join(destinationDir, header.Name)
info := header.FileInfo()
if info.IsDir() {
if err = os.MkdirAll(destPath, sanitizePermissions(info)); err != nil {
return fmt.Errorf("creating directory %s for tar file: %w", destPath, err)
}
continue
}

if err := writeBundleFile(destPath, sanitizePermissions(info), tr); err != nil {
return fmt.Errorf("writing file: %w", err)
}
}
return nil
}

// sanitizeExtractPath checks that the supplied extraction path is not
// vulnerable to zip slip attacks. See https://snyk.io/research/zip-slip-vulnerability
func sanitizeExtractPath(filePath string, destination string) error {
destpath := filepath.Join(destination, filePath)
if !strings.HasPrefix(destpath, filepath.Clean(destination)+string(os.PathSeparator)) {
return fmt.Errorf("illegal file path %s", filePath)
}
return nil
}

// Allow owner read/write/execute, and only read/execute for all others.
const allowedPermissionBits fs.FileMode = fs.ModeType | 0755

// sanitizePermissions ensures that only the file owner has write permissions.
func sanitizePermissions(fileInfo fs.FileInfo) fs.FileMode {
return fileInfo.Mode() & allowedPermissionBits
}

// writeBundleFile reads from the given reader to create a file at the given path, with the desired permissions.
func writeBundleFile(destPath string, perm fs.FileMode, srcReader io.Reader) error {
file, err := os.OpenFile(destPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, perm)
if err != nil {
return fmt.Errorf("opening %s: %w", destPath, err)
}
if _, err := io.Copy(file, srcReader); err != nil {
if closeErr := file.Close(); closeErr != nil {
return fmt.Errorf("copying to %s: %v; close error: %w", destPath, err, closeErr)
}
return fmt.Errorf("copying to %s: %w", destPath, err)
}
if err := file.Close(); err != nil {
return fmt.Errorf("closing %s: %w", destPath, err)
}
return nil
}

// removeUpdate removes a given version from the given binary's update library.
func (ulm *updateLibraryManager) removeUpdate(binary autoupdatableBinary, binaryVersion string) {
directoryToRemove := filepath.Join(updatesDirectory(binary, ulm.baseDir), binaryVersion)
Expand Down
144 changes: 144 additions & 0 deletions ee/tuf/library_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package tuf
import (
"context"
"fmt"
"io/fs"
"net/http"
"net/http/httptest"
"os"
Expand All @@ -12,6 +13,7 @@ import (
"sync"
"testing"

"github.com/kolide/kit/ulid"
tufci "github.com/kolide/launcher/ee/tuf/ci"
"github.com/kolide/launcher/pkg/log/multislogger"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -291,6 +293,148 @@ func TestAddToLibrary_verifyStagedUpdate_handlesInvalidFiles(t *testing.T) {
}
}

func Test_sanitizeExtractPath(t *testing.T) {
t.Parallel()

var tests = []struct {
filepath string
destination string
expectError bool
}{
{
filepath: "file",
destination: "/tmp",
expectError: false,
},
{
filepath: "subdir/../subdir/file",
destination: "/tmp",
expectError: false,
},

{
filepath: "../../../file",
destination: "/tmp",
expectError: true,
},
{
filepath: "./././file",
destination: "/tmp",
expectError: false,
},
}

for _, tt := range tests {
tt := tt
t.Run(tt.filepath, func(t *testing.T) {
t.Parallel()

if tt.expectError {
require.Error(t, sanitizeExtractPath(tt.filepath, tt.destination), tt.filepath)
} else {
require.NoError(t, sanitizeExtractPath(tt.filepath, tt.destination), tt.filepath)
}
})
}
}

func Test_sanitizePermissions(t *testing.T) {
t.Parallel()

testCases := []struct {
testCaseName string
givenFilePermissions fs.FileMode
}{
{
testCaseName: "directory, valid permissions",
givenFilePermissions: fs.ModeDir | 0755,
},
{
testCaseName: "directory, invalid permissions (group has write)",
givenFilePermissions: fs.ModeDir | 0775,
},
{
testCaseName: "directory, invalid permissions (public has write)",
givenFilePermissions: fs.ModeDir | 0757,
},
{
testCaseName: "directory, invalid permissions (everyone has write)",
givenFilePermissions: fs.ModeDir | 0777,
},
{
testCaseName: "executable file, valid permissions",
givenFilePermissions: 0755,
},
{
testCaseName: "executable file, invalid permissions (group has write)",
givenFilePermissions: 0775,
},
{
testCaseName: "executable file, invalid permissions (public has write)",
givenFilePermissions: 0757,
},
{
testCaseName: "executable file, invalid permissions (everyone has write)",
givenFilePermissions: 0777,
},
{
testCaseName: "non-executable file, valid permissions",
givenFilePermissions: 0644,
},
{
testCaseName: "non-executable file, invalid permissions (group has write)",
givenFilePermissions: 0664,
},
{
testCaseName: "non-executable file, invalid permissions (public has write)",
givenFilePermissions: 0646,
},
{
testCaseName: "non-executable file, invalid permissions (everyone has write)",
givenFilePermissions: 0666,
},
}

for _, tt := range testCases {
tt := tt
t.Run(tt.testCaseName, func(t *testing.T) {
t.Parallel()

// Create a temp file to extract a FileInfo from it with tt.givenFilePermissions
tmpDir := t.TempDir()
pathUnderTest := filepath.Join(tmpDir, ulid.New())
if tt.givenFilePermissions.IsDir() {
require.NoError(t, os.MkdirAll(pathUnderTest, tt.givenFilePermissions))
} else {
f, err := os.OpenFile(pathUnderTest, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, tt.givenFilePermissions)
require.NoError(t, err)
require.NoError(t, f.Close())
}
fileInfo, err := os.Stat(pathUnderTest)
require.NoError(t, err)

sanitizedPermissions := sanitizePermissions(fileInfo)

// Confirm no group write
require.True(t, sanitizedPermissions&0020 == 0)

// Confirm no public write
require.True(t, sanitizedPermissions&0002 == 0)

// Confirm type is set correctly
require.Equal(t, tt.givenFilePermissions.Type(), sanitizedPermissions.Type())

// Confirm owner permissions are unmodified
var ownerBits fs.FileMode = 0700
if runtime.GOOS == "windows" {
// Windows doesn't have executable bit
ownerBits = 0600
}
require.Equal(t, tt.givenFilePermissions&ownerBits, sanitizedPermissions&ownerBits)
})
}
}

func TestTidyLibrary(t *testing.T) {
t.Parallel()

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ require (
github.com/gorilla/websocket v1.4.2
github.com/groob/plist v0.0.0-20190114192801-a99fbe489d03
github.com/knightsc/system_policy v1.1.1-0.20211029142728-5f4c0d5419cc
github.com/kolide/kit v0.0.0-20221107170827-fb85e3d59eab
github.com/kolide/kit v0.0.0-20240411131714-94dd1939cf50
github.com/kolide/krypto v0.1.1-0.20231229162826-db516b7e0121
github.com/mat/besticon v3.9.0+incompatible
github.com/mattn/go-sqlite3 v1.14.19
Expand Down
Loading

0 comments on commit 67ade2a

Please sign in to comment.