Skip to content

Commit c10b0f6

Browse files
committed
fix: 修复 Trojan URI 解析
1 parent aa7b689 commit c10b0f6

3 files changed

Lines changed: 115 additions & 26 deletions

File tree

backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "sub-store",
3-
"version": "2.36.1",
3+
"version": "2.36.2",
44
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.",
55
"main": "src/main.js",
66
"packageManager": "pnpm@11.0.9",

backend/src/core/proxy-utils/parsers/peggy/trojan-uri.js

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,27 @@
11
// trojan-parser.js
22

3+
import {
4+
extractPathQueryParam,
5+
getPathQueryParam,
6+
parseSafeIntegerValue,
7+
} from '../../transport-path';
8+
import { isIPv6 } from '@/utils';
9+
10+
const unsafePathSegments = new Set(["__proto__", "constructor", "prototype"]);
11+
312
let parser;
413

514
function $set(obj, path, value) {
615
if (Object(obj) !== obj) return obj;
716
if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || [];
17+
if (path.some((segment) => unsafePathSegments.has(segment))) {
18+
throw new Error("Unsafe property path");
19+
}
820

921
path
1022
.slice(0, -1)
1123
.reduce((a, c, i) => (
12-
Object(a[c]) === a[c]
24+
Object.prototype.hasOwnProperty.call(a, c) && Object(a[c]) === a[c]
1325
? a[c]
1426
: (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})
1527
), obj)[path[path.length - 1]] = value;
@@ -22,14 +34,8 @@ function toBool(str) {
2234
return /(TRUE)|1/i.test(str);
2335
}
2436

25-
function parseEarlyDataSize(value) {
26-
if (value == null || !/^\d+$/.test(String(value))) return null;
27-
const n = parseInt(value, 10);
28-
return Number.isSafeInteger(n) ? n : null;
29-
}
30-
3137
function isNumericEarlyData(value) {
32-
return parseEarlyDataSize(value) != null;
38+
return parseSafeIntegerValue(value) != null;
3339
}
3440

