mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-04 09:27:15 +00:00
fix(xray): confine log paths written under any key case
resolveXrayLogPaths looked the log object up by the exact keys "access" and "error", but xray-core decodes that object with encoding/json, which falls back to a case-insensitive field match. "Access": "/tmp/pwn.log" therefore reached AccessLog untouched and Xray — root, in a standard install — created the file there, reopening the arbitrary write that GHSA-jm48-m3rr-9hgg closed. Fold every case variant onto the canonical key before confining it. When both a canonical key and a variant are present the canonical value wins, so a "none" cannot be overridden by a smuggled "Access" path.
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@@ -1034,6 +1035,19 @@ func ensureStatsPolicy(policy json_util.RawMessage) json_util.RawMessage {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// caseVariantKeys returns every key of parsed that equals want ignoring case,
|
||||||
|
// lowest first so the fold is deterministic when several variants are present.
|
||||||
|
func caseVariantKeys(parsed map[string]any, want string) []string {
|
||||||
|
var keys []string
|
||||||
|
for key := range parsed {
|
||||||
|
if strings.EqualFold(key, want) {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slices.Sort(keys)
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
func resolveXrayLogPaths(logCfg json_util.RawMessage) json_util.RawMessage {
|
func resolveXrayLogPaths(logCfg json_util.RawMessage) json_util.RawMessage {
|
||||||
if len(logCfg) == 0 {
|
if len(logCfg) == 0 {
|
||||||
return logCfg
|
return logCfg
|
||||||
@@ -1044,12 +1058,29 @@ func resolveXrayLogPaths(logCfg json_util.RawMessage) json_util.RawMessage {
|
|||||||
}
|
}
|
||||||
changed := false
|
changed := false
|
||||||
for _, key := range []string{"access", "error"} {
|
for _, key := range []string{"access", "error"} {
|
||||||
v, ok := parsed[key].(string)
|
// xray-core decodes this object with encoding/json, whose case-insensitive
|
||||||
|
// field match makes "Access" reach AccessLog too — fold every variant.
|
||||||
|
variants := caseVariantKeys(parsed, key)
|
||||||
|
value, hasValue := parsed[key]
|
||||||
|
for _, variant := range variants {
|
||||||
|
if variant == key {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !hasValue {
|
||||||
|
value, hasValue = parsed[variant], true
|
||||||
|
}
|
||||||
|
delete(parsed, variant)
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
v, ok := value.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
trimmed := strings.TrimSpace(v)
|
trimmed := strings.TrimSpace(v)
|
||||||
if trimmed == "" || strings.EqualFold(trimmed, "none") {
|
if trimmed == "" || strings.EqualFold(trimmed, "none") {
|
||||||
|
if changed {
|
||||||
|
parsed[key] = v
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
base := path.Base(filepath.ToSlash(trimmed))
|
base := path.Base(filepath.ToSlash(trimmed))
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mhsanaei/3x-ui/v3/internal/config"
|
||||||
|
"github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A log path must never escape the log folder whatever case the key is written
|
||||||
|
// in: xray-core matches JSON keys onto its struct fields case-insensitively.
|
||||||
|
func TestResolveXrayLogPathsConfinesEveryKeyCase(t *testing.T) {
|
||||||
|
folder := config.GetLogFolder()
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
in string
|
||||||
|
want map[string]any
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "lowercase keys",
|
||||||
|
in: `{"access":"/tmp/pwn.log","error":"/tmp/pwn-err.log"}`,
|
||||||
|
want: map[string]any{"access": filepath.Join(folder, "pwn.log"), "error": filepath.Join(folder, "pwn-err.log")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "capitalised keys",
|
||||||
|
in: `{"Access":"/tmp/pwn.log","Error":"/tmp/pwn-err.log"}`,
|
||||||
|
want: map[string]any{"access": filepath.Join(folder, "pwn.log"), "error": filepath.Join(folder, "pwn-err.log")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "upper-case keys",
|
||||||
|
in: `{"ACCESS":"/tmp/pwn.log"}`,
|
||||||
|
want: map[string]any{"access": filepath.Join(folder, "pwn.log")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "a variant cannot smuggle a path past a none",
|
||||||
|
in: `{"access":"none","Access":"/tmp/pwn.log"}`,
|
||||||
|
want: map[string]any{"access": "none"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "already confined name is left alone",
|
||||||
|
in: `{"access":"none","error":"none","loglevel":"warning"}`,
|
||||||
|
want: map[string]any{"access": "none", "error": "none", "loglevel": "warning"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
out := resolveXrayLogPaths(json_util.RawMessage(tt.in))
|
||||||
|
var got map[string]any
|
||||||
|
if err := json.Unmarshal(out, &got); err != nil {
|
||||||
|
t.Fatalf("unmarshal %s: %v", out, err)
|
||||||
|
}
|
||||||
|
if len(got) != len(tt.want) {
|
||||||
|
t.Fatalf("got %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
for key, want := range tt.want {
|
||||||
|
if got[key] != want {
|
||||||
|
t.Fatalf("key %q: got %v, want %v", key, got[key], want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for key := range got {
|
||||||
|
if strings.EqualFold(key, "access") && key != "access" {
|
||||||
|
t.Fatalf("case variant %q survived in %v", key, got)
|
||||||
|
}
|
||||||
|
if strings.EqualFold(key, "error") && key != "error" {
|
||||||
|
t.Fatalf("case variant %q survived in %v", key, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user