don't follow symlinks when writing copyToHost destination

Signed-off-by: nashit hayyat <nashit@bugqore.com>
This commit is contained in:
nashit hayyat committed 2026-07-21 15:36:29 +05:30
1 parent 549afab3f3
commit a32fa6a2c9
3 files changed
+66 -1

No files matched your search

+3 -1
View File
@@ -1316,7 +1316,9 @@ func copyToHost(ctx context.Context, sshConfig *ssh.SSHConfig, sshAddress string
return fmt.Errorf("failed to run %v: %#q: %w", cmd.Args, string(out), err)
}
}
if err := os.WriteFile(local, out, 0o600); err != nil {
// The destination may sit inside a writable mount, so don't follow a
// symlink planted there by the guest.
if err := osutil.WriteFileBeneathDir(local, out, 0o600); err != nil {
return fmt.Errorf("can't write to local file %#q: %w", local, err)
}
return nil
+25
View File
@@ -6,6 +6,7 @@ package osutil
import (
"errors"
"os"
"path/filepath"
)
// FileExists reports whether path exists and is accessible.
@@ -23,3 +24,27 @@ func Touch(path string) error {
}
return f.Close()
}
// WriteFileBeneathDir writes data to name without following a symlink at the
// final path component. It is meant for destinations that untrusted code may be
// able to write to, e.g. a path inside a writable mount: a symlink planted at
// name (say pointing at ~/.ssh/authorized_keys) would otherwise get the bytes
// written through the link to a file outside the intended directory.
// os.OpenRoot confines the write to name's parent directory, so a symlink whose
// target escapes that directory is refused.
func WriteFileBeneathDir(name string, data []byte, perm os.FileMode) error {
root, err := os.OpenRoot(filepath.Dir(name))
if err != nil {
return err
}
defer root.Close()
f, err := root.OpenFile(filepath.Base(name), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
if err != nil {
return err
}
if _, err := f.Write(data); err != nil {
_ = f.Close()
return err
}
return f.Close()
}
+38
View File
@@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: Copyright The Lima Authors
// SPDX-License-Identifier: Apache-2.0
package osutil
import (
"os"
"path/filepath"
"testing"
"gotest.tools/v3/assert"
)
func TestWriteFileBeneathDir(t *testing.T) {
base := t.TempDir()
dir := filepath.Join(base, "mnt")
assert.NilError(t, os.MkdirAll(dir, 0o700))
// A regular destination is written normally.
regular := filepath.Join(dir, "config")
assert.NilError(t, WriteFileBeneathDir(regular, []byte("ok\n"), 0o600))
got, err := os.ReadFile(regular)
assert.NilError(t, err)
assert.Equal(t, string(got), "ok\n")
// A symlink at the destination pointing outside the parent directory is
// refused, and the link target is left untouched.
secret := filepath.Join(base, "authorized_keys")
assert.NilError(t, os.WriteFile(secret, []byte("ORIGINAL\n"), 0o600))
linked := filepath.Join(dir, "linked")
assert.NilError(t, os.Symlink(secret, linked))
err = WriteFileBeneathDir(linked, []byte("ATTACKER\n"), 0o600)
assert.Assert(t, err != nil, "expected the symlinked write to be refused")
got, err = os.ReadFile(secret)
assert.NilError(t, err)
assert.Equal(t, string(got), "ORIGINAL\n")
}