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 }