From 35a3121a83ccb0368e2f641572ef257753aab824 Mon Sep 17 00:00:00 2001 From: Meow <197331664+Meo597@users.noreply.github.com> Date: Thu, 29 Oct 2026 05:23:18 +0800 Subject: [PATCH] File loader: Check file extensions without resolving symlinks (#7255) https://github.com/XTLS/Xray-core/pull/7255#issuecomment-6481194940 https://github.com/XTLS/Xray-core/pull/7250#issuecomment-6398816842 Fixes https://github.com/XTLS/Xray-core/issues/7239#issuecomment-6356122816 --------- Co-authored-by: Eljees <3.14hell@gmail.com> --- common/lua/program.go | 4 +- common/lua/program_test.go | 19 +++--- common/platform/filesystem/file.go | 48 +++++++------- common/platform/filesystem/file_test.go | 49 +++++++++++++- common/platform/others.go | 4 +- common/platform/platform.go | 67 +------------------ common/platform/platform_test.go | 88 ------------------------- common/platform/windows.go | 4 +- infra/conf/geodata.go | 2 +- main/commands/all/lua/sign.go | 15 ++--- main/confloader/external/external.go | 12 +--- 11 files changed, 94 insertions(+), 218 deletions(-) diff --git a/common/lua/program.go b/common/lua/program.go index 57b950cee..23e9d5767 100644 --- a/common/lua/program.go +++ b/common/lua/program.go @@ -11,7 +11,7 @@ import ( "github.com/xtls/xray-core/common/base64" "github.com/xtls/xray-core/common/errors" - "github.com/xtls/xray-core/common/platform" + "github.com/xtls/xray-core/common/platform/filesystem" glua "github.com/yuin/gopher-lua" "github.com/yuin/gopher-lua/parse" ) @@ -36,7 +36,7 @@ func Compile(script string) (*Program, error) { // TODO: Restore inline script support and re-enable the related tests. return nil, errors.New("currently unavailable") code, err = base64.Decode(script) - } else if name, err = platform.ResolveLuaFile(script); err == nil { + } else if name, err = filesystem.ResolveLuaFile(script); err == nil { code, err = os.ReadFile(name) // TODO: Restore privileged signature authorization and re-enable the related tests and CLI entries. // if err == nil { diff --git a/common/lua/program_test.go b/common/lua/program_test.go index fbd34de19..67ce8153b 100644 --- a/common/lua/program_test.go +++ b/common/lua/program_test.go @@ -187,19 +187,20 @@ func TestCompileInlineDiagnostics(t *testing.T) { */ func TestCompileFileExtension(t *testing.T) { - for _, name := range []string{"script", "script.dat", "script.lua.txt", "script.LUA"} { + for _, name := range []string{"script", "script.dat", "script.lua.txt"} { + if program, err := Compile(name); program != nil || err == nil || !strings.Contains(err.Error(), "Lua file extension must be .lua") { + t.Fatalf("Compile(%q) = %v, %v; want nil program and .lua extension error", name, program, err) + } + } + for _, name := range []string{"script.lua", "script.LUA", "script.Lua", "script.lUa"} { path := filepath.Join(t.TempDir(), name) if err := os.WriteFile(path, []byte("value = 42"), 0o600); err != nil { t.Fatal(err) } - if program, err := Compile(path); program != nil || err == nil || !strings.Contains(err.Error(), "Lua file extension must be .lua") { - t.Fatalf("Compile(%q) = %v, %v; want nil program and .lua extension error", path, program, err) + if program, err := Compile(path); program == nil || err != nil { + t.Fatalf("Compile(%q) = %v, %v; want successful compilation", path, program, err) } } - path := writeTestScript(t, "value = 42") + string(filepath.Separator) + "." - if program, err := Compile(path); program == nil || err != nil { - t.Fatalf("Compile(%q) = %v, %v; want acceptance of the resolved .lua path", path, program, err) - } } func TestCompileRejectsReadFailure(t *testing.T) { @@ -234,10 +235,6 @@ func writeTestScript(t *testing.T, code string) string { if err := os.WriteFile(path, []byte(code), 0o600); err != nil { t.Fatal(err) } - path, err := filepath.EvalSymlinks(path) - if err != nil { - t.Fatal(err) - } return path } diff --git a/common/platform/filesystem/file.go b/common/platform/filesystem/file.go index 24cf112e2..55c0e9365 100644 --- a/common/platform/filesystem/file.go +++ b/common/platform/filesystem/file.go @@ -5,6 +5,7 @@ import ( "io" "os" "path/filepath" + "strings" "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/platform" @@ -26,14 +27,6 @@ func ReadFile(path string) ([]byte, error) { return buf.ReadAllToBytes(reader) } -func ReadAsset(file string) ([]byte, error) { - path, _, err := getAssetFileLocation(file) - if err != nil { - return nil, err - } - return ReadFile(path) -} - func OpenAsset(file string) (io.ReadCloser, int64, error) { path, info, err := getAssetFileLocation(file) if err != nil { @@ -43,11 +36,6 @@ func OpenAsset(file string) (io.ReadCloser, int64, error) { return r, info.Size(), err } -func StatAsset(file string) (os.FileInfo, error) { - _, info, err := getAssetFileLocation(file) - return info, err -} - func ResolveAsset(file string) (string, error) { path, _, err := getAssetFileLocation(file) return path, err @@ -79,17 +67,29 @@ func ReadCert(file string) ([]byte, error) { return ReadFile(platform.GetCertLocation(file)) } -func CopyFile(dst string, src string) error { - bytes, err := ReadFile(src) - if err != nil { - return err +// ResolveLuaFile finds a local .lua script and returns its absolute path. +// Relative paths: XRAY_LOCATION_CONFDIR > XRAY_LOCATION_CONFIG > working dir > executable dir. +func ResolveLuaFile(path string) (string, error) { + if !strings.EqualFold(filepath.Ext(path), ".lua") { + return "", errors.New("Lua file extension must be .lua") } - f, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY, 0o644) - if err != nil { - return err + if filepath.IsAbs(path) { + return path, nil } - defer f.Close() - - _, err = f.Write(bytes) - return err + for _, dir := range []string{ + platform.GetConfDirPath(), + platform.NewEnvFlag(platform.ConfigLocation).GetValue(func() string { return "" }), + ".", + platform.GetExecutableDir(), + } { + if dir != "" { + resolved := filepath.Join(dir, path) + if _, err := os.Stat(resolved); err == nil { + return filepath.Abs(resolved) + } else if !os.IsNotExist(err) { + return "", err + } + } + } + return "", os.ErrNotExist } diff --git a/common/platform/filesystem/file_test.go b/common/platform/filesystem/file_test.go index 17da6e046..89a18074a 100644 --- a/common/platform/filesystem/file_test.go +++ b/common/platform/filesystem/file_test.go @@ -1,13 +1,17 @@ package filesystem_test import ( + "errors" + "os" "path/filepath" "testing" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/platform" . "github.com/xtls/xray-core/common/platform/filesystem" ) -func TestStatAssetRejectsInvalidPath(t *testing.T) { +func TestResolveAssetRejectsInvalidPath(t *testing.T) { for _, file := range []string{ "", ".", @@ -25,8 +29,49 @@ func TestStatAssetRejectsInvalidPath(t *testing.T) { `nested\..\geoip.dat`, filepath.Join(t.TempDir(), "geoip.dat"), } { - if _, err := StatAsset(file); err == nil { + if _, err := ResolveAsset(file); err == nil { t.Fatalf("expected error for %q", file) } } } + +func TestResolveLuaFile(t *testing.T) { + t.Chdir(t.TempDir()) + workingDir, err := os.Getwd() + common.Must(err) + executable, err := os.Executable() + common.Must(err) + file, err := os.CreateTemp(filepath.Dir(executable), "lua-*.lua") + common.Must(err) + common.Must(file.Close()) + defer os.Remove(file.Name()) + + name := filepath.Base(file.Name()) + paths := []string{ + filepath.Join(t.TempDir(), name), + filepath.Join(t.TempDir(), name), + filepath.Join(workingDir, name), + file.Name(), + } + t.Setenv(platform.ConfdirLocation, filepath.Dir(paths[0])) + t.Setenv(platform.ConfigLocation, filepath.Dir(paths[1])) + for _, path := range paths[:3] { + common.Must(os.WriteFile(path, nil, 0o600)) + } + if got, err := ResolveLuaFile(paths[2]); err != nil || got != paths[2] { + t.Fatalf("absolute path = %q, %v; want %q", got, err, paths[2]) + } + for i, want := range paths { + if i == 2 { + t.Setenv(platform.ConfdirLocation, "") + t.Setenv(platform.ConfigLocation, "") + } + if got, err := ResolveLuaFile(name); err != nil || got != want { + t.Fatalf("resolved path = %q, %v; want %q", got, err, want) + } + common.Must(os.Remove(want)) + } + if _, err := ResolveLuaFile(name); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("missing file error = %v", err) + } +} diff --git a/common/platform/others.go b/common/platform/others.go index be86b6faa..f3e491d62 100644 --- a/common/platform/others.go +++ b/common/platform/others.go @@ -14,7 +14,7 @@ func LineSeparator() string { // GetAssetLocation searches for `file` in the env dir, the executable dir, and certain locations func GetAssetLocation(file string) string { - assetPath := NewEnvFlag(AssetLocation).GetValue(getExecutableDir) + assetPath := NewEnvFlag(AssetLocation).GetValue(GetExecutableDir) defPath := filepath.Join(assetPath, file) for _, p := range []string{ defPath, @@ -36,6 +36,6 @@ func GetAssetLocation(file string) string { // GetCertLocation searches for `file` in the env dir and the executable dir func GetCertLocation(file string) string { - certPath := NewEnvFlag(CertLocation).GetValue(getExecutableDir) + certPath := NewEnvFlag(CertLocation).GetValue(GetExecutableDir) return filepath.Join(certPath, file) } diff --git a/common/platform/platform.go b/common/platform/platform.go index 9a650876a..ce7c323db 100644 --- a/common/platform/platform.go +++ b/common/platform/platform.go @@ -1,8 +1,6 @@ package platform // import "github.com/xtls/xray-core/common/platform" import ( - "errors" - "fmt" "os" "path/filepath" "strconv" @@ -74,7 +72,7 @@ func NormalizeEnvName(name string) string { return strings.ReplaceAll(strings.ToUpper(strings.TrimSpace(name)), ".", "_") } -func getExecutableDir() string { +func GetExecutableDir() string { exec, err := os.Executable() if err != nil { return "" @@ -83,7 +81,7 @@ func getExecutableDir() string { } func GetConfigurationPath() string { - configPath := NewEnvFlag(ConfigLocation).GetValue(getExecutableDir) + configPath := NewEnvFlag(ConfigLocation).GetValue(GetExecutableDir) return filepath.Join(configPath, "config.json") } @@ -92,64 +90,3 @@ func GetConfDirPath() string { configPath := NewEnvFlag(ConfdirLocation).GetValue(func() string { return "" }) return configPath } - -// ResolveLuaFile finds a local .lua script and returns its resolved absolute path. -// Relative paths: XRAY_LOCATION_CONFDIR > XRAY_LOCATION_CONFIG > working dir > executable dir. -func ResolveLuaFile(path string) (string, error) { - if path == "" { - return "", errors.New("Lua file path is empty") - } - paths := []string{path} - if !filepath.IsAbs(path) { - paths = nil - for _, dir := range []string{ - GetConfDirPath(), - NewEnvFlag(ConfigLocation).GetValue(func() string { return "" }), - ".", - getExecutableDir(), - } { - if dir != "" { - paths = append(paths, filepath.Join(dir, path)) - } - } - } - path, err := resolveFile(paths) - if err != nil { - return "", err - } - return resolveLuaTarget(path) -} - -func resolveLuaTarget(path string) (string, error) { - path, err := filepath.EvalSymlinks(path) - if err != nil { - return "", fmt.Errorf("failed to resolve Lua file symlinks: %w", err) - } - if filepath.Ext(path) != ".lua" { - return "", errors.New("Lua file extension must be .lua") - } - return path, nil -} - -func resolveFile(paths []string) (string, error) { - var tried []string - for _, path := range paths { - path, err := filepath.Abs(path) - if err != nil { - return "", fmt.Errorf("failed to resolve file path: %w", err) - } - tried = append(tried, path) - info, err := os.Stat(path) - if errors.Is(err, os.ErrNotExist) { - continue - } - if err != nil { - return "", fmt.Errorf("failed to inspect file %q: %w", path, err) - } - if !info.Mode().IsRegular() { - return "", fmt.Errorf("file is not a regular file: %s", path) - } - return path, nil - } - return "", fmt.Errorf("file not found; tried %q: %w", tried, os.ErrNotExist) -} diff --git a/common/platform/platform_test.go b/common/platform/platform_test.go index 87df1a195..854c397f6 100644 --- a/common/platform/platform_test.go +++ b/common/platform/platform_test.go @@ -1,11 +1,9 @@ package platform_test import ( - "errors" "os" "path/filepath" "runtime" - "strings" "testing" "github.com/xtls/xray-core/common" @@ -66,89 +64,3 @@ func TestGetAssetLocation(t *testing.T) { } } } - -func TestResolveLuaFile(t *testing.T) { - workingDir := t.TempDir() - t.Chdir(workingDir) - executable, err := os.Executable() - common.Must(err) - file, err := os.CreateTemp(filepath.Dir(executable), "lua-*.lua") - common.Must(err) - common.Must(file.Close()) - defer os.Remove(file.Name()) - - name := filepath.Base(file.Name()) - paths := []string{ - filepath.Join(t.TempDir(), name), - filepath.Join(t.TempDir(), name), - filepath.Join(workingDir, name), - file.Name(), - } - t.Setenv(ConfdirLocation, filepath.Dir(paths[0])) - t.Setenv(ConfigLocation, filepath.Dir(paths[1])) - for _, path := range paths[:3] { - common.Must(os.WriteFile(path, nil, 0o600)) - } - for i, path := range paths { - paths[i], err = filepath.EvalSymlinks(path) - common.Must(err) - } - if got, err := ResolveLuaFile(paths[2]); err != nil || got != paths[2] { - t.Fatalf("absolute path = %q, %v; want %q", got, err, paths[2]) - } - for i, want := range paths { - if i == 2 { - t.Setenv(ConfdirLocation, "") - t.Setenv(ConfigLocation, "") - } - if got, err := ResolveLuaFile(name); err != nil || got != want { - t.Fatalf("resolved path = %q, %v; want %q", got, err, want) - } - common.Must(os.Remove(want)) - } - if _, err := ResolveLuaFile(name); !errors.Is(err, os.ErrNotExist) { - t.Fatalf("missing file error = %v", err) - } - - t.Setenv(ConfdirLocation, filepath.Dir(paths[0])) - t.Setenv(ConfigLocation, filepath.Dir(paths[1])) - common.Must(os.Mkdir(paths[0], 0o700)) - common.Must(os.WriteFile(paths[1], nil, 0o600)) - for _, path := range []string{"", name, filepath.Join(t.TempDir(), name)} { - if _, err := ResolveLuaFile(path); err == nil { - t.Fatalf("accepted invalid path %q", path) - } - } -} - -func TestResolveLuaFileSymlink(t *testing.T) { - for _, tc := range []struct { - link, target string - valid bool - }{ - {"alias.lua", "data.dat", false}, - {"alias.dat", "script.lua", true}, - } { - t.Run(tc.link, func(t *testing.T) { - dir, err := filepath.EvalSymlinks(t.TempDir()) - common.Must(err) - target := filepath.Join(dir, tc.target) - common.Must(os.WriteFile(target, nil, 0o600)) - link := filepath.Join(dir, tc.link) - if err := os.Symlink(tc.target, link); err != nil { - if runtime.GOOS == "windows" { - t.Skipf("cannot create symlinks: %v", err) - } - t.Fatal(err) - } - got, err := ResolveLuaFile(link) - if tc.valid { - if err != nil || got != target { - t.Fatalf("ResolveLuaFile(%q) = %q, %v; want %q", link, got, err, target) - } - } else if err == nil || got != "" || !strings.Contains(err.Error(), "Lua file extension must be .lua") { - t.Fatalf("ResolveLuaFile(%q) = %q, %v; want .lua extension error", link, got, err) - } - }) - } -} diff --git a/common/platform/windows.go b/common/platform/windows.go index 684ddc9c3..b6554b4b7 100644 --- a/common/platform/windows.go +++ b/common/platform/windows.go @@ -11,12 +11,12 @@ func LineSeparator() string { // GetAssetLocation searches for `file` in the env dir and the executable dir func GetAssetLocation(file string) string { - assetPath := NewEnvFlag(AssetLocation).GetValue(getExecutableDir) + assetPath := NewEnvFlag(AssetLocation).GetValue(GetExecutableDir) return filepath.Join(assetPath, file) } // GetCertLocation searches for `file` in the env dir and the executable dir func GetCertLocation(file string) string { - certPath := NewEnvFlag(CertLocation).GetValue(getExecutableDir) + certPath := NewEnvFlag(CertLocation).GetValue(GetExecutableDir) return filepath.Join(certPath, file) } diff --git a/infra/conf/geodata.go b/infra/conf/geodata.go index 3e80967ac..13e01a072 100644 --- a/infra/conf/geodata.go +++ b/infra/conf/geodata.go @@ -19,7 +19,7 @@ func (c *GeodataAssetConfig) Build() (*geodata.Asset, error) { if err := validateHTTPS(c.URL); err != nil { return nil, errors.New("invalid geodata asset url: ", c.URL).Base(err) } - if _, err := filesystem.StatAsset(c.File); err != nil { + if _, err := filesystem.ResolveAsset(c.File); err != nil { return nil, errors.New("invalid geodata asset file: ", c.File).Base(err) } return &geodata.Asset{ diff --git a/main/commands/all/lua/sign.go b/main/commands/all/lua/sign.go index a300e1540..3ef27e190 100644 --- a/main/commands/all/lua/sign.go +++ b/main/commands/all/lua/sign.go @@ -3,6 +3,7 @@ package lua import ( "os" "path/filepath" + "strings" xlua "github.com/xtls/xray-core/common/lua" "github.com/xtls/xray-core/main/commands/base" @@ -32,14 +33,13 @@ func executeSign(cmd *base.Command, args []string) { cmd.Usage() } + if !strings.EqualFold(filepath.Ext(args[0]), ".lua") { + base.Fatalf("Lua file extension must be .lua") + } info, err := os.Stat(args[0]) if err != nil { base.Fatalf("failed to inspect Lua file: %s", err) } - if !info.Mode().IsRegular() { - base.Fatalf("Lua input is not a regular file: %s", args[0]) - } - code, err := os.ReadFile(args[0]) if err != nil { base.Fatalf("failed to read Lua file: %s", err) @@ -67,14 +67,7 @@ func executeSign(cmd *base.Command, args []string) { func writeSignedFile(path string, code []byte, mode os.FileMode) error { if info, err := os.Stat(path); err == nil { - if !info.Mode().IsRegular() { - return &os.PathError{Op: "write", Path: path, Err: os.ErrInvalid} - } mode = info.Mode().Perm() - path, err = filepath.EvalSymlinks(path) - if err != nil { - return err - } } else if !os.IsNotExist(err) { return err } diff --git a/main/confloader/external/external.go b/main/confloader/external/external.go index 412890bb2..340bc9f2f 100644 --- a/main/confloader/external/external.go +++ b/main/confloader/external/external.go @@ -58,18 +58,10 @@ func readConfigFile(file *os.File) ([]byte, error) { return nil, err } if info.Mode().IsRegular() { - path, err := filepath.Abs(file.Name()) - if err != nil { - return nil, err - } - path, err = filepath.EvalSymlinks(path) - if err != nil { - return nil, err - } - switch strings.ToLower(filepath.Ext(path)) { + switch strings.ToLower(filepath.Ext(file.Name())) { case ".jsonc", ".json", ".yml", ".yaml", ".toml", ".pb", ".protobuf": default: - return nil, errors.New("unsupported config file extension: ", path) + return nil, errors.New("unsupported config file extension: ", file.Name()) } } return io.ReadAll(file)