Prevent unwanted path traversal in file writes from buildtools. (#1491)
* Prevent unwanted path traversal in file writes from buildtools.
Buildtools previously allowed file writes through arbitrary symlinks, which could result in unintended path traversal outside of the Bazel workspace.
Use safeopen.WriteFileBeneath to ensure file writes remain confined within the Bazel workspace root. Add the -disable_symlink_safety flag to both buildifier and buildozer to allow opting out of this restriction when modifying files targeted via external symlinks.
New Behavior:
- Without flag (default): both tools refused to write and exited with error (invalid cross-device link).
- With -disable_symlink_safety: both tools successfully formatted/edited the target file.
* Fixing MODULE.bazel
* Disabling Symlink safety for windows
* Only check symlink safety on Linux for now
* Also OS-gating buildozer test
---------
Co-authored-by: Tim Malmström <oreflow@google.com>
diff --git a/MODULE.bazel b/MODULE.bazel
index a508bb6..ec98eaf 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -21,6 +21,15 @@
go_deps = use_extension("@bazel_gazelle//:extensions.bzl", "go_deps")
go_deps.from_file(go_mod = "//:go.mod")
+go_deps.gazelle_override(
+ build_file_generation = "clean",
+ path = "github.com/google/safeopen",
+)
+go_deps.module(
+ path = "golang.org/x/sys",
+ sum = "h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=",
+ version = "v0.10.0",
+)
go_deps.module(
path = "golang.org/x/tools",
sum = "h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8=",
@@ -30,8 +39,10 @@
go_deps,
"com_github_golang_protobuf",
"com_github_google_go_cmp",
+ "com_github_google_safeopen",
"net_starlark_go",
"org_golang_google_protobuf",
+ "org_golang_x_sys",
"org_golang_x_tools",
)
diff --git a/buildifier/BUILD.bazel b/buildifier/BUILD.bazel
index 5bf6338..4b27215 100644
--- a/buildifier/BUILD.bazel
+++ b/buildifier/BUILD.bazel
@@ -115,6 +115,7 @@
"//buildifier/config",
"//buildifier/utils",
"//differ",
+ "//file",
"//wspace",
],
)
diff --git a/buildifier/README.md b/buildifier/README.md
index f183c33..faef977 100644
--- a/buildifier/README.md
+++ b/buildifier/README.md
@@ -232,3 +232,14 @@
When the `--format` flag is provided, buildifier always returns `0` unless there are internal
failures or wrong input parameters, this means the output can be parsed as JSON, and its `success`
field should be used to determine whether the diagnostics result is positive.
+
+## Symlink Safety
+
+By default, Buildifier will not write to symlinks pointing outside of the Bazel workspace to prevent unintended path traversal.
+
+To disable this behavior and allow writing through symlinks pointing outside the workspace, use the `--disable_symlink_safety` flag or set `"disable_symlink_safety": true` in `.buildifier.json`:
+
+```bash
+buildifier --disable_symlink_safety path/to/file
+```
+
diff --git a/buildifier/buildifier.go b/buildifier/buildifier.go
index 60124fc..2ede03f 100644
--- a/buildifier/buildifier.go
+++ b/buildifier/buildifier.go
@@ -30,6 +30,7 @@
"github.com/bazelbuild/buildtools/buildifier/config"
"github.com/bazelbuild/buildtools/buildifier/utils"
"github.com/bazelbuild/buildtools/differ"
+ "github.com/bazelbuild/buildtools/file"
"github.com/bazelbuild/buildtools/wspace"
)
@@ -145,6 +146,7 @@
// Pass down debug flags into build package
build.DisableRewrites = c.DisableRewrites
build.AllowSort = c.AllowSort
+ file.DisableSymlinkSafety = c.DisableSymlinkSafety
differ, deprecationWarning := differ.Find()
if c.DiffCommand != "" {
@@ -361,7 +363,7 @@
return fileDiagnostics, exitCode
}
- err := os.WriteFile(filename, ndata, 0666)
+ err := file.WriteFileMode(filename, ndata, 0666)
if err != nil {
fmt.Fprintf(os.Stderr, "buildifier: %s\n", err)
return fileDiagnostics, 3
diff --git a/buildifier/config/config.go b/buildifier/config/config.go
index e9783b9..825770a 100644
--- a/buildifier/config/config.go
+++ b/buildifier/config/config.go
@@ -131,6 +131,10 @@
// AllowSort specifies additional sort contexts to treat as safe
AllowSort ArrayFlags `json:"allowsort,omitempty"`
+ // Per default Buildifier will not write to symlinks pointing outside of the Bazel workspace.
+ // Setting this to true will disable this behavior.
+ DisableSymlinkSafety bool `json:"disable_symlink_safety,omitempty"`
+
// Help is true if the -h flag is set
Help bool `json:"-"`
// Version is true if the -v flag is set
@@ -185,6 +189,7 @@
flags.StringVar(&c.ConfigPath, "config", "", "path to .buildifier.json config file")
flags.Var(&c.AllowSort, "allowsort", "additional sort contexts to treat as safe")
flags.Var(&c.DisableRewrites, "buildifier_disable", "list of buildifier rewrites to disable")
+ flags.BoolVar(&c.DisableSymlinkSafety, "disable_symlink_safety", c.DisableSymlinkSafety, "per default Buildifier will not write to symlinks pointing outside of the Bazel workspace. Setting this to true will disable this behavior")
return flags
}
diff --git a/buildifier/config/config_test.go b/buildifier/config/config_test.go
index c929176..6f553ad 100644
--- a/buildifier/config/config_test.go
+++ b/buildifier/config/config_test.go
@@ -160,6 +160,7 @@
// config: path to .buildifier.json config file ("")
// d: alias for -mode=diff ("false")
// diff_command: command to run when the formatting mode is diff (default uses the BUILDIFIER_DIFF, BUILDIFIER_MULTIDIFF, and DISPLAY environment variables to create the diff command) ("")
+ // disable_symlink_safety: per default Buildifier will not write to symlinks pointing outside of the Bazel workspace. Setting this to true will disable this behavior ("false")
// format: diagnostics format: text or json (default text) ("")
// help: print usage information ("false")
// lint: lint mode: off, warn, or fix (default off) ("")
@@ -185,6 +186,7 @@
"--config=/path/to/.buildifier.json",
"-d",
"--diff_command=diff",
+ "--disable_symlink_safety=true",
"--format=json",
"--help",
"--lint=fix",
@@ -226,7 +228,8 @@
// "allowsort": [
// "proto_library.deps",
// "proto_library.srcs"
- // ]
+ // ],
+ // "disable_symlink_safety": true
// }
}
diff --git a/buildifier/integration_test.sh b/buildifier/integration_test.sh
index b9a6389..a6339f5 100755
--- a/buildifier/integration_test.sh
+++ b/buildifier/integration_test.sh
@@ -725,3 +725,65 @@
diff -u report_golden report || die "$1: wrong console output for allowed symbol load locations"
cd ../..
+
+# Test that buildifier cannot write to symlinks pointing outside the workspace or directory
+if [[ "$(uname -s)" == "Linux" ]]; then
+# 1. Inside a workspace: symlink pointing outside the workspace should fail
+mkdir -p test_dir/symlinks_ws/ws
+mkdir -p test_dir/symlinks_ws/outside
+touch test_dir/symlinks_ws/ws/WORKSPACE.bazel
+UNFORMATTED="foo( b=2, a=1 )"
+echo "$UNFORMATTED" > test_dir/symlinks_ws/outside/target.bzl
+ln -s ../outside/target.bzl test_dir/symlinks_ws/ws/symlink_outside.bzl
+
+ret=0
+"$buildifier" test_dir/symlinks_ws/ws/symlink_outside.bzl 2> /dev/null || ret=$?
+if [[ $ret -ne 3 ]]; then
+ die "Symlink outside workspace: expected buildifier to exit with 3, actual: $ret"
+fi
+diff -u <(echo "$UNFORMATTED") test_dir/symlinks_ws/outside/target.bzl || die "Symlink outside workspace should not modify target file"
+
+# Disabling symlink safety via flag should allow modifying target through symlink
+"$buildifier" --disable_symlink_safety test_dir/symlinks_ws/ws/symlink_outside.bzl || die "Symlink outside workspace with --disable_symlink_safety: expected buildifier to succeed"
+diff -u <(echo "$UNFORMATTED") test_dir/symlinks_ws/outside/target.bzl > /dev/null && die "Symlink outside workspace with --disable_symlink_safety: target file should be formatted"
+
+# Disabling symlink safety via config file should allow modifying target through symlink
+echo "$UNFORMATTED" > test_dir/symlinks_ws/outside/target.bzl
+echo '{"disable_symlink_safety": true}' > test_dir/symlinks_ws/ws/.buildifier.json
+"$buildifier" --config=test_dir/symlinks_ws/ws/.buildifier.json test_dir/symlinks_ws/ws/symlink_outside.bzl || die "Symlink outside workspace with .buildifier.json: expected buildifier to succeed"
+diff -u <(echo "$UNFORMATTED") test_dir/symlinks_ws/outside/target.bzl > /dev/null && die "Symlink outside workspace with .buildifier.json: target file should be formatted"
+rm -f test_dir/symlinks_ws/ws/.buildifier.json
+
+# Symlink pointing inside the workspace should succeed
+echo "$UNFORMATTED" > test_dir/symlinks_ws/ws/target_inside.bzl
+ln -s target_inside.bzl test_dir/symlinks_ws/ws/symlink_inside.bzl
+"$buildifier" test_dir/symlinks_ws/ws/symlink_inside.bzl || die "Symlink inside workspace: expected buildifier to succeed"
+diff -u <(echo "$UNFORMATTED") test_dir/symlinks_ws/ws/target_inside.bzl > /dev/null && die "Symlink inside workspace: target file should be formatted"
+
+# 2. Outside a workspace: symlink pointing outside the directory should fail
+NOWS_DIR="${TEST_TMPDIR:-/tmp}/nowspace_test_$$"
+mkdir -p "$NOWS_DIR/dir"
+mkdir -p "$NOWS_DIR/outside"
+echo "$UNFORMATTED" > "$NOWS_DIR/outside/target.bzl"
+ln -s ../outside/target.bzl "$NOWS_DIR/dir/symlink_outside.bzl"
+
+ret=0
+"$buildifier" "$NOWS_DIR/dir/symlink_outside.bzl" 2> /dev/null || ret=$?
+if [[ $ret -ne 3 ]]; then
+ die "Symlink outside directory (no workspace): expected buildifier to exit with 3, actual: $ret"
+fi
+diff -u <(echo "$UNFORMATTED") "$NOWS_DIR/outside/target.bzl" || die "Symlink outside directory should not modify target file"
+
+# Disabling symlink safety via flag should allow modifying target through symlink
+"$buildifier" --disable_symlink_safety "$NOWS_DIR/dir/symlink_outside.bzl" || die "Symlink outside directory with --disable_symlink_safety: expected buildifier to succeed"
+diff -u <(echo "$UNFORMATTED") "$NOWS_DIR/outside/target.bzl" > /dev/null && die "Symlink outside directory with --disable_symlink_safety: target file should be formatted"
+
+# Symlink pointing inside the directory should succeed
+echo "$UNFORMATTED" > "$NOWS_DIR/dir/target_inside.bzl"
+ln -s target_inside.bzl "$NOWS_DIR/dir/symlink_inside.bzl"
+"$buildifier" "$NOWS_DIR/dir/symlink_inside.bzl" || die "Symlink inside directory: expected buildifier to succeed"
+diff -u <(echo "$UNFORMATTED") "$NOWS_DIR/dir/target_inside.bzl" > /dev/null && die "Symlink inside directory: target file should be formatted"
+
+rm -rf "$NOWS_DIR"
+fi
+
diff --git a/buildozer/README.md b/buildozer/README.md
index 82a9394..f10c9dd 100644
--- a/buildozer/README.md
+++ b/buildozer/README.md
@@ -79,6 +79,7 @@
* `-types`: Filter the targets, keeping only those of the given types, e.g.
`buildozer -types go_library,go_binary 'print rule' '//buildtools/buildozer:*'`
* `-eol-comments=false`: When adding new comments, put them on a separate line.
+ * `-disable_symlink_safety`: By default Buildozer will not write to symlinks pointing outside of the Bazel workspace. Setting this to true will disable this behavior.
See `buildozer -help` for the full list.
diff --git a/buildozer/buildozer_test.sh b/buildozer/buildozer_test.sh
index 8ed014b..ab3815c 100755
--- a/buildozer/buildozer_test.sh
+++ b/buildozer/buildozer_test.sh
@@ -2471,4 +2471,33 @@
diff -u MODULE.bazel.expected.stderr stderr || fail "Error output didn't match"
}
+function test_disable_symlink_safety() {
+ if [[ "$(uname -s)" != "Linux" ]]; then
+ return
+ fi
+ outside="$TEST_TMPDIR/outside_$$"
+ mkdir -p "$outside"
+ cat > "$outside/BUILD" <<EOF
+go_library(
+ name = "edit",
+)
+EOF
+ mkdir -p "$PKG"
+ rm -f "$PKG/BUILD"
+ ln -s "$outside/BUILD" "$PKG/BUILD"
+
+ # Without -disable_symlink_safety, should fail
+ ERROR=2
+ run_with_current_workspace "$buildozer --buildifier=" 'add deps //dep' '//pkg:edit'
+
+ # With -disable_symlink_safety, should succeed
+ ERROR=0
+ run_with_current_workspace "$buildozer --buildifier= -disable_symlink_safety" 'add deps //dep' '//pkg:edit'
+ if ! grep "//dep" "$outside/BUILD" > /dev/null; then
+ fail "Target file was not modified when -disable_symlink_safety was used"
+ fi
+ rm -f "$PKG/BUILD"
+ rm -rf "$outside"
+}
+
run_suite "buildozer tests"
diff --git a/buildozer/main.go b/buildozer/main.go
index 69fe90b..e802d58 100644
--- a/buildozer/main.go
+++ b/buildozer/main.go
@@ -59,9 +59,10 @@
tablesPath = flag.String("tables", "", "path to JSON file with custom table definitions which will replace the built-in tables")
addTablesPath = flag.String("add_tables", "", "path to JSON file with custom table definitions which will be merged with the built-in tables")
- shortenLabelsFlag = flag.Bool("shorten_labels", true, "convert added labels to short form, e.g. //foo:bar => :bar")
- deleteWithComments = flag.Bool("delete_with_comments", true, "If a list attribute should be deleted even if there is a comment attached to it")
- respectBazelignore = flag.Bool("respect_bazelignore", true, "use .bazelignore file for ignoring paths")
+ shortenLabelsFlag = flag.Bool("shorten_labels", true, "convert added labels to short form, e.g. //foo:bar => :bar")
+ deleteWithComments = flag.Bool("delete_with_comments", true, "If a list attribute should be deleted even if there is a comment attached to it")
+ respectBazelignore = flag.Bool("respect_bazelignore", true, "use .bazelignore file for ignoring paths")
+ disableSymlinkSafety = flag.Bool("disable_symlink_safety", false, "per default Buildozer will not write to symlinks pointing outside of the Bazel workspace. Setting this to true will disable this behavior")
)
func stringList(name, help string) func() []string {
@@ -108,20 +109,21 @@
edit.ShortenLabelsFlag = *shortenLabelsFlag
edit.DeleteWithComments = *deleteWithComments
opts := &edit.Options{
- Stdout: *stdout,
- Buildifier: *buildifier,
- Parallelism: *parallelism,
- NumIO: *numio,
- CommandsFiles: commandsFiles,
- KeepGoing: *keepGoing,
- FilterRuleTypes: filterRuleTypes(),
- PreferEOLComments: *preferEOLComments,
- RootDir: *rootDir,
- Quiet: *quiet,
- EditVariables: *editVariables,
- IsPrintingProto: *isPrintingProto,
- IsPrintingJSON: *isPrintingJSON,
- RespectBazelignore: *respectBazelignore,
+ Stdout: *stdout,
+ Buildifier: *buildifier,
+ Parallelism: *parallelism,
+ NumIO: *numio,
+ CommandsFiles: commandsFiles,
+ KeepGoing: *keepGoing,
+ FilterRuleTypes: filterRuleTypes(),
+ PreferEOLComments: *preferEOLComments,
+ RootDir: *rootDir,
+ Quiet: *quiet,
+ EditVariables: *editVariables,
+ IsPrintingProto: *isPrintingProto,
+ IsPrintingJSON: *isPrintingJSON,
+ RespectBazelignore: *respectBazelignore,
+ DisableSymlinkSafety: *disableSymlinkSafety,
}
os.Exit(edit.Buildozer(opts, flag.Args()))
}
diff --git a/edit/buildozer.go b/edit/buildozer.go
index ac7173f..861cc7e 100644
--- a/edit/buildozer.go
+++ b/edit/buildozer.go
@@ -45,22 +45,23 @@
// Options represents choices about how buildozer should behave.
type Options struct {
- Stdout bool // write changed BUILD file to stdout
- Buildifier string // path to buildifier binary
- Parallelism int // number of cores to use for concurrent actions
- NumIO int // number of concurrent actions
- CommandsFiles []string // file names to read commands from, use '-' for stdin (format:|-separated command line arguments to buildozer, excluding flags
- KeepGoing bool // apply all commands, even if there are failures
- FilterRuleTypes []string // list of rule types to change, empty means all
- PreferEOLComments bool // when adding a new comment, put it on the same line if possible
- RootDir string // If present, use this folder rather than $PWD to find the root dir
- Quiet bool // suppress informational messages.
- EditVariables bool // for attributes that simply assign a variable (e.g. hdrs = LIB_HDRS), edit the build variable instead of appending to the attribute.
- IsPrintingProto bool // output serialized devtools.buildozer.Output protos instead of human-readable strings
- IsPrintingJSON bool // output serialized devtools.buildozer.Output json instead of human-readable strings
- OutWriter io.Writer // where to write normal output (`os.Stdout` will be used if not specified)
- ErrWriter io.Writer // where to write error output (`os.Stderr` will be used if not specified)
- RespectBazelignore bool // whether to use .bazelignore file for ignoring paths
+ Stdout bool // write changed BUILD file to stdout
+ Buildifier string // path to buildifier binary
+ Parallelism int // number of cores to use for concurrent actions
+ NumIO int // number of concurrent actions
+ CommandsFiles []string // file names to read commands from, use '-' for stdin (format:|-separated command line arguments to buildozer, excluding flags
+ KeepGoing bool // apply all commands, even if there are failures
+ FilterRuleTypes []string // list of rule types to change, empty means all
+ PreferEOLComments bool // when adding a new comment, put it on the same line if possible
+ RootDir string // If present, use this folder rather than $PWD to find the root dir
+ Quiet bool // suppress informational messages.
+ EditVariables bool // for attributes that simply assign a variable (e.g. hdrs = LIB_HDRS), edit the build variable instead of appending to the attribute.
+ IsPrintingProto bool // output serialized devtools.buildozer.Output protos instead of human-readable strings
+ IsPrintingJSON bool // output serialized devtools.buildozer.Output json instead of human-readable strings
+ OutWriter io.Writer // where to write normal output (`os.Stdout` will be used if not specified)
+ ErrWriter io.Writer // where to write error output (`os.Stderr` will be used if not specified)
+ RespectBazelignore bool // whether to use .bazelignore file for ignoring paths
+ DisableSymlinkSafety bool // whether to disable symlink safety checks
}
// NewOpts returns a new Options struct with some defaults set.
@@ -1206,7 +1207,7 @@
// BuildFileNames is exported so that users that want to override it
// in scripts are free to do so.
-var BuildFileNames = [...]string{"BUILD.bazel", "BUILD", "BUCK"}
+var BuildFileNames = [...]string{"BUILD", "BUILD.bazel", "BUCK"}
// Buildifier formats the build file using the buildifier logic.
type Buildifier interface {
@@ -1624,6 +1625,7 @@
// Buildozer loops over all arguments on the command line fixing BUILD files.
func Buildozer(opts *Options, args []string) int {
+ file.DisableSymlinkSafety = opts.DisableSymlinkSafety
if opts.OutWriter == nil {
opts.OutWriter = os.Stdout
}
diff --git a/edit/buildozer_test.go b/edit/buildozer_test.go
index 5b1780f..c382e38 100644
--- a/edit/buildozer_test.go
+++ b/edit/buildozer_test.go
@@ -21,6 +21,7 @@
"os"
"path/filepath"
"reflect"
+ "runtime"
"strings"
"testing"
@@ -1329,3 +1330,55 @@
})
}
}
+
+func TestBuildozerDisableSymlinkSafety(t *testing.T) {
+ if runtime.GOOS != "linux" {
+ t.Skip("Symlink safety checks are only supported on Linux")
+ }
+ wsDir := t.TempDir()
+ outsideDir := t.TempDir()
+
+ if err := os.WriteFile(filepath.Join(wsDir, "WORKSPACE"), nil, 0755); err != nil {
+ t.Fatal(err)
+ }
+
+ outsideBuildFile := filepath.Join(outsideDir, "BUILD")
+ initialContent := `cc_library(
+ name = "lib",
+)
+`
+ if err := os.WriteFile(outsideBuildFile, []byte(initialContent), 0644); err != nil {
+ t.Fatal(err)
+ }
+
+ symlinkBuildFile := filepath.Join(wsDir, "BUILD")
+ if err := os.Symlink(outsideBuildFile, symlinkBuildFile); err != nil {
+ t.Fatal(err)
+ }
+
+ // 1. With DisableSymlinkSafety = false, Buildozer editing the file should fail (return 1 or error)
+ opts := NewOpts()
+ opts.RootDir = wsDir
+ opts.DisableSymlinkSafety = false
+ exitCode := Buildozer(opts, []string{"add deps //foo:bar", "//:lib"})
+ if exitCode == 0 {
+ t.Errorf("Buildozer with DisableSymlinkSafety=false succeeded on symlink pointing outside workspace, want error (non-zero exit code)")
+ }
+
+ // 2. With DisableSymlinkSafety = true, Buildozer should succeed and edit outsideBuildFile
+ opts = NewOpts()
+ opts.RootDir = wsDir
+ opts.DisableSymlinkSafety = true
+ exitCode = Buildozer(opts, []string{"add deps //foo:bar", "//:lib"})
+ if exitCode != 0 {
+ t.Fatalf("Buildozer with DisableSymlinkSafety=true failed with exit code %d", exitCode)
+ }
+
+ content, err := os.ReadFile(outsideBuildFile)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !strings.Contains(string(content), "//foo:bar") {
+ t.Errorf("outside BUILD file did not contain added dependency, content:\n%s", string(content))
+ }
+}
diff --git a/file/BUILD.bazel b/file/BUILD.bazel
index cdc722d..a358823 100644
--- a/file/BUILD.bazel
+++ b/file/BUILD.bazel
@@ -5,6 +5,10 @@
srcs = ["file.go"],
importpath = "github.com/bazelbuild/buildtools/file",
visibility = ["//visibility:public"],
+ deps = [
+ "//wspace",
+ "@com_github_google_safeopen//:safeopen",
+ ],
)
alias(
diff --git a/file/file.go b/file/file.go
index 9e6252e..28716d4 100644
--- a/file/file.go
+++ b/file/file.go
@@ -21,6 +21,11 @@
"fmt"
"io"
"os"
+ "path/filepath"
+ "runtime"
+
+ "github.com/bazelbuild/buildtools/wspace"
+ "github.com/google/safeopen"
)
// ReadFile can be updated from the caller to change the API
@@ -31,10 +36,17 @@
// for writing a file.
var WriteFile = writeFile
+// WriteFileMode can be updated from the caller to change the API
+// for writing a file with a specific mode.
+var WriteFileMode = writeFileMode
+
// OpenReadFile can be updated from the caller to change the API
// for opening a file.
var OpenReadFile = openReadFile
+// DisableSymlinkSafety disables symlink safety checks for WriteFile and WriteFileMode.
+var DisableSymlinkSafety bool
+
// readFile is like os.ReadFile.
func readFile(name string) ([]byte, os.FileInfo, error) {
fi, err := os.Stat(name)
@@ -48,7 +60,29 @@
// writeFile is like os.WriteFile.
func writeFile(name string, data []byte) error {
- return os.WriteFile(name, data, 0644)
+ return WriteFileMode(name, data, 0644)
+}
+
+// writeFile is like os.WriteFile.
+func writeFileMode(name string, data []byte, mode os.FileMode) error {
+ if DisableSymlinkSafety || runtime.GOOS != "linux" {
+ return os.WriteFile(name, data, mode)
+ }
+ // If we are in a workspace, we allow writes to any symlinked file within the workspace.
+ if wsRoot, _ := wspace.FindWorkspaceRoot(name); wsRoot != "" {
+ relPath, err := filepath.Rel(wsRoot, name)
+ if err != nil {
+ return err
+ }
+ return safeopen.WriteFileBeneath(wsRoot, relPath, data, mode)
+ }
+ dir, file := filepath.Split(name)
+ absDir, err := filepath.Abs(dir)
+ if err != nil {
+ return err
+ }
+ // If we are not in a workspace, we only allow writes to the directory where the file is located.
+ return safeopen.WriteFileBeneath(absDir, file, data, mode)
}
// openReadFile is like os.Open.
diff --git a/go.mod b/go.mod
index bf1b5f6..a29c1ad 100644
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,7 @@
require (
github.com/golang/protobuf v1.5.0
github.com/google/go-cmp v0.5.9
+ github.com/google/safeopen v0.0.0-20260327150837-43626d6f4685
go.starlark.net v0.0.0-20210223155950-e043a3d3c984
google.golang.org/protobuf v1.33.0
)
diff --git a/go.sum b/go.sum
index 3c909c2..d5a2ba2 100644
--- a/go.sum
+++ b/go.sum
@@ -25,8 +25,11 @@
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/safeopen v0.0.0-20260327150837-43626d6f4685 h1:NcJjfIYRDHuboRtptwjtdQflVDKgXSqTIfwu9PpE9uo=
+github.com/google/safeopen v0.0.0-20260327150837-43626d6f4685/go.mod h1:D59KewtQCiD2Avi8N/v2zb/xTYaefwJl+ux2ejB58GQ=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
go.starlark.net v0.0.0-20200203144150-6677ee5c7211 h1:Qoe+9POtDT51UBQ8XEnS9QKeHDQzEl2QRh3eok9R4aw=
go.starlark.net v0.0.0-20200203144150-6677ee5c7211/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o=
@@ -49,6 +52,8 @@
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
+golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
diff --git a/unused_deps/BUILD.bazel b/unused_deps/BUILD.bazel
index 978c474..d1bfd78 100644
--- a/unused_deps/BUILD.bazel
+++ b/unused_deps/BUILD.bazel
@@ -18,6 +18,7 @@
"//deps_proto",
"//edit",
"//extra_actions_base_proto",
+ "//file",
"//labels",
"@com_github_golang_protobuf//proto:go_default_library",
],
diff --git a/unused_deps/README.md b/unused_deps/README.md
index 0f89ad0..eeae97f 100644
--- a/unused_deps/README.md
+++ b/unused_deps/README.md
@@ -20,3 +20,8 @@
```
Here, `TARGET` is a space-separated list of Bazel labels, with support for `:all` and `...`
+
+## Options
+
+* `-disable_symlink_safety`: By default unused_deps will not write to symlinks pointing outside of the Bazel workspace. Setting this to true will disable this behavior.
+
diff --git a/unused_deps/unused_deps.go b/unused_deps/unused_deps.go
index 42e0c52..35bbea7 100644
--- a/unused_deps/unused_deps.go
+++ b/unused_deps/unused_deps.go
@@ -35,6 +35,7 @@
depspb "github.com/bazelbuild/buildtools/deps_proto"
"github.com/bazelbuild/buildtools/edit"
eapb "github.com/bazelbuild/buildtools/extra_actions_base_proto"
+ "github.com/bazelbuild/buildtools/file"
"github.com/bazelbuild/buildtools/labels"
"github.com/golang/protobuf/proto"
)
@@ -43,12 +44,13 @@
buildVersion = "redacted"
buildScmRevision = "redacted"
- version = flag.Bool("version", false, "Print the version of unused_deps")
- cQuery = flag.Bool("cquery", false, "Use 'cquery' command instead of 'query'")
- buildTool = flag.String("build_tool", config.DefaultBuildTool, config.BuildToolHelp)
- extraActionFileName = flag.String("extra_action_file", "", config.ExtraActionFileNameHelp)
- outputFileName = flag.String("output_file", "", "used only with extra_action_file")
- buildOptions = stringList("extra_build_flags", "Extra build flags to use when building the targets.")
+ version = flag.Bool("version", false, "Print the version of unused_deps")
+ cQuery = flag.Bool("cquery", false, "Use 'cquery' command instead of 'query'")
+ buildTool = flag.String("build_tool", config.DefaultBuildTool, config.BuildToolHelp)
+ extraActionFileName = flag.String("extra_action_file", "", config.ExtraActionFileNameHelp)
+ outputFileName = flag.String("output_file", "", "used only with extra_action_file")
+ buildOptions = stringList("extra_build_flags", "Extra build flags to use when building the targets.")
+ disableSymlinkSafety = flag.Bool("disable_symlink_safety", false, "per default unused_deps will not write to symlinks pointing outside of the Bazel workspace. Setting this to true will disable this behavior")
blazeFlags = []string{"--tool_tag=unused_deps", "--keep_going", "--color=yes", "--curses=yes"}
@@ -306,11 +308,11 @@
return "", err
}
for _, f := range []string{"MODULE.bazel", "WORKSPACE", "BUILD"} {
- if err := os.WriteFile(path.Join(tmp, f), []byte{}, 0666); err != nil {
+ if err := file.WriteFileMode(path.Join(tmp, f), []byte{}, 0666); err != nil {
return "", err
}
}
- if err := os.WriteFile(path.Join(tmp, "unused_deps.bzl"), []byte(aspect), 0666); err != nil {
+ if err := file.WriteFileMode(path.Join(tmp, "unused_deps.bzl"), []byte(aspect), 0666); err != nil {
return "", err
}
return tmp, nil
@@ -330,6 +332,7 @@
func main() {
flag.Usage = usage
flag.Parse()
+ file.DisableSymlinkSafety = *disableSymlinkSafety
if *version {
fmt.Printf("unused_deps version: %s \n", buildVersion)
fmt.Printf("unused_deps scm revision: %s \n", buildScmRevision)