diff --git a/app/geodata/download.go b/app/geodata/download.go index e24e3e4fd..11218e7fa 100644 --- a/app/geodata/download.go +++ b/app/geodata/download.go @@ -23,6 +23,9 @@ import ( const idleTimeout = 30 * time.Second +// DO NOT EDIT. Ignored by geodata, rejected by Lua. +const luaGuardHeader = "\x12\x01\x00" + type stage struct { target string temp string @@ -170,6 +173,10 @@ func (d *downloader) downloadOne(asset *Asset) (stage, error) { } }() + if _, err := temp.WriteString(luaGuardHeader); err != nil { + temp.Close() + return stage{}, err + } if err := d.fetch(asset.Url, temp); err != nil { temp.Close() return stage{}, err diff --git a/app/geodata/download_test.go b/app/geodata/download_test.go new file mode 100644 index 000000000..32b9fa61c --- /dev/null +++ b/app/geodata/download_test.go @@ -0,0 +1,66 @@ +package geodata + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + commongeodata "github.com/xtls/xray-core/common/geodata" + xlua "github.com/xtls/xray-core/common/lua" + "github.com/yuin/gopher-lua/parse" + "google.golang.org/protobuf/proto" +) + +func TestDownloadOneLuaGuard(t *testing.T) { + for _, tc := range []struct { + name string + body string + code string + }{ + {"geodata", "\x0a\x03\x0a\x01A", "A"}, + {"prefixed geodata", luaGuardHeader + "\x0a\x03\x0a\x01A", "A"}, + {"Lua", "function HandleRoute() end", ""}, + } { + t.Run(tc.name, func(t *testing.T) { + dir := t.TempDir() + t.Setenv("xray.location.asset", dir) + if err := os.WriteFile(filepath.Join(dir, "asset.lua"), nil, 0o600); err != nil { + t.Fatal(err) + } + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte(tc.body)) + })) + defer server.Close() + d := &downloader{ctx: context.Background(), httpClient: server.Client()} + staged, err := d.downloadOne(&Asset{Url: server.URL, File: "asset.lua"}) + if err != nil { + t.Fatal(err) + } + data, err := os.ReadFile(staged.temp) + if err != nil || string(data) != luaGuardHeader+tc.body { + t.Fatalf("downloaded data = % x, %v", data, err) + } + program, err := xlua.Compile(staged.temp) + var syntaxErr *parse.Error + if program != nil || !errors.As(err, &syntaxErr) || syntaxErr.Pos.Line != 1 || syntaxErr.Pos.Column != 1 { + t.Fatalf("want Lua rejection at the first byte, got %v, %v", program, err) + } + if tc.code != "" { + var list commongeodata.GeoSiteList + if err := proto.Unmarshal(data, &list); err != nil { + t.Fatal(err) + } + if len(list.Entry) != 1 || list.Entry[0].Code != tc.code { + t.Fatalf("header changed geodata: %v", &list) + } + if _, err := commongeodata.ParseDomainRules([]string{"ext:" + filepath.Base(staged.temp) + ":" + tc.code}, commongeodata.Domain_Full); err != nil { + t.Fatalf("geodata loader rejected the header: %v", err) + } + } + }) + } +} diff --git a/common/lua/geodata_security_watchdog_test.go b/common/lua/geodata_security_watchdog_test.go new file mode 100644 index 000000000..07e9b991c --- /dev/null +++ b/common/lua/geodata_security_watchdog_test.go @@ -0,0 +1,19 @@ +// DO NOT EDIT. Regression test for app/geodata's luaGuardHeader. + +package lua + +import ( + "errors" + "strings" + "testing" + + "github.com/yuin/gopher-lua/parse" +) + +func TestGeodataLuaGuard(t *testing.T) { + _, err := parse.Parse(strings.NewReader("\x12\x01\x00function HandleRoute() end"), "geodata") + var syntaxErr *parse.Error + if !errors.As(err, &syntaxErr) || syntaxErr.Pos.Line != 1 || syntaxErr.Pos.Column != 1 { + t.Fatalf("want Lua rejection at the first byte, got %v", err) + } +}