diff --git a/internal/builder/archive_symlink_test.go b/internal/builder/archive_symlink_test.go index cf5d622b..a6c2eae4 100644 --- a/internal/builder/archive_symlink_test.go +++ b/internal/builder/archive_symlink_test.go @@ -102,3 +102,48 @@ func TestAModuleDirectoryThatIsALinkOutOfTheRepositoryIsRefused(t *testing.T) { t.Errorf("an absent module directory was refused here: %v", err) } } + +// A repository that commits `.mesh-build` as a link must not make a bundle's build write into, or pack, +// a directory outside the module (issue 1609): the compile, the bundling and the pack each refuse it. +func TestABundleWhoseOutputDirectoryIsALinkOutOfTheModuleIsRefused(t *testing.T) { + outside := t.TempDir() + if err := os.WriteFile(filepath.Join(outside, "secret"), []byte("s"), 0o644); err != nil { + t.Fatal(err) + } + tree := t.TempDir() + if err := os.Symlink(outside, filepath.Join(tree, ".mesh-build")); err != nil { + t.Fatal(err) + } + chain, err := ToolchainFor("go") + if err != nil { + t.Fatal(err) + } + a := catalogue.Artifact{Name: "b", Kind: catalogue.ArtifactBundle, Language: "go", From: "cmd/b"} + ran := false + run := func(context.Context, string, string, ...string) (string, error) { ran = true; return "", nil } + realOutside, _ := filepath.EvalSymlinks(outside) + realTree, _ := filepath.EvalSymlinks(tree) + + check := func(what string, err error) { + t.Helper() + if err == nil { + t.Fatalf("%s went through a link out of the module", what) + } + for _, want := range []string{realOutside, realTree} { + if !strings.Contains(err.Error(), want) { + t.Errorf("%s: the refusal %q does not name %q", what, err, want) + } + } + } + _, err = compile(context.Background(), run, tree, chain, "base", a) + check("compile", err) + _, err = bundled(context.Background(), run, tree, chain, "base", a, nil) + check("bundled", err) + check("the pack", outsideTheModule(tree, filepath.Join(tree, Out(a.Name)))) + if ran { + t.Error("a command ran for a refused bundle") + } + if entries, _ := os.ReadDir(outside); len(entries) != 1 { + t.Errorf("a refused bundle wrote into the directory outside: %d entries", len(entries)) + } +} diff --git a/internal/builder/builder.go b/internal/builder/builder.go index 4e899c0b..d4997009 100644 --- a/internal/builder/builder.go +++ b/internal/builder/builder.go @@ -1295,6 +1295,9 @@ func one(ctx context.Context, run Runner, publish Publisher, } } say("bundle", "compiled, packing") + if err := outsideTheModule(tree, compiled); err != nil { + return catalogue.Built{}, fmt.Errorf("%s: packing %s failed: %w", module, a.Name, err) + } body, err := pack(compiled) if err != nil { return catalogue.Built{}, fmt.Errorf("%s: packing %s failed: %w", module, a.Name, err) @@ -1384,6 +1387,36 @@ func archiveRoot(tree string, a catalogue.Artifact) (string, error) { return root, nil } +// outsideTheModule refuses a path that, once every link in it is followed, is not inside the module's +// directory, naming where it really is and where the module is. For a path not there yet (an output +// directory about to be made) the deepest part that exists is the one followed. A repository commits +// its own links, so `.mesh-build` as a link would otherwise have a bundle's build write into, and pack, +// any directory the build account can read (issue 1609). +func outsideTheModule(tree, path string) error { + realTree, err := filepath.EvalSymlinks(tree) + if err != nil { + return err + } + real, rest := path, "" + for { + resolved, err := filepath.EvalSymlinks(real) + if err == nil { + real = filepath.Join(resolved, rest) + break + } + if !errors.Is(err, os.ErrNotExist) || filepath.Dir(real) == real { + return err + } + rest = filepath.Join(filepath.Base(real), rest) + real = filepath.Dir(real) + } + if rel, err := filepath.Rel(realTree, real); err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) { + return fmt.Errorf("%s leads out of the module through a link: it resolves to %s, and the module is %s", + path, real, realTree) + } + return nil +} + // pack tars and gzips a directory. // // **Deterministically**: entries sorted, and no timestamps, uid, gid or original names carried @@ -1687,6 +1720,9 @@ func compile(ctx context.Context, run Runner, tree string, chain Toolchain, // a package in a third (ADR 0040). Compiling them all into one place would have them overwrite // each other and then be packed together, so each bundle compiles and packs alone. out := Out(a.Name) + if err := outsideTheModule(tree, filepath.Join(tree, out)); err != nil { + return "", err + } // **The output directory exists before the compiler is told about it.** `tsc --outDir` makes // one; `go build -o` writes a file into a directory and does not create it, failing with a @@ -2013,6 +2049,9 @@ func bundled(ctx context.Context, run Runner, tree string, chain Toolchain, base a catalogue.Artifact, launchers map[string]string) (string, error) { const within = "/app/modules/module" out, final := Out(a.Name), Out(a.Name)+bundledSuffix + if err := outsideTheModule(tree, filepath.Join(tree, final)); err != nil { + return "", err + } if err := os.RemoveAll(filepath.Join(tree, final)); err != nil { return "", err }