fix(weixin): trust international WeChat hosts

This commit is contained in:
xmanrui 2026-08-26 18:46:55 +08:00
parent 50d55090b9
commit dc66366ac0
6 changed files with 14 additions and 5 deletions

View file

@ -8330,7 +8330,7 @@ function safeVerificationUrl(value) {
try {
const url = new URL(value);
const host = url.hostname.toLowerCase();
return url.protocol === "https:" && (host === "weixin.qq.com" || host.endsWith(".weixin.qq.com")) ? url.toString() : void 0;
return url.protocol === "https:" && (host === "weixin.qq.com" || host.endsWith(".weixin.qq.com") || host === "wechat.com" || host.endsWith(".wechat.com")) ? url.toString() : void 0;
} catch {
return void 0;
}

File diff suppressed because one or more lines are too long

View file

@ -74,7 +74,8 @@ export function safeVerificationUrl(value) {
const url = new URL(value);
const host = url.hostname.toLowerCase();
return url.protocol === 'https:'
&& (host === 'weixin.qq.com' || host.endsWith('.weixin.qq.com'))
&& (host === 'weixin.qq.com' || host.endsWith('.weixin.qq.com')
|| host === 'wechat.com' || host.endsWith('.wechat.com'))
? url.toString()
: undefined;
} catch {

View file

@ -240,7 +240,8 @@ export function extractWeixinFiles(message, { fetchImpl = fetch } = {}) {
function isWeixinHost(hostname) {
const normalized = hostname.toLowerCase().replace(/\.$/, '');
return normalized === 'weixin.qq.com' || normalized.endsWith('.weixin.qq.com');
return normalized === 'weixin.qq.com' || normalized.endsWith('.weixin.qq.com')
|| normalized === 'wechat.com' || normalized.endsWith('.wechat.com');
}
export function normalizeWeixinApiBaseUrl(value) {

View file

@ -652,7 +652,12 @@ test('Weixin URL, inbound text, and reply chunk helpers enforce their narrow for
normalizeWeixinApiBaseUrl('https://ilinkai.weixin.qq.com/path'),
'https://ilinkai.weixin.qq.com/path/',
);
assert.equal(
normalizeWeixinApiBaseUrl('https://ilinkai.wechat.com/path'),
'https://ilinkai.wechat.com/path/',
);
assert.throws(() => normalizeWeixinApiBaseUrl('https://ilinkai.weixin.qq.com:444/'));
assert.throws(() => normalizeWeixinApiBaseUrl('https://ilinkai.wechat.com.attacker.test/'));
assert.equal(extractWeixinText({ item_list: [{ type: 1, text_item: { text: ' 你好 ' } }] }), '你好');
assert.equal(extractWeixinText({ item_list: [{ type: 3, voice_item: { text: '语音转写' } }] }), '语音转写');
assert.deepEqual(splitWeixinText('abcdefgh', 5), ['abcde', 'fgh']);

View file

@ -28,11 +28,13 @@ test('client normalizes the exact redacted Host account view', () => {
assert.doesNotMatch(JSON.stringify(snapshot), /host-secret|token/);
});
test('client accepts only image data URLs and Tencent Weixin HTTPS links', () => {
test('client accepts only image data URLs and Tencent Weixin or WeChat HTTPS links', () => {
assert.match(safeQrSource('data:image/png;base64,AAAA'), /^data:image/);
assert.equal(safeQrSource('javascript:alert(1)'), undefined);
assert.equal(safeVerificationUrl('https://liteapp.weixin.qq.com/q/test'), 'https://liteapp.weixin.qq.com/q/test');
assert.equal(safeVerificationUrl('https://liteapp.wechat.com/q/test'), 'https://liteapp.wechat.com/q/test');
assert.equal(safeVerificationUrl('https://attacker.test/q/test'), undefined);
assert.equal(safeVerificationUrl('https://liteapp.wechat.com.attacker.test/q/test'), undefined);
});
test('client preserves verification-required provisioning without accepting unknown states', () => {