From 0b83ef7f880dc194077bf3a20d9c5b9e89a5ca1c Mon Sep 17 00:00:00 2001 From: WuYongJie Date: Wed, 15 Jul 2026 21:59:18 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E9=92=89=E9=92=89):=20=E5=85=BC=E5=AE=B9?= =?UTF-8?q?=E6=A1=8C=E9=9D=A2=E7=AB=AF=E5=85=8D=E7=99=BB=E6=9D=A5=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../middleware/__tests__/originGuard.test.ts | 13 +++++++++++++ backend/src/utils/__tests__/corsPolicy.test.ts | 18 ++++++++++++++++++ backend/src/utils/corsPolicy.ts | 7 ++++++- 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/backend/src/middleware/__tests__/originGuard.test.ts b/backend/src/middleware/__tests__/originGuard.test.ts index e13075d..dae6c0e 100644 --- a/backend/src/middleware/__tests__/originGuard.test.ts +++ b/backend/src/middleware/__tests__/originGuard.test.ts @@ -79,3 +79,16 @@ test('通知中心来源只可进入一次性票据回调', () => { path: '/api/equipment/1' }).status, 403) }) + +test('钉钉 WebView 的 null Origin 只可进入一次性票据回调', () => { + assert.equal(runGuard({ + method: 'POST', + origin: 'null', + path: '/api/dingtalk/broker-login' + }).nextCalls, 1) + assert.equal(runGuard({ + method: 'POST', + origin: 'null', + path: '/api/equipment' + }).status, 403) +}) diff --git a/backend/src/utils/__tests__/corsPolicy.test.ts b/backend/src/utils/__tests__/corsPolicy.test.ts index 232a097..970ea98 100644 --- a/backend/src/utils/__tests__/corsPolicy.test.ts +++ b/backend/src/utils/__tests__/corsPolicy.test.ts @@ -45,6 +45,24 @@ test('跨源写操作默认拒绝,只单独放行通知中心票据回调', () requestOrigin: 'https://asset.example.com', path: '/api/dingtalk/broker-login' }, env), true) + assert.equal(isUnsafeRequestOriginAllowed({ + method: 'POST', + origin: 'null', + requestOrigin: 'https://asset.example.com', + path: '/api/dingtalk/broker-login' + }, env), true) + assert.equal(isUnsafeRequestOriginAllowed({ + method: 'POST', + origin: 'null', + requestOrigin: 'https://asset.example.com', + path: '/api/equipment' + }, env), false) + assert.equal(isUnsafeRequestOriginAllowed({ + method: 'POST', + origin: 'https://evil.example.com', + requestOrigin: 'https://asset.example.com', + path: '/api/dingtalk/broker-login' + }, env), false) assert.equal(isUnsafeRequestOriginAllowed({ method: 'DELETE', origin: 'https://evil.example.com', diff --git a/backend/src/utils/corsPolicy.ts b/backend/src/utils/corsPolicy.ts index f91d611..2694303 100644 --- a/backend/src/utils/corsPolicy.ts +++ b/backend/src/utils/corsPolicy.ts @@ -39,11 +39,16 @@ export const isUnsafeRequestOriginAllowed = (input: { if (safeMethods.has(String(input.method || '').toUpperCase())) return true if (!input.origin) return true + const isBrokerCallback = input.path === '/api/dingtalk/broker-login' + // 钉钉桌面 WebView 的跨域 form POST 会把 Origin 固定为 null。 + // 仅一次性票据回调接受该值;票据仍需通过项目签名兑换、受众校验、60秒过期和原子单次消费。 + if (isBrokerCallback && input.origin.trim().toLowerCase() === 'null') return true + const origin = parseOrigin(input.origin) const requestOrigin = parseOrigin(input.requestOrigin) if (!origin || !requestOrigin) return false if (origin === requestOrigin || getAllowedCorsOrigins(env).has(origin)) return true const notificationCenterOrigin = parseOrigin(String(env.YYD_NOTIFICATION_CENTER_URL || '').trim()) - return input.path === '/api/dingtalk/broker-login' && origin === notificationCenterOrigin + return isBrokerCallback && origin === notificationCenterOrigin }