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:
MHSanaei
2026-10-31 08:53:27 +00:00
parent 3cdb7e2b49
commit 534afe13ab
10 changed files with 203 additions and 21 deletions
+13
View File
@@ -3,6 +3,7 @@ package sub
import (
"encoding/base64"
"net/url"
"slices"
"strings"
"testing"
"time"
@@ -176,3 +177,15 @@ func TestClashProxyFromExternalTrojanReality(t *testing.T) {
t.Fatalf("expected reality→tls true, got %v", proxy["tls"])
}
}
// Only server links pass: an app deeplink or a plain URL from the provider would reach the
// subscriber's app as a directive (a routing profile, an import) the operator never chose.
func TestDecodeSubscriptionBodyKeepsOnlyServerShareLinks(t *testing.T) {
body := []byte("VLESS://u@a:1#a\nincy://autorouting/onadd/https://p.example/r.json\nhttps://p.example/page\n" +
"happ://routing/add/x\nvpn://payload\ntuic://u:p@a:1#t\nhy2://p@a:1#h\nnot a link://x\n")
got := decodeSubscriptionBody(body)
want := []string{"VLESS://u@a:1#a", "vpn://payload", "tuic://u:p@a:1#t", "hy2://p@a:1#h"}
if !slices.Equal(got, want) {
t.Fatalf("decoded links = %q, want %q", got, want)
}
}