fix(fmt): decode the SIP002 plain-text password only once (#10520)

For a SIP002 link whose user info is "method:password" rather than
base64 (the 2022-blake3 form), ResolveSip002 unescapes the whole user
info and then unescapes the password a second time. A password that
still holds a valid percent sequence after the first pass decays:
p%41ss is written as p%2541ss and imported as pAss.

Same double-decoding as #10027 fixed for query values.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Wei-Lun Li
2026-11-04 10:34:45 +09:00
committed by GitHub
parent 9d1063f3f2
commit 0d9e84556e
2 changed files with 20 additions and 1 deletions
@@ -0,0 +1,19 @@
namespace ServiceLib.Tests.Fmt;
public class ShadowsocksFmtTests
{
// SIP002 user info that is not base64 is "method:password", percent-encoded once.
[Test]
[Arguments("ss://2022-blake3-aes-128-gcm:p%2541ss@1.2.3.4:8388#demo", "p%41ss")]
[Arguments("ss://2022-blake3-aes-128-gcm:p%25ss@1.2.3.4:8388#demo", "p%ss")]
[Arguments("ss://2022-blake3-aes-128-gcm:a%3Ab%2Fc%2B%3D@1.2.3.4:8388#demo", "a:b/c+=")]
public async Task ResolveConfig_PlainUserInfo_ShouldDecodePasswordOnce(string shareUri, string password)
{
var resolved = FmtHandler.ResolveConfig(shareUri, out var msg);
await resolved.Should().NotBeNull().Because($"uri: {shareUri}, msg: {msg}");
await resolved!.ConfigType.Should().BeEqualTo(EConfigType.Shadowsocks);
await resolved.GetProtocolExtra().SsMethod.Should().BeEqualTo("2022-blake3-aes-128-gcm");
await resolved.Password.Should().BeEqualTo(password);
}
}
@@ -165,7 +165,7 @@ public class ShadowsocksFmt : BaseFmt
return null;
}
item.SetProtocolExtra(item.GetProtocolExtra() with { SsMethod = userInfoParts.First() });
item.Password = Utils.UrlDecode(userInfoParts.Last());
item.Password = userInfoParts.Last();
}
else
{