mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 11:15:03 +03:00
fix(sub): treat external subscription providers as untrusted input
A client's external subscription is fetched from a third party on every subscriber refresh, and the fetcher trusted it more than its siblings: - it dialled any address, redirects included; it now goes through the SSRF guard like the outbound subscription and remote routing fetchers; - it forwarded any line containing "://"; it now keeps only server share links, so app deeplinks and plain URLs never reach the subscriber's app; - it bounded the body but not the link count (now 1000) or the headers (now 64 KiB), and stored the raw error, URL included, on every row; the stored error is now host-only and at most 1 KiB. The link parser also refuses links that nest JSON past 16 levels, since the JSON subscription pretty-prints it at quadratic cost, and external configs are marshalled compactly until the final, single indent pass.
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
package common
|
||||
|
||||
import "strconv"
|
||||
import (
|
||||
"strconv"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// FormatTraffic formats traffic bytes into human-readable units (B, KB, MB, GB, TB, PB).
|
||||
func FormatTraffic(trafficBytes int64) string {
|
||||
@@ -14,3 +17,19 @@ func FormatTraffic(trafficBytes int64) string {
|
||||
}
|
||||
return strconv.FormatFloat(size, 'f', 2, 64) + units[unitIndex]
|
||||
}
|
||||
|
||||
// maxStatusTextBytes bounds an error text a third party shapes before it is stored as a
|
||||
// row's status; legitimate messages are a line or two.
|
||||
const maxStatusTextBytes = 1024
|
||||
|
||||
// TruncateStatusText cuts s to maxStatusTextBytes on a UTF-8 boundary.
|
||||
func TruncateStatusText(s string) string {
|
||||
if len(s) <= maxStatusTextBytes {
|
||||
return s
|
||||
}
|
||||
cut := maxStatusTextBytes
|
||||
for cut > 0 && !utf8.RuneStart(s[cut]) {
|
||||
cut--
|
||||
}
|
||||
return s[:cut] + "... (truncated)"
|
||||
}
|
||||
|
||||
@@ -131,9 +131,37 @@ func ParseLink(link string) (*ParseResult, error) {
|
||||
return nil, err
|
||||
}
|
||||
dropHostLocalKeys(res.Outbound)
|
||||
if !nestsWithin(map[string]any(res.Outbound), maxOutboundDepth) {
|
||||
return nil, fmt.Errorf("link nests JSON deeper than %d levels", maxOutboundDepth)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// maxOutboundDepth bounds the JSON a link can nest into its outbound (the deepest real shape,
|
||||
// an xhttp download stream's finalmask, is about 11): pretty-printing costs depth squared.
|
||||
const maxOutboundDepth = 16
|
||||
|
||||
func nestsWithin(v any, depth int) bool {
|
||||
if depth < 0 {
|
||||
return false
|
||||
}
|
||||
switch v := v.(type) {
|
||||
case map[string]any:
|
||||
for _, child := range v {
|
||||
if !nestsWithin(child, depth-1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
case []any:
|
||||
for _, child := range v {
|
||||
if !nestsWithin(child, depth-1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func parseLinkByScheme(link string) (*ParseResult, error) {
|
||||
switch {
|
||||
case strings.HasPrefix(link, "vmess://"):
|
||||
|
||||
@@ -661,3 +661,17 @@ func TestParseLinkDropsHostLocalKeys(t *testing.T) {
|
||||
t.Fatalf("remote-server settings were dropped too: %s", raw)
|
||||
}
|
||||
}
|
||||
|
||||
// Pretty-printing costs the square of the nesting depth, and extra=, fm= and the vmess
|
||||
// JSON copy provider JSON into the outbound; a link may not nest past real shapes.
|
||||
func TestParseLinkRejectsDeeplyNestedJSON(t *testing.T) {
|
||||
deep := strings.Repeat(`{"a":`, 40) + `1` + strings.Repeat(`}`, 40)
|
||||
if _, err := ParseLink("vless://uuid@8.8.8.8:443?type=xhttp&security=tls&extra=" + url.QueryEscape(`{"xmux":`+deep+`}`) + "#n"); err == nil {
|
||||
t.Fatal("a link nesting JSON 40 levels deep was accepted")
|
||||
}
|
||||
real := url.QueryEscape(`{"downloadSettings":{"address":"8.8.8.8","port":443,"network":"xhttp","xhttpSettings":{"headers":{"a":"b"}},` +
|
||||
`"finalmask":{"udp":[{"type":"noise","settings":{"noise":[{"type":"rand","packet":"10-20"}]}}]}}}`)
|
||||
if _, err := ParseLink("vless://uuid@8.8.8.8:443?type=xhttp&security=tls&extra=" + real + "#n"); err != nil {
|
||||
t.Fatalf("a real download stream with a finalmask was refused: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user