mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-10-14 19:44:02 +00:00
lua: prevent configuration files from being executed as scripts
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type configFile struct {
|
||||
path string
|
||||
info os.FileInfo
|
||||
}
|
||||
|
||||
var configFiles []configFile
|
||||
|
||||
// ReadConfigFile reads a config source and records local regular files.
|
||||
func ReadConfigFile(file *os.File) ([]byte, error) {
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path := ""
|
||||
if info.Mode().IsRegular() && file != os.Stdin {
|
||||
path, err = filepath.Abs(file.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path, err = filepath.EvalSymlinks(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info.Mode().IsRegular() {
|
||||
configFiles = append(configFiles, configFile{path: path, info: info})
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func isConfigFile(path string, info os.FileInfo) bool {
|
||||
for _, file := range configFiles {
|
||||
if file.path == path || os.SameFile(file.info, info) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -113,11 +113,18 @@ func ResolveLuaFile(path string) (string, error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
path, err := resolveFile(paths)
|
||||
path, info, err := resolveFile(paths)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resolveLuaTarget(path)
|
||||
path, err = resolveLuaTarget(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if isConfigFile(path, info) {
|
||||
return "", fmt.Errorf("Lua file was loaded as a configuration file: %s", path)
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func resolveLuaTarget(path string) (string, error) {
|
||||
@@ -131,12 +138,12 @@ func resolveLuaTarget(path string) (string, error) {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func resolveFile(paths []string) (string, error) {
|
||||
func resolveFile(paths []string) (string, os.FileInfo, 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)
|
||||
return "", nil, fmt.Errorf("failed to resolve file path: %w", err)
|
||||
}
|
||||
tried = append(tried, path)
|
||||
info, err := os.Stat(path)
|
||||
@@ -144,12 +151,12 @@ func resolveFile(paths []string) (string, error) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to inspect file %q: %w", path, err)
|
||||
return "", nil, 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 "", nil, fmt.Errorf("file is not a regular file: %s", path)
|
||||
}
|
||||
return path, nil
|
||||
return path, info, nil
|
||||
}
|
||||
return "", fmt.Errorf("file not found; tried %q: %w", tried, os.ErrNotExist)
|
||||
return "", nil, fmt.Errorf("file not found; tried %q: %w", tried, os.ErrNotExist)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
package serial_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
xlua "github.com/xtls/xray-core/common/lua"
|
||||
"github.com/xtls/xray-core/core"
|
||||
"github.com/xtls/xray-core/infra/conf"
|
||||
"github.com/xtls/xray-core/infra/conf/serial"
|
||||
_ "github.com/xtls/xray-core/main/confloader/external"
|
||||
glua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
func TestConfigSourceSecurity(t *testing.T) {
|
||||
// Keep registered files alive until every file identity check finishes.
|
||||
dir := t.TempDir()
|
||||
t.Run("polyglots", testConfigLuaPolyglots)
|
||||
t.Run("merged", func(t *testing.T) { testMergedConfigCannotRunAsLua(t, dir) })
|
||||
t.Run("hardlink", func(t *testing.T) { testConfigHardLinkCannotRunAsLua(t, dir) })
|
||||
t.Run("stdin", func(t *testing.T) { testStdinConfigCannotRunAsLua(t, dir) })
|
||||
}
|
||||
|
||||
func testConfigLuaPolyglots(t *testing.T) {
|
||||
const luaBody = "polyglotExecuted = true\nfunction HandleRoute() return 'direct' end\n"
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
decode func(io.Reader) (*conf.Config, error)
|
||||
source string
|
||||
script string
|
||||
}{
|
||||
{"JSON", serial.DecodeJSONConfig, "null = 0\n" + luaBody, ""},
|
||||
{"YAML", serial.DecodeYAMLConfig, `--[[: ignored
|
||||
routing:
|
||||
script: payload.lua
|
||||
--]] --: ignored
|
||||
_G: assert((function() polyglotExecuted = true; function HandleRoute() return 'direct' end end)())
|
||||
`, "payload.lua"},
|
||||
{"TOML", serial.DecodeTOMLConfig, `routing = { script = "payload.lua" }
|
||||
-- = """
|
||||
polyglotExecuted = true
|
||||
function HandleRoute() return 'direct' end
|
||||
-- """
|
||||
`, "payload.lua"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
config, err := tc.decode(strings.NewReader(tc.source))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
script := ""
|
||||
if config.RouterConfig != nil {
|
||||
script = config.RouterConfig.Script
|
||||
}
|
||||
if script != tc.script {
|
||||
t.Fatalf("routing.script = %q, want %q", script, tc.script)
|
||||
}
|
||||
if built, err := config.Build(); err != nil || built == nil {
|
||||
t.Fatalf("config Build failed: %v", err)
|
||||
}
|
||||
if _, err := serial.DecodeJSONConfigStrict(strings.NewReader(tc.source)); err == nil {
|
||||
t.Fatal("strict JSON accepted the polyglot")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
t.Setenv("xray.location.confdir", dir)
|
||||
if err := os.WriteFile(filepath.Join(dir, "payload.lua"), []byte(tc.source), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
configPath := filepath.Join(dir, "payload."+strings.ToLower(tc.name))
|
||||
if err := os.WriteFile(configPath, []byte(tc.source), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if program, err := xlua.Compile(configPath); program != nil || err == nil || !strings.Contains(err.Error(), "Lua file extension must be .lua") {
|
||||
t.Fatalf("Compile(%q) = %v, %v; want .lua extension rejection", configPath, program, err)
|
||||
}
|
||||
program, err := xlua.Compile("payload.lua")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
state, err := program.NewState(ctx, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer state.Close()
|
||||
if state.GetGlobal("polyglotExecuted") != glua.LTrue {
|
||||
t.Fatal("Lua top-level code did not execute")
|
||||
}
|
||||
if err := state.CallByParam(glua.P{Fn: state.GetGlobal("HandleRoute"), NRet: 1, Protect: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := state.Get(-1); got != glua.LString("direct") {
|
||||
t.Fatalf("HandleRoute() = %v, want direct", got)
|
||||
}
|
||||
t.Logf("config built with routing.script=%q; the same bytes executed as Lua", script)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testMergedConfigCannotRunAsLua(t *testing.T, dir string) {
|
||||
t.Setenv("xray.location.confdir", dir)
|
||||
base := filepath.Join(dir, "base.lua")
|
||||
for format, body := range map[string]string{
|
||||
"json": "null = 0\nfunction HandleRoute() return 'direct' end\n",
|
||||
"yaml": "_G: assert((function() function HandleRoute() return 'direct' end end)())\n",
|
||||
"toml": "-- = \"\"\"\nfunction HandleRoute() return 'direct' end\n-- \"\"\"\n",
|
||||
} {
|
||||
t.Run(format, func(t *testing.T) {
|
||||
path := filepath.Join(dir, format+".lua")
|
||||
if err := os.WriteFile(base, []byte(`{"routing":{"script":"`+format+`.lua"}}`), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if program, err := xlua.Compile(path); err != nil || program == nil {
|
||||
t.Fatalf("fixture must compile before config loading: %v", err)
|
||||
}
|
||||
if _, err := serial.BuildConfig([]*core.ConfigSource{
|
||||
{Name: base, Format: "json"},
|
||||
{Name: path, Format: format},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, source := range []string{base, path} {
|
||||
if program, err := xlua.Compile(source); program != nil || err == nil || !strings.Contains(err.Error(), "loaded as a configuration file") {
|
||||
t.Fatalf("Compile(%q) = %v, %v; want config source rejection", source, program, err)
|
||||
}
|
||||
}
|
||||
if err := os.Rename(path, path+".old"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte("function HandleRoute() end"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if program, err := xlua.Compile(path); program != nil || err == nil || !strings.Contains(err.Error(), "loaded as a configuration file") {
|
||||
t.Fatalf("Compile = %v, %v; want replaced config path rejection", program, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
path := filepath.Join(dir, "ordinary.lua")
|
||||
if err := os.WriteFile(path, []byte("function HandleRoute() end"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if program, err := xlua.Compile(path); err != nil || program == nil {
|
||||
t.Fatalf("ordinary script in the config directory must remain usable: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func testConfigHardLinkCannotRunAsLua(t *testing.T, dir string) {
|
||||
path := filepath.Join(dir, "config.json")
|
||||
if err := os.WriteFile(path, []byte("null = 0\nfunction HandleRoute() end"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
alias := filepath.Join(dir, "alias.lua")
|
||||
if err := os.Link(path, alias); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := serial.BuildConfig([]*core.ConfigSource{{Name: path, Format: "json"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if program, err := xlua.Compile(alias); program != nil || err == nil || !strings.Contains(err.Error(), "loaded as a configuration file") {
|
||||
t.Fatalf("Compile = %v, %v; want config hard link rejection", program, err)
|
||||
}
|
||||
}
|
||||
|
||||
func testStdinConfigCannotRunAsLua(t *testing.T, dir string) {
|
||||
path := filepath.Join(dir, "config.lua")
|
||||
if err := os.WriteFile(path, []byte("null = 0\nfunction HandleRoute() end"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
originalStdin := os.Stdin
|
||||
os.Stdin = file
|
||||
t.Cleanup(func() {
|
||||
os.Stdin = originalStdin
|
||||
file.Close()
|
||||
})
|
||||
if _, err := serial.BuildConfig([]*core.ConfigSource{{Name: "stdin:", Format: "json"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if program, err := xlua.Compile(path); program != nil || err == nil || !strings.Contains(err.Error(), "loaded as a configuration file") {
|
||||
t.Fatalf("Compile = %v, %v; want redirected config rejection", program, err)
|
||||
}
|
||||
}
|
||||
Vendored
+8
-2
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/platform"
|
||||
"github.com/xtls/xray-core/common/utils"
|
||||
"github.com/xtls/xray-core/main/confloader"
|
||||
)
|
||||
@@ -28,10 +29,15 @@ func ConfigLoader(arg string) (out io.Reader, err error) {
|
||||
data, err = FetchHTTPContent(arg)
|
||||
|
||||
case arg == "stdin:":
|
||||
data, err = io.ReadAll(os.Stdin)
|
||||
data, err = platform.ReadConfigFile(os.Stdin)
|
||||
|
||||
default:
|
||||
data, err = os.ReadFile(arg)
|
||||
file, openErr := os.Open(arg)
|
||||
if openErr != nil {
|
||||
return nil, openErr
|
||||
}
|
||||
defer file.Close()
|
||||
data, err = platform.ReadConfigFile(file)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user