fix: Prevent provider binary being placed outside of .terraform/providers cache unexpectedly due to use of symlinks.

pull/38611/head
Sarah French 4 days ago
parent b046d8a010
commit 90fcc2cf69

@ -0,0 +1,5 @@
kind: BUG FIXES
body: 'init: Prevent provider binaries from being installed into symlinked directories'
time: 2026-05-18T16:49:38.375301+01:00
custom:
Issue: "38611"

@ -10,6 +10,7 @@ import (
"strings"
"testing"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/e2e"
"github.com/hashicorp/terraform/internal/getproviders"
)
@ -271,3 +272,54 @@ terraform {
}
}
`
func TestSymlinkProviderTargetDirectory(t *testing.T) {
// This test reaches out to releases.hashicorp.com to download the
// null provider, so it can only run if network access is allowed.
skipIfCannotAccessNetwork(t)
fixturePath := filepath.Join("testdata", "provider-tampering-base")
tf := e2e.NewBinary(t, terraformBin, fixturePath)
// Create a directory that will be symlinked to inside the plugin cache directory.
//
// If the provider is downloaded without protections against symlinks present,
// the binary will be downloaded to ./other-dir, not the expected
// ./terraform/providers/registry.terraform.io/hashicorp/null/3.1.0/<platform>/ location
otherDir := tf.Path("other-dir")
err := os.MkdirAll(otherDir, 0755)
if err != nil {
t.Fatal(err)
}
const providerVersion = "3.1.0" // must match the version in the fixture config
symlinkPathInCache := tf.Path(
".terraform",
"providers",
addrs.DefaultProviderRegistryHost.String(),
"hashicorp",
"null",
providerVersion,
getproviders.CurrentPlatform.String(),
)
err = os.MkdirAll(filepath.Dir(symlinkPathInCache), 0755)
if err != nil {
t.Fatal(err)
}
err = os.Symlink(otherDir, symlinkPathInCache)
if err != nil {
t.Fatal(err)
}
stdout, stderr, err := tf.Run("init", "-no-color")
if err == nil {
t.Fatalf("unexpected init success\nstdout:\n%s\nstderr:\n%s", stdout, stderr)
}
if !strings.Contains(stderr, "Failed to install provider") {
t.Fatalf("missing expected error message\nstderr:\n%s", stderr)
}
if !strings.Contains(stderr, "symlink") {
t.Fatalf("missing expected error message\nstderr:\n%s", stderr)
}
}

@ -124,6 +124,15 @@ func installFromLocalArchive(ctx context.Context, meta getproviders.PackageMeta,
// match the allowed hashes and so our caller should catch that after
// we return if so.
// We will, however, check that the target directory the provider binary will be
// placed isn't a symlink, if it already exists. This could be a security risk that
// enables files to be written to unexpected locations.
if fi, err := os.Lstat(targetDir); err == nil {
if fi.Mode().Type() == os.ModeSymlink {
return authResult, fmt.Errorf("cannot install package into target directory %s because it is a symlink.", targetDir)
}
}
err := unzip.Decompress(targetDir, filename, true, 0000)
if err != nil {
return authResult, err

Loading…
Cancel
Save