Skip to content

Commit 0fe89e2

Browse files
authored
Merge pull request #105 from hashicorp/sec/os-root
sec: use os.Root for file manipulation
2 parents 225b79e + f7e8fff commit 0fe89e2

File tree

9 files changed

+295
-36
lines changed

9 files changed

+295
-36
lines changed

go.mod

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
module github.com/hashicorp/go-slug
22

3-
go 1.24
3+
go 1.25
44

55
require (
66
github.com/apparentlymart/go-versions v1.0.1
77
github.com/google/go-cmp v0.7.0
88
github.com/hashicorp/terraform-registry-address v0.4.0
99
github.com/hashicorp/terraform-svchost v0.1.1
10-
golang.org/x/mod v0.26.0
11-
golang.org/x/sys v0.35.0
10+
golang.org/x/mod v0.28.0
11+
golang.org/x/sys v0.36.0
1212
)
1313

1414
require (
1515
github.com/go-test/deep v1.0.3 // indirect
16-
golang.org/x/net v0.43.0 // indirect
17-
golang.org/x/text v0.28.0 // indirect
16+
golang.org/x/net v0.44.0 // indirect
17+
golang.org/x/text v0.29.0 // indirect
1818
)

go.sum

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S
1313
github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc=
1414
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
1515
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
16-
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
17-
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
18-
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
19-
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
20-
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
21-
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
22-
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
23-
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
16+
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
17+
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
18+
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
19+
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
20+
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
21+
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
22+
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
23+
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=

internal/ignorefiles/ignorerules.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"fmt"
1212
"io"
1313
"os"
14-
"path/filepath"
1514
)
1615

1716
// A Ruleset is the result of reading, parsing, and compiling a
@@ -48,7 +47,14 @@ func ParseIgnoreFileContent(r io.Reader) (*Ruleset, error) {
4847
// This function will return an error only if an ignore file is present but
4948
// unreadable, or if an ignore file is present but contains invalid syntax.
5049
func LoadPackageIgnoreRules(packageDir string) (*Ruleset, error) {
51-
file, err := os.Open(filepath.Join(packageDir, ".terraformignore"))
50+
// Use os.Root for secure file access within the package directory
51+
packageRoot, err := os.OpenRoot(packageDir)
52+
if err != nil {
53+
return nil, fmt.Errorf("cannot open package directory %q: %s", packageDir, err)
54+
}
55+
defer func() { _ = packageRoot.Close() }()
56+
57+
file, err := packageRoot.Open(".terraformignore")
5258
if err != nil {
5359
if os.IsNotExist(err) {
5460
return DefaultRuleset, nil

internal/unpackinfo/unpackinfo.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,28 @@ func (i UnpackInfo) RestoreInfo() error {
140140
}
141141
}
142142

143+
// RestoreInfoWithRoot changes the file mode and timestamps for the given UnpackInfo data
144+
// using os.Root for enhanced security to ensure operations stay within the destination
145+
func (i UnpackInfo) RestoreInfoWithRoot(root *os.Root, dst string) error {
146+
// Calculate relative path from dst to i.Path for Root operations
147+
relPath, err := filepath.Rel(dst, i.Path)
148+
if err != nil {
149+
return fmt.Errorf("failed to get relative path: %w", err)
150+
}
151+
152+
switch {
153+
case i.IsDirectory():
154+
return i.restoreDirectoryWithRoot(root, relPath)
155+
case i.IsSymlink():
156+
if CanMaintainSymlinkTimestamps() {
157+
return i.restoreSymlinkWithRoot(root, relPath)
158+
}
159+
return nil
160+
default: // Normal file
161+
return i.restoreNormalWithRoot(root, relPath)
162+
}
163+
}
164+
143165
func (i UnpackInfo) restoreDirectory() error {
144166
if err := os.Chmod(i.Path, i.OriginalMode); err != nil && !os.IsNotExist(err) {
145167
return fmt.Errorf("failed setting permissions on directory %q: %w", i.Path, err)
@@ -168,3 +190,34 @@ func (i UnpackInfo) restoreNormal() error {
168190
}
169191
return nil
170192
}
193+
194+
func (i UnpackInfo) restoreDirectoryWithRoot(root *os.Root, relPath string) error {
195+
if err := root.Chmod(relPath, i.OriginalMode); err != nil && !os.IsNotExist(err) {
196+
return fmt.Errorf("failed setting permissions on directory %q: %w", i.Path, err)
197+
}
198+
199+
if err := root.Chtimes(relPath, i.OriginalAccessTime, i.OriginalModTime); err != nil && !os.IsNotExist(err) {
200+
return fmt.Errorf("failed setting times on directory %q: %w", i.Path, err)
201+
}
202+
return nil
203+
}
204+
205+
func (i UnpackInfo) restoreSymlinkWithRoot(root *os.Root, relPath string) error {
206+
// Note: Go 1.25's os.Root doesn't have Lchtimes, so we fall back to the original method
207+
// This is a limitation but still provides some security benefit for other operations
208+
if err := i.Lchtimes(); err != nil {
209+
return fmt.Errorf("failed setting times on symlink %q: %w", i.Path, err)
210+
}
211+
return nil
212+
}
213+
214+
func (i UnpackInfo) restoreNormalWithRoot(root *os.Root, relPath string) error {
215+
if err := root.Chmod(relPath, i.OriginalMode); err != nil {
216+
return fmt.Errorf("failed setting permissions on %q: %w", i.Path, err)
217+
}
218+
219+
if err := root.Chtimes(relPath, i.OriginalAccessTime, i.OriginalModTime); err != nil {
220+
return fmt.Errorf("failed setting times on %q: %w", i.Path, err)
221+
}
222+
return nil
223+
}

slug.go

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,13 @@ func (p *Packer) Unpack(r io.Reader, dst string) error {
390390
// for more details about how tar attempts to preserve file metadata.
391391
directoriesExtracted := []unpackinfo.UnpackInfo{}
392392

393+
// Create a root for secure file operations - this prevents access outside dst
394+
root, err := os.OpenRoot(dst)
395+
if err != nil {
396+
return fmt.Errorf("failed to open root directory %q: %w", dst, err)
397+
}
398+
defer func() { _ = root.Close() }()
399+
393400
// Decompress as we read.
394401
uncompressed, err := gzip.NewReader(r)
395402
if err != nil {
@@ -413,36 +420,37 @@ func (p *Packer) Unpack(r io.Reader, dst string) error {
413420
if header.Name == "" {
414421
continue
415422
}
423+
header.Name = filepath.Clean(header.Name)
416424

417425
info, err := unpackinfo.NewUnpackInfo(dst, header)
418426
if err != nil {
419427
return &IllegalSlugError{Err: err}
420428
}
421429

422-
// Make the directories to the path.
423-
dir := filepath.Dir(info.Path)
424-
425-
// Timestamps and permissions will be restored after all files are extracted.
426-
if err := os.MkdirAll(dir, 0755); err != nil {
427-
return fmt.Errorf("failed to create directory %q: %w", dir, err)
430+
// Make the directories to the path using root for security.
431+
dir := filepath.Dir(header.Name)
432+
if dir != "." {
433+
// Timestamps and permissions will be restored after all files are extracted.
434+
if err := root.MkdirAll(dir, 0755); err != nil {
435+
return fmt.Errorf("failed to create directory %q: %w", dir, err)
436+
}
428437
}
429438

430439
// Handle symlinks, directories, non-regular files
431440
if info.IsSymlink() {
432441

433442
if ok, err := p.validSymlink(dst, header.Name, header.Linkname); ok {
434-
// Create the symlink.
435-
headerName := filepath.Clean(header.Name)
443+
// Create the symlink using root for security.
436444
headerLinkname := filepath.Clean(header.Linkname)
437-
if err = os.Symlink(headerLinkname, info.Path); err != nil {
445+
if err = root.Symlink(headerLinkname, header.Name); err != nil {
438446
return fmt.Errorf("failed creating symlink (%q -> %q): %w",
439-
headerName, headerLinkname, err)
447+
header.Name, headerLinkname, err)
440448
}
441449
} else {
442450
return err
443451
}
444452

445-
if err := info.RestoreInfo(); err != nil {
453+
if err := info.RestoreInfoWithRoot(root, dst); err != nil {
446454
return err
447455
}
448456

@@ -461,16 +469,16 @@ func (p *Packer) Unpack(r io.Reader, dst string) error {
461469
continue
462470
}
463471

464-
// Open a handle to the destination.
465-
fh, err := os.Create(info.Path)
472+
// Open a handle to the destination using root for security.
473+
fh, err := root.Create(header.Name)
466474
if err != nil {
467475
// This mimics tar's behavior wrt the tar file containing duplicate files
468476
// and it allowing later ones to clobber earlier ones even if the file
469477
// has perms that don't allow overwriting. The file permissions will be restored
470478
// once the file contents are copied.
471479
if os.IsPermission(err) {
472-
_ = os.Chmod(info.Path, 0600)
473-
fh, err = os.Create(info.Path)
480+
_ = root.Chmod(header.Name, 0600)
481+
fh, err = root.Create(header.Name)
474482
}
475483

476484
if err != nil {
@@ -485,13 +493,13 @@ func (p *Packer) Unpack(r io.Reader, dst string) error {
485493
return fmt.Errorf("failed to copy slug file %q: %w", info.Path, err)
486494
}
487495

488-
if err := info.RestoreInfo(); err != nil {
496+
if err := info.RestoreInfoWithRoot(root, dst); err != nil {
489497
return err
490498
}
491499
}
492500

493501
for _, dir := range directoriesExtracted {
494-
if err := dir.RestoreInfo(); err != nil {
502+
if err := dir.RestoreInfoWithRoot(root, dst); err != nil {
495503
return err
496504
}
497505
}

0 commit comments

Comments
 (0)