mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-10 21:30:59 +00:00
fix(server): guard access-log parser against malformed lines
GetXrayLogs split each Xray access-log line on whitespace and then read fixed offsets — parts[1] for the timestamp and parts[i+1] after the "from", "accepted" and "email:" markers — without checking the line had that many fields. A truncated or malformed line (the logged destination is attacker-influenced) indexed past the slice and panicked; the panel handler returned a 500 via Gin's recovery. Extract the per-line field parsing into parseAccessLogFields and length guard every positional lookup so a short line yields a partial entry instead of panicking.
This commit is contained in:
@@ -0,0 +1,49 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// Xray access-log lines carry attacker-influenced content (a client's requested
|
||||||
|
// destination is logged verbatim) and can be truncated. parseAccessLogFields
|
||||||
|
// must never panic on a short or malformed line, and must still parse a
|
||||||
|
// well-formed line correctly.
|
||||||
|
func TestParseAccessLogFields(t *testing.T) {
|
||||||
|
malformed := []string{
|
||||||
|
"",
|
||||||
|
"singletoken",
|
||||||
|
"2024/01/02",
|
||||||
|
"2024/01/02 15:04:05.000000 from",
|
||||||
|
"2024/01/02 15:04:05.000000 accepted",
|
||||||
|
"2024/01/02 15:04:05.000000 email:",
|
||||||
|
}
|
||||||
|
for _, line := range malformed {
|
||||||
|
func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("parseAccessLogFields panicked on %q: %v", line, r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
_ = parseAccessLogFields(line)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
line := "2024/01/02 15:04:05.123456 from 1.2.3.4:555 accepted tcp:example.com:443 [inbound-tag >> outbound-tag] email: alice@example.com"
|
||||||
|
entry := parseAccessLogFields(line)
|
||||||
|
if entry.FromAddress != "1.2.3.4:555" {
|
||||||
|
t.Errorf("FromAddress = %q, want %q", entry.FromAddress, "1.2.3.4:555")
|
||||||
|
}
|
||||||
|
if entry.ToAddress != "tcp:example.com:443" {
|
||||||
|
t.Errorf("ToAddress = %q, want %q", entry.ToAddress, "tcp:example.com:443")
|
||||||
|
}
|
||||||
|
if entry.Inbound != "inbound-tag" {
|
||||||
|
t.Errorf("Inbound = %q, want %q", entry.Inbound, "inbound-tag")
|
||||||
|
}
|
||||||
|
if entry.Outbound != "outbound-tag" {
|
||||||
|
t.Errorf("Outbound = %q, want %q", entry.Outbound, "outbound-tag")
|
||||||
|
}
|
||||||
|
if entry.Email != "alice@example.com" {
|
||||||
|
t.Errorf("Email = %q, want %q", entry.Email, "alice@example.com")
|
||||||
|
}
|
||||||
|
if entry.DateTime.IsZero() {
|
||||||
|
t.Error("DateTime was not parsed from a well-formed line")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1132,6 +1132,40 @@ func (s *ServerService) GetLogs(count string, level string, syslog string) []str
|
|||||||
return lines
|
return lines
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseAccessLogFields extracts the structured fields from one Xray access-log
|
||||||
|
// line. Lines are attacker-influenced (a client's requested destination lands in
|
||||||
|
// the log verbatim) and may be truncated, so every positional lookup is length
|
||||||
|
// guarded: a malformed line yields a partial entry rather than panicking.
|
||||||
|
func parseAccessLogFields(line string) LogEntry {
|
||||||
|
var entry LogEntry
|
||||||
|
parts := strings.Fields(line)
|
||||||
|
|
||||||
|
for i, part := range parts {
|
||||||
|
|
||||||
|
if i == 0 && len(parts) > 1 {
|
||||||
|
dateTime, err := time.ParseInLocation("2006/01/02 15:04:05.999999", parts[0]+" "+parts[1], time.Local)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entry.DateTime = dateTime.UTC()
|
||||||
|
}
|
||||||
|
|
||||||
|
if part == "from" && i+1 < len(parts) {
|
||||||
|
entry.FromAddress = strings.TrimLeft(parts[i+1], "/")
|
||||||
|
} else if part == "accepted" && i+1 < len(parts) {
|
||||||
|
entry.ToAddress = strings.TrimLeft(parts[i+1], "/")
|
||||||
|
} else if strings.HasPrefix(part, "[") {
|
||||||
|
entry.Inbound = part[1:]
|
||||||
|
} else if strings.HasSuffix(part, "]") {
|
||||||
|
entry.Outbound = part[:len(part)-1]
|
||||||
|
} else if part == "email:" && i+1 < len(parts) {
|
||||||
|
entry.Email = parts[i+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ServerService) GetXrayLogs(
|
func (s *ServerService) GetXrayLogs(
|
||||||
count string,
|
count string,
|
||||||
filter string,
|
filter string,
|
||||||
@@ -1176,31 +1210,7 @@ func (s *ServerService) GetXrayLogs(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var entry LogEntry
|
entry := parseAccessLogFields(line)
|
||||||
parts := strings.Fields(line)
|
|
||||||
|
|
||||||
for i, part := range parts {
|
|
||||||
|
|
||||||
if i == 0 {
|
|
||||||
dateTime, err := time.ParseInLocation("2006/01/02 15:04:05.999999", parts[0]+" "+parts[1], time.Local)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
entry.DateTime = dateTime.UTC()
|
|
||||||
}
|
|
||||||
|
|
||||||
if part == "from" {
|
|
||||||
entry.FromAddress = strings.TrimLeft(parts[i+1], "/")
|
|
||||||
} else if part == "accepted" {
|
|
||||||
entry.ToAddress = strings.TrimLeft(parts[i+1], "/")
|
|
||||||
} else if strings.HasPrefix(part, "[") {
|
|
||||||
entry.Inbound = part[1:]
|
|
||||||
} else if strings.HasSuffix(part, "]") {
|
|
||||||
entry.Outbound = part[:len(part)-1]
|
|
||||||
} else if part == "email:" {
|
|
||||||
entry.Email = parts[i+1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if logEntryContains(line, freedoms) {
|
if logEntryContains(line, freedoms) {
|
||||||
if showDirect == "false" {
|
if showDirect == "false" {
|
||||||
|
|||||||
Reference in New Issue
Block a user