3541
function decode(value) {
@@ -45,7 +51,7 @@ function parseTrojan(url) {
4551
const proxy = {};
4652

4753
const match = url.match(
48-
/^trojan:\/\/([^@]+)@([^:/?#]+):(\d+)(?:\/)?(?:\?([^#]*))?(?:#(.*))?$/
54+
/^trojan:\/\/([^@]+)@(\[[^\]]+\]|[^/?#]+):(\d+)(?:\/)?(?:\?([^#]*))?(?:#(.*))?$/
4955
);
5056

5157
if (!match) {
@@ -61,11 +67,19 @@ function parseTrojan(url) {
6167
name
6268
] = match;
6369

70+
const normalizedServer = server.replace(/^\[|\]$/g, "");
71+
if ((server.startsWith("[") || server.includes(":")) && !isIPv6(normalizedServer)) {
72+
throw new Error(`Invalid server: ${server}`);
73+
}
74+
6475

6576
proxy.type = "trojan";
6677
proxy.password = decode(password);
6778
proxy.server = server;
6879
proxy.port = Number(port);
80+
if (!Number.isSafeInteger(proxy.port) || proxy.port < 1 || proxy.port > 65535) {
81+
throw new Error(`Invalid port: ${port}`);
82+
}
6983

7084
proxy.name = name
7185
? decode(name)
@@ -76,8 +90,11 @@ function parseTrojan(url) {
7690

7791
if (query) {
7892
for (const item of query.split("&")) {
79-
const [k, v = ""] = item.split("=");
80-
params[k] = decode(v);
93+
const separatorIndex = item.indexOf("=");
94+
const key = separatorIndex === -1 ? item : item.slice(0, separatorIndex);
95+
params[key] = separatorIndex === -1
96+
? true
97+
: decode(item.slice(separatorIndex + 1));
8198
}
8299
}
83100

@@ -144,18 +161,12 @@ function parseTrojan(url) {
144161

145162
if (proxy.network === "ws") {
146163

147-
const ed = new URL(
148-
"http://a" + path
149-
).searchParams.get("ed");
164+
const ed = getPathQueryParam(path, "ed");
150165

151166

152167
if (isNumericEarlyData(ed)) {
153168

154-
path =
155-
path.replace(
156-
/[?&]ed=\d+/,
157-
""
158-
);
169+
path = extractPathQueryParam(path, "ed").path;
159170

160171

161172
if (httpupgrade)
@@ -227,7 +238,7 @@ function parseTrojan(url) {
227238
$set(
228239
proxy,
229240
"ws-opts.max-early-data",
230-
parseEarlyDataSize(pathEarlyData)
241+
parseSafeIntegerValue(pathEarlyData)
231242
);
232243

233244

@@ -253,6 +264,12 @@ function parseTrojan(url) {
253264
if (params.spx)
254265
opts["_spider-x"] = params.spx;
255266

267+
if (params.mode)
268+
proxy._mode = params.mode;
269+
270+
if (params.extra)
271+
proxy._extra = params.extra;
272+
256273

257274
if (Object.keys(opts).length) {
258275
$set(
@@ -281,4 +298,4 @@ export default function getParser() {
281298
}
282299

283300
return parser;
284-
}
301+
}

backend/src/test/proxy-parsers/uri.spec.js

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -620,7 +620,7 @@ describe('Proxy URI parser coverage', function () {
620620

621621
it('parses Trojan URIs with websocket transport', function () {
622622
const proxy = parseOne(
623-
'trojan://trojan-pass@trojan-ws.example.com?type=ws&host=ws.example.com&path=%2Fws%3Fa%3D1%26ed%3D1024%26b%3D2#Trojan%20WS',
623+
'trojan://trojan-pass@trojan-ws.example.com?type=ws&host=ws.example.com&path=%2Fws%3Fed%3D1024%26a%3D1%26b%3D2#Trojan%20WS',
624624
);
625625

626626
expectSubset(proxy, {
@@ -641,6 +641,35 @@ describe('Proxy URI parser coverage', function () {
641641
});
642642
});
643643

644+
it('parses Trojan IPv6 URIs with valueless flags', function () {
645+
const proxy = parseOne(
646+
'trojan://trojan-pass@[2001:db8::1]:443?udp&tfo&ws&wspath=%2Fws#Trojan%20IPv6',
647+
);
648+
649+
expectSubset(proxy, {
650+
type: 'trojan',
651+
name: 'Trojan IPv6',
652+
server: '2001:db8::1',
653+
port: 443,
654+
network: 'ws',
655+
udp: true,
656+
tfo: true,
657+
'ws-opts': {
658+
path: '/ws',
659+
},
660+
});
661+
662+
expectSubset(
663+
parseOne(
664+
'trojan://trojan-pass@2001:db8::1:443#Trojan%20Bare%20IPv6',
665+
),
666+
{
667+
server: '2001:db8::1',
668+
port: 443,
669+
},
670+
);
671+
});
672+
644673
it('does not double-decode Trojan path query values before extracting early data', function () {
645674
const proxy = parseOne(
646675
`trojan://trojan-pass@trojan-ws.example.com?type=ws&host=ws.example.com&path=${encodeURIComponent(
@@ -689,7 +718,7 @@ describe('Proxy URI parser coverage', function () {
689718

690719
it('parses Trojan URIs with grpc reality metadata', function () {
691720
const proxy = parseOne(
692-
'trojan://trojan-pass@trojan-grpc.example.com?type=grpc&serviceName=grpc-service&authority=grpc.example.com&mode=multi&security=reality&pbk=pubkey&sid=08&spx=%2Fspider&udp=1&tfo=1#Trojan%20Reality',
721+
'trojan://trojan-pass@trojan-grpc.example.com?type=grpc&serviceName=grpc-service&authority=grpc.example.com&mode=multi&security=reality&pbk=pubkey==&sid=08&spx=%2Fspider&extra=%7B%22x%22%3A1%7D&udp=1&tfo=1#Trojan%20Reality',
693722
);
694723

695724
expectSubset(proxy, {
@@ -707,12 +736,55 @@ describe('Proxy URI parser coverage', function () {
707736
'_grpc-authority': 'grpc.example.com',
708737
},
709738
'reality-opts': {
710-
'public-key': 'pubkey',
739+
'public-key': 'pubkey==',
711740
'short-id': '08',
712741
'_spider-x': '/spider',
713742
},
714743
_mode: 'multi',
715-
});
744+
_extra: '{"x":1}',
745+
});
746+
});
747+
748+
it('rejects Trojan URIs with out-of-range ports', function () {
749+
for (const port of [0, 65536]) {
750+
expect(
751+
parseAll(
752+
`trojan://trojan-pass@trojan.example.com:${port}#Invalid%20Port`,
753+
),
754+
).to.deep.equal([]);
755+
}
756+
});
757+
758+
it('rejects colon-containing Trojan hosts that are not IPv6', function () {
759+
expect(
760+
parseAll(
761+
'trojan://trojan-pass@host:123:443#Invalid%20Host',
762+
),
763+
).to.deep.equal([]);
764+
});
765+
766+
it('rejects Trojan transport paths that target object prototypes', function () {
767+
const pollutedKey = 'ws-opts';
768+
769+
for (const type of ['__proto__.ws', 'constructor.prototype.ws']) {
770+
delete Object.prototype[pollutedKey];
771+
772+
try {
773+
expect(
774+
parseAll(
775+
`trojan://trojan-pass@trojan.example.com:443?type=${type}&path=%2Fx#Prototype%20Pollution`,
776+
),
777+
).to.deep.equal([]);
778+
expect(
779+
Object.prototype.hasOwnProperty.call(
780+
Object.prototype,
781+
pollutedKey,
782+
),
783+
).to.equal(false);
784+
} finally {
785+
delete Object.prototype[pollutedKey];
786+
}
787+
}
716788
});
717789
});
718790
});

0 commit comments

Comments
 (0)