mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/yunionio/cloudpods.git
synced 2026-09-20 08:03:53 +08:00
fix(guestfs): keep guest file deploy inside the mounted filesystem (#25601)
Co-authored-by: Qiu Jian <qiujian@yunionyun.com> Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -932,6 +932,10 @@ func (self *SGuest) PerformDeploy(
|
||||
input.ResetPassword = true
|
||||
}
|
||||
|
||||
if err := ValidateDeployConfigs(input.DeployConfigs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
driver, err := self.GetDriver()
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "GetDriver")
|
||||
|
||||
42
pkg/compute/models/guest_deploy.go
Normal file
42
pkg/compute/models/guest_deploy.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright 2019 Yunion
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or authorized to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"yunion.io/x/pkg/util/sets"
|
||||
|
||||
api "yunion.io/x/onecloud/pkg/apis/compute"
|
||||
"yunion.io/x/onecloud/pkg/httperrors"
|
||||
"yunion.io/x/onecloud/pkg/util/fileutils2"
|
||||
)
|
||||
|
||||
var deployConfigActions = sets.NewString("", "create", "append")
|
||||
|
||||
func ValidateDeployConfigs(deploys []*api.DeployConfig) error {
|
||||
for i, d := range deploys {
|
||||
if d == nil {
|
||||
continue
|
||||
}
|
||||
if !deployConfigActions.Has(d.Action) {
|
||||
return httperrors.NewInputParameterError("invalid deploy action %q", d.Action)
|
||||
}
|
||||
clean, err := fileutils2.CleanGuestDeployPath(d.Path)
|
||||
if err != nil {
|
||||
return httperrors.NewInputParameterError("deploy_configs[%d].path: %v", i, err)
|
||||
}
|
||||
d.Path = clean
|
||||
}
|
||||
return nil
|
||||
}
|
||||
53
pkg/compute/models/guest_deploy_test.go
Normal file
53
pkg/compute/models/guest_deploy_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright 2019 Yunion
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or authorized to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
api "yunion.io/x/onecloud/pkg/apis/compute"
|
||||
)
|
||||
|
||||
func TestValidateDeployConfigs(t *testing.T) {
|
||||
cfgs := []*api.DeployConfig{
|
||||
{Action: "create", Path: "/etc/hosts", Content: "x"},
|
||||
}
|
||||
if err := ValidateDeployConfigs(cfgs); err != nil {
|
||||
t.Fatalf("valid: %v", err)
|
||||
}
|
||||
if cfgs[0].Path != "/etc/hosts" {
|
||||
t.Fatalf("path %q", cfgs[0].Path)
|
||||
}
|
||||
|
||||
rel := []*api.DeployConfig{{Action: "create", Path: "attack/etc/pwn"}}
|
||||
if err := ValidateDeployConfigs(rel); err == nil {
|
||||
t.Fatal("expected relative path to fail")
|
||||
}
|
||||
root := []*api.DeployConfig{{Action: "create", Path: "/"}}
|
||||
if err := ValidateDeployConfigs(root); err == nil {
|
||||
t.Fatal("expected / to fail")
|
||||
}
|
||||
badAct := []*api.DeployConfig{{Action: "exec", Path: "/etc/hosts"}}
|
||||
if err := ValidateDeployConfigs(badAct); err == nil {
|
||||
t.Fatal("expected invalid action to fail")
|
||||
}
|
||||
cleaned := []*api.DeployConfig{{Path: "/etc/../root/.ssh/authorized_keys"}}
|
||||
if err := ValidateDeployConfigs(cleaned); err != nil {
|
||||
t.Fatalf("cleaned path: %v", err)
|
||||
}
|
||||
if cleaned[0].Path != "/root/.ssh/authorized_keys" {
|
||||
t.Fatalf("got %q", cleaned[0].Path)
|
||||
}
|
||||
}
|
||||
@@ -1762,6 +1762,10 @@ func (manager *SGuestManager) validateCreateData(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := ValidateDeployConfigs(input.DeployConfigs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(input.Metadata) > 20 {
|
||||
return nil, httperrors.NewInputParameterError("metadata must be less than 20")
|
||||
}
|
||||
@@ -5606,6 +5610,9 @@ func (self *SGuest) GetDeployConfigOnHost(ctx context.Context, userCred mcclient
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ValidateDeployConfigs(deploys); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(deploys) > 0 {
|
||||
config.Add(jsonutils.Marshal(deploys), "deploys")
|
||||
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
"yunion.io/x/onecloud/pkg/httperrors"
|
||||
"yunion.io/x/onecloud/pkg/mcclient"
|
||||
modules "yunion.io/x/onecloud/pkg/mcclient/modules/compute"
|
||||
"yunion.io/x/onecloud/pkg/util/fileutils2"
|
||||
"yunion.io/x/onecloud/pkg/util/procutils"
|
||||
)
|
||||
|
||||
@@ -59,6 +60,11 @@ func (d *sGuestRootFsDriver) DeployFiles(deploys []*deployapi.DeployContent) err
|
||||
if len(deploy.Path) == 0 {
|
||||
return fmt.Errorf("Deploy file missing param path")
|
||||
}
|
||||
clean, err := fileutils2.CleanGuestDeployPath(deploy.Path)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "deploy path")
|
||||
}
|
||||
deploy.Path = clean
|
||||
dirname := filepath.Dir(deploy.Path)
|
||||
if !d.GetPartition().Exists(dirname, caseInsensitive) {
|
||||
modeRWXOwner := syscall.S_IRWXU | syscall.S_IRGRP | syscall.S_IXGRP | syscall.S_IROTH | syscall.S_IXOTH
|
||||
|
||||
@@ -562,6 +562,9 @@ func (l *sLinuxRootFs) GetArch(rootFs IDiskPartition) string {
|
||||
log.Errorf("readlink of %s: %s", p, err)
|
||||
continue
|
||||
}
|
||||
if mnt := rootFs.GetMountPath(); mnt != "" && !fileutils2.IsPathInside(mnt, rp) {
|
||||
continue
|
||||
}
|
||||
elfHeader, err := elf.Open(rp)
|
||||
if err != nil {
|
||||
log.Errorf("failed read file elf %s: %s", rp, err)
|
||||
@@ -2462,7 +2465,11 @@ func (d *SCoreOsRootFs) GetLoginAccount(rootFs IDiskPartition, user string, defa
|
||||
|
||||
func (d *SCoreOsRootFs) DeployFiles(deploys []*deployapi.DeployContent) error {
|
||||
for _, deploy := range deploys {
|
||||
d.GetConfig().AddWriteFile(deploy.Path, deploy.Content, "", "", false)
|
||||
clean, err := fileutils2.CleanGuestDeployPath(deploy.Path)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "deploy path")
|
||||
}
|
||||
d.GetConfig().AddWriteFile(clean, deploy.Content, "", "", false)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -16,10 +16,10 @@ package kvmpart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"yunion.io/x/log"
|
||||
@@ -48,33 +48,111 @@ func (f *SLocalGuestFS) SupportSerialPorts() bool {
|
||||
}
|
||||
|
||||
func (f *SLocalGuestFS) GetLocalPath(sPath string, caseInsensitive bool) string {
|
||||
p, err := f.resolveLocalPath(sPath, caseInsensitive, false)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (f *SLocalGuestFS) resolveLocalPath(sPath string, caseInsensitive bool, allowMissingLast bool) (string, error) {
|
||||
if sPath == "." {
|
||||
sPath = ""
|
||||
}
|
||||
var fullPath = f.mountPath
|
||||
pathSegs := strings.Split(sPath, "/")
|
||||
for _, seg := range pathSegs {
|
||||
if len(seg) > 0 {
|
||||
var realSeg string
|
||||
files, _ := ioutil.ReadDir(fullPath)
|
||||
for _, file := range files {
|
||||
var f = file.Name()
|
||||
if f == seg || (caseInsensitive && strings.ToLower(f) == strings.ToLower(seg)) ||
|
||||
(seg[len(seg)-1] == '*' && (strings.HasPrefix(f, seg[:len(seg)-1]) ||
|
||||
(caseInsensitive && strings.HasPrefix(strings.ToLower(f),
|
||||
strings.ToLower(seg[:len(seg)-1]))))) {
|
||||
realSeg = f
|
||||
break
|
||||
}
|
||||
mount := filepath.Clean(f.mountPath)
|
||||
fullPath := mount
|
||||
segs := strings.Split(sPath, "/")
|
||||
for i, seg := range segs {
|
||||
if len(seg) == 0 || seg == "." {
|
||||
continue
|
||||
}
|
||||
if seg == ".." {
|
||||
parent := filepath.Dir(fullPath)
|
||||
if !fileutils2.IsPathInside(mount, parent) {
|
||||
return "", errors.Errorf("path %q is outside mount", sPath)
|
||||
}
|
||||
if len(realSeg) > 0 {
|
||||
fullPath = path.Join(fullPath, realSeg)
|
||||
} else {
|
||||
return ""
|
||||
fullPath = parent
|
||||
continue
|
||||
}
|
||||
isLast := i == len(segs)-1
|
||||
realSeg, fi, err := f.lookupSeg(fullPath, seg, caseInsensitive)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if realSeg == "" {
|
||||
if allowMissingLast && isLast {
|
||||
joined := filepath.Join(fullPath, seg)
|
||||
if !fileutils2.IsPathInside(mount, joined) {
|
||||
return "", errors.Errorf("path %q is outside mount", sPath)
|
||||
}
|
||||
return joined, nil
|
||||
}
|
||||
return "", errors.Errorf("path %q not found", sPath)
|
||||
}
|
||||
next := filepath.Join(fullPath, realSeg)
|
||||
if !fileutils2.IsPathInside(mount, next) {
|
||||
return "", errors.Errorf("path %q is outside mount", sPath)
|
||||
}
|
||||
if fi != nil && !isLast {
|
||||
if fi.Mode()&os.ModeSymlink != 0 {
|
||||
return "", errors.Errorf("path %q traverses a symlink", sPath)
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
return "", errors.Errorf("path %q traverses a non-directory", sPath)
|
||||
}
|
||||
}
|
||||
fullPath = next
|
||||
}
|
||||
return fullPath
|
||||
if !fileutils2.IsPathInside(mount, fullPath) {
|
||||
return "", errors.Errorf("path %q is outside mount", sPath)
|
||||
}
|
||||
return fullPath, nil
|
||||
}
|
||||
|
||||
func (f *SLocalGuestFS) lookupSeg(dir, seg string, caseInsensitive bool) (string, os.FileInfo, error) {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
var realSeg string
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
match := name == seg
|
||||
if !match && caseInsensitive && strings.EqualFold(name, seg) {
|
||||
match = true
|
||||
}
|
||||
if !match && len(seg) > 0 && seg[len(seg)-1] == '*' {
|
||||
prefix := seg[:len(seg)-1]
|
||||
match = strings.HasPrefix(name, prefix) ||
|
||||
(caseInsensitive && strings.HasPrefix(strings.ToLower(name), strings.ToLower(prefix)))
|
||||
}
|
||||
if match {
|
||||
realSeg = name
|
||||
break
|
||||
}
|
||||
}
|
||||
if realSeg == "" {
|
||||
return "", nil, nil
|
||||
}
|
||||
fi, err := os.Lstat(filepath.Join(dir, realSeg))
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return realSeg, fi, nil
|
||||
}
|
||||
|
||||
func (f *SLocalGuestFS) mustRegularDir(p string) error {
|
||||
fi, err := os.Lstat(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fi.Mode()&os.ModeSymlink != 0 {
|
||||
return errors.Errorf("%s is a symlink", p)
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
return errors.Errorf("%s is not a directory", p)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *SLocalGuestFS) Remove(path string, caseInsensitive bool) {
|
||||
@@ -88,16 +166,25 @@ func (f *SLocalGuestFS) Mkdir(sPath string, mode int, caseInsensitive bool) erro
|
||||
segs := strings.Split(sPath, "/")
|
||||
sPath = ""
|
||||
pPath := f.GetLocalPath("/", caseInsensitive)
|
||||
if err := f.mustRegularDir(pPath); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, s := range segs {
|
||||
if len(s) > 0 {
|
||||
sPath = path.Join(sPath, s)
|
||||
vPath := f.GetLocalPath(sPath, caseInsensitive)
|
||||
if len(vPath) == 0 {
|
||||
if err := f.mustRegularDir(pPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Mkdir(path.Join(pPath, s), os.FileMode(mode)); err != nil {
|
||||
return err
|
||||
}
|
||||
pPath = f.GetLocalPath(sPath, caseInsensitive)
|
||||
} else {
|
||||
if err := f.mustRegularDir(vPath); err != nil {
|
||||
return err
|
||||
}
|
||||
pPath = vPath
|
||||
}
|
||||
}
|
||||
@@ -108,7 +195,11 @@ func (f *SLocalGuestFS) Mkdir(sPath string, mode int, caseInsensitive bool) erro
|
||||
func (f *SLocalGuestFS) ListDir(sPath string, caseInsensitive bool) []string {
|
||||
sPath = f.GetLocalPath(sPath, caseInsensitive)
|
||||
if len(sPath) > 0 {
|
||||
files, err := ioutil.ReadDir(sPath)
|
||||
if err := f.mustRegularDir(sPath); err != nil {
|
||||
log.Errorln(err)
|
||||
return nil
|
||||
}
|
||||
files, err := os.ReadDir(sPath)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
return nil
|
||||
@@ -155,7 +246,7 @@ func (f *SLocalGuestFS) Passwd(account, password string, caseInsensitive bool) e
|
||||
func (f *SLocalGuestFS) Stat(usrDir string, caseInsensitive bool) os.FileInfo {
|
||||
sPath := f.GetLocalPath(usrDir, caseInsensitive)
|
||||
if len(sPath) > 0 {
|
||||
fileInfo, err := os.Stat(sPath)
|
||||
fileInfo, err := os.Lstat(sPath)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
}
|
||||
@@ -173,6 +264,9 @@ func (f *SLocalGuestFS) Symlink(src string, dst string, caseInsensitive bool) er
|
||||
f.Remove(dst, caseInsensitive)
|
||||
}
|
||||
dir = f.GetLocalPath(dir, caseInsensitive)
|
||||
if err := f.mustRegularDir(dir); err != nil {
|
||||
return err
|
||||
}
|
||||
dst = path.Join(dir, path.Base(dst))
|
||||
return os.Symlink(src, dst)
|
||||
}
|
||||
@@ -188,6 +282,9 @@ func (f *SLocalGuestFS) Exists(sPath string, caseInsensitive bool) bool {
|
||||
func (f *SLocalGuestFS) Chown(sPath string, uid, gid int, caseInsensitive bool) error {
|
||||
sPath = f.GetLocalPath(sPath, caseInsensitive)
|
||||
if len(sPath) > 0 {
|
||||
if fileutils2.IsSymlink(sPath) {
|
||||
return errors.Errorf("cannot chown symlink %s", sPath)
|
||||
}
|
||||
return os.Chown(sPath, uid, gid)
|
||||
}
|
||||
return nil
|
||||
@@ -196,6 +293,9 @@ func (f *SLocalGuestFS) Chown(sPath string, uid, gid int, caseInsensitive bool)
|
||||
func (f *SLocalGuestFS) Chmod(sPath string, mode uint32, caseInsensitive bool) error {
|
||||
sPath = f.GetLocalPath(sPath, caseInsensitive)
|
||||
if len(sPath) > 0 {
|
||||
if fileutils2.IsSymlink(sPath) {
|
||||
return errors.Errorf("cannot chmod symlink %s", sPath)
|
||||
}
|
||||
return os.Chmod(sPath, os.FileMode(mode))
|
||||
}
|
||||
return nil
|
||||
@@ -206,10 +306,14 @@ func (f *SLocalGuestFS) updateUserEtcShadow(username string) error {
|
||||
if !fileutils2.Exists(sPath) {
|
||||
return nil
|
||||
}
|
||||
content, err := fileutils2.FileGetContents(sPath)
|
||||
if fileutils2.IsSymlink(sPath) {
|
||||
return errors.Errorf("cannot update symlink %s", sPath)
|
||||
}
|
||||
contentBytes, err := fileutils2.FileGetContentsNoFollow(sPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "read /etc/shadow")
|
||||
}
|
||||
content := string(contentBytes)
|
||||
|
||||
var (
|
||||
minimumDays = "0" // -m 0
|
||||
@@ -231,7 +335,7 @@ func (f *SLocalGuestFS) updateUserEtcShadow(username string) error {
|
||||
}
|
||||
}
|
||||
newContent := strings.Join(lines, "\n")
|
||||
err = fileutils2.FilePutContents(sPath, newContent, false)
|
||||
err = fileutils2.FilePutContentsNoFollow(sPath, newContent, false)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "read %s, put %s to /etc/shadow", content, newContent)
|
||||
}
|
||||
@@ -316,27 +420,28 @@ func (f *SLocalGuestFS) FileGetContents(sPath string, caseInsensitive bool) ([]b
|
||||
}
|
||||
|
||||
func (f *SLocalGuestFS) FileGetContentsByPath(sPath string) ([]byte, error) {
|
||||
if len(sPath) > 0 {
|
||||
return ioutil.ReadFile(sPath)
|
||||
if len(sPath) == 0 {
|
||||
return nil, fmt.Errorf("Cann't find local path")
|
||||
}
|
||||
return nil, fmt.Errorf("Cann't find local path")
|
||||
if fileutils2.IsSymlink(sPath) {
|
||||
return nil, errors.Errorf("cannot read symlink %s", sPath)
|
||||
}
|
||||
return fileutils2.FileGetContentsNoFollow(sPath)
|
||||
}
|
||||
|
||||
func (f *SLocalGuestFS) FilePutContents(sPath, content string, modAppend, caseInsensitive bool) error {
|
||||
sFilePath := f.GetLocalPath(sPath, caseInsensitive)
|
||||
if len(sFilePath) > 0 {
|
||||
sPath = sFilePath
|
||||
} else {
|
||||
dirPath := f.GetLocalPath(path.Dir(sPath), caseInsensitive)
|
||||
if len(dirPath) > 0 {
|
||||
sPath = path.Join(dirPath, path.Base(sPath))
|
||||
}
|
||||
target, err := f.resolveLocalPath(sPath, caseInsensitive, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(sPath) > 0 {
|
||||
return fileutils2.FilePutContents(sPath, content, modAppend)
|
||||
} else {
|
||||
return fmt.Errorf("Can't put content to empty Path")
|
||||
if fileutils2.IsSymlink(target) {
|
||||
return errors.Errorf("cannot write through symlink %s", sPath)
|
||||
}
|
||||
parent := filepath.Dir(target)
|
||||
if err := f.mustRegularDir(parent); err != nil {
|
||||
return err
|
||||
}
|
||||
return fileutils2.FilePutContentsNoFollow(target, content, modAppend)
|
||||
}
|
||||
|
||||
func (f *SLocalGuestFS) GenerateSshHostKeys() error {
|
||||
|
||||
100
pkg/hostman/guestfs/kvmpart/localfs_test.go
Normal file
100
pkg/hostman/guestfs/kvmpart/localfs_test.go
Normal file
@@ -0,0 +1,100 @@
|
||||
// Copyright 2019 Yunion
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or authorized to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package kvmpart
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetLocalPathDoesNotFollowSymlink(t *testing.T) {
|
||||
mount := t.TempDir()
|
||||
fs := NewLocalGuestFS(mount)
|
||||
if err := os.MkdirAll(filepath.Join(mount, "etc"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink("/", filepath.Join(mount, "attack")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if p := fs.GetLocalPath("/attack/etc", false); p != "" {
|
||||
t.Fatalf("expected empty path, got %q", p)
|
||||
}
|
||||
if p := fs.GetLocalPath("/etc", false); p != filepath.Join(mount, "etc") {
|
||||
t.Fatalf("got %q", p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilePutContentsDoesNotFollowSymlink(t *testing.T) {
|
||||
mount := t.TempDir()
|
||||
fs := NewLocalGuestFS(mount)
|
||||
if err := os.MkdirAll(filepath.Join(mount, "etc"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink("/", filepath.Join(mount, "attack")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := fs.FilePutContents("/attack/etc/pwn", "x", false, false); err == nil {
|
||||
t.Fatal("expected write through symlink prefix to fail")
|
||||
}
|
||||
|
||||
marker := filepath.Join(t.TempDir(), "host-target")
|
||||
if err := os.WriteFile(marker, []byte("orig"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink(marker, filepath.Join(mount, "etc", "hosts")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := fs.FilePutContents("/etc/hosts", "pwn", false, false); err == nil {
|
||||
t.Fatal("expected write through symlink file to fail")
|
||||
}
|
||||
got, err := os.ReadFile(marker)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got) != "orig" {
|
||||
t.Fatalf("host file changed: %q", got)
|
||||
}
|
||||
|
||||
if err := fs.FilePutContents("/etc/hostname", "vm1", false, false); err != nil {
|
||||
t.Fatalf("regular write: %v", err)
|
||||
}
|
||||
got, err = os.ReadFile(filepath.Join(mount, "etc", "hostname"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got) != "vm1" {
|
||||
t.Fatalf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMkdirDoesNotFollowSymlink(t *testing.T) {
|
||||
mount := t.TempDir()
|
||||
fs := NewLocalGuestFS(mount)
|
||||
if err := os.Symlink("/", filepath.Join(mount, "attack")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := fs.Mkdir("/attack/etc/cron.d", 0755, false); err == nil {
|
||||
t.Fatal("expected mkdir through symlink to fail")
|
||||
}
|
||||
if err := fs.Mkdir("/etc/cron.d", 0755, false); err != nil {
|
||||
t.Fatalf("regular mkdir: %v", err)
|
||||
}
|
||||
if st, err := os.Stat(filepath.Join(mount, "etc", "cron.d")); err != nil || !st.IsDir() {
|
||||
t.Fatalf("cron.d not created: %v", err)
|
||||
}
|
||||
}
|
||||
103
pkg/util/fileutils2/nofollow.go
Normal file
103
pkg/util/fileutils2/nofollow.go
Normal file
@@ -0,0 +1,103 @@
|
||||
// Copyright 2019 Yunion
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or authorized to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package fileutils2
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"yunion.io/x/pkg/errors"
|
||||
)
|
||||
|
||||
// IsPathInside reports whether target is the same as base or a descendant of it.
|
||||
func IsPathInside(base, target string) bool {
|
||||
base = filepath.Clean(base)
|
||||
target = filepath.Clean(target)
|
||||
rel, err := filepath.Rel(base, target)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
||||
return false
|
||||
}
|
||||
return !filepath.IsAbs(rel)
|
||||
}
|
||||
|
||||
// CleanGuestDeployPath requires an absolute guest path and rejects parent-dir
|
||||
// traversal after cleaning.
|
||||
func CleanGuestDeployPath(p string) (string, error) {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == "" {
|
||||
return "", errors.Errorf("path is empty")
|
||||
}
|
||||
if strings.ContainsRune(p, 0) {
|
||||
return "", errors.Errorf("path contains NUL")
|
||||
}
|
||||
if !path.IsAbs(p) && !filepath.IsAbs(p) {
|
||||
return "", errors.Errorf("path %q must be absolute", p)
|
||||
}
|
||||
clean := path.Clean("/" + strings.TrimPrefix(filepath.ToSlash(p), "/"))
|
||||
if !path.IsAbs(clean) {
|
||||
return "", errors.Errorf("path %q must be absolute", p)
|
||||
}
|
||||
if clean == "/" {
|
||||
return "", errors.Errorf("path %q is not allowed", p)
|
||||
}
|
||||
if clean == ".." || strings.HasPrefix(clean, "../") {
|
||||
return "", errors.Errorf("path %q is not allowed", p)
|
||||
}
|
||||
return clean, nil
|
||||
}
|
||||
|
||||
func IsSymlink(path string) bool {
|
||||
fi, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return fi.Mode()&os.ModeSymlink != 0
|
||||
}
|
||||
|
||||
func OpenFileNoFollow(name string, flag int, perm os.FileMode) (*os.File, error) {
|
||||
return os.OpenFile(name, flag|syscall.O_NOFOLLOW, perm)
|
||||
}
|
||||
|
||||
func FilePutContentsNoFollow(filename string, content string, modAppend bool) error {
|
||||
mode := os.O_WRONLY | os.O_CREATE
|
||||
if modAppend {
|
||||
mode |= os.O_APPEND
|
||||
} else {
|
||||
mode |= os.O_TRUNC
|
||||
}
|
||||
fd, err := OpenFileNoFollow(filename, mode, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fd.Close()
|
||||
_, err = fd.WriteString(content)
|
||||
return err
|
||||
}
|
||||
|
||||
func FileGetContentsNoFollow(filename string) ([]byte, error) {
|
||||
fd, err := OpenFileNoFollow(filename, os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fd.Close()
|
||||
return io.ReadAll(fd)
|
||||
}
|
||||
91
pkg/util/fileutils2/nofollow_test.go
Normal file
91
pkg/util/fileutils2/nofollow_test.go
Normal file
@@ -0,0 +1,91 @@
|
||||
// Copyright 2019 Yunion
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or authorized to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package fileutils2
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsPathInside(t *testing.T) {
|
||||
if !IsPathInside("/mnt/guest", "/mnt/guest") {
|
||||
t.Fatal("self")
|
||||
}
|
||||
if !IsPathInside("/mnt/guest", "/mnt/guest/etc/hosts") {
|
||||
t.Fatal("child")
|
||||
}
|
||||
if IsPathInside("/mnt/guest", "/etc/hosts") {
|
||||
t.Fatal("outside")
|
||||
}
|
||||
if IsPathInside("/mnt/guest", "/mnt/guest/../etc") {
|
||||
t.Fatal("escape")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilePutContentsNoFollow(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
target := filepath.Join(dir, "real")
|
||||
link := filepath.Join(dir, "link")
|
||||
if err := os.WriteFile(target, []byte("orig"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink(target, link); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := FilePutContentsNoFollow(link, "pwn", false); err == nil {
|
||||
t.Fatal("expected write through symlink to fail")
|
||||
}
|
||||
got, err := os.ReadFile(target)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got) != "orig" {
|
||||
t.Fatalf("target changed: %q", got)
|
||||
}
|
||||
regular := filepath.Join(dir, "file")
|
||||
if err := FilePutContentsNoFollow(regular, "ok", false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err = os.ReadFile(regular)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got) != "ok" {
|
||||
t.Fatalf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanGuestDeployPath(t *testing.T) {
|
||||
got, err := CleanGuestDeployPath("/etc/hosts")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != "/etc/hosts" {
|
||||
t.Fatalf("got %q", got)
|
||||
}
|
||||
got, err = CleanGuestDeployPath("/etc/../root/.ssh/authorized_keys")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != "/root/.ssh/authorized_keys" {
|
||||
t.Fatalf("got %q", got)
|
||||
}
|
||||
for _, p := range []string{"", "relative", "/", "/."} {
|
||||
if _, err := CleanGuestDeployPath(p); err == nil {
|
||||
t.Fatalf("expected error for %q", p)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user