fix: P1/P2 优化 - OAuth验证 + API响应 + 缓存击穿 + Webhook关闭
P1 - OAuth auth_url origin 验证: - 添加 validateOAuthUrl() 函数验证 OAuth URL origin - 仅允许同源或可信 OAuth 提供商 - LoginPage 和 ProfileSecurityPage 调用前验证 P2 - API 响应运行时类型验证: - 添加 isApiResponse() 运行时验证函数 - parseJsonResponse 验证响应结构完整性 P2 - 缓存击穿防护 (singleflight): - AuthMiddleware.isJTIBlacklisted 使用 singleflight.Group - 防止 L1 miss 时并发请求同时打 L2 P2 - Webhook 服务优雅关闭: - WebhookService 添加 Shutdown() 方法 - 服务器关闭时等待 worker 完成 - main.go 集成 shutdown 调用
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { afterAll, describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
buildOAuthCallbackReturnTo,
|
||||
parseOAuthCallbackHash,
|
||||
sanitizeAuthRedirect,
|
||||
validateOAuthUrl,
|
||||
} from './oauth'
|
||||
|
||||
describe('oauth auth helpers', () => {
|
||||
@@ -26,4 +27,40 @@ describe('oauth auth helpers', () => {
|
||||
message: '',
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateOAuthUrl', () => {
|
||||
const originalOrigin = window.location.origin
|
||||
|
||||
afterAll(() => {
|
||||
// 恢复原始 origin
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { origin: originalOrigin },
|
||||
writable: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('allows same-origin URLs', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { origin: 'http://localhost:3000' },
|
||||
writable: true,
|
||||
})
|
||||
expect(validateOAuthUrl('http://localhost:3000/api/v1/auth/oauth/authorize')).toBe(true)
|
||||
})
|
||||
|
||||
it('allows trusted OAuth provider origins', () => {
|
||||
expect(validateOAuthUrl('https://github.com/login/oauth/authorize')).toBe(true)
|
||||
expect(validateOAuthUrl('https://google.com/oauth/authorize')).toBe(true)
|
||||
expect(validateOAuthUrl('https://facebook.com/v1.0/oauth/authorize')).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects untrusted origins', () => {
|
||||
expect(validateOAuthUrl('https://evil.example.com/oauth/authorize')).toBe(false)
|
||||
expect(validateOAuthUrl('https://attacker.com/callback')).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects invalid URLs', () => {
|
||||
expect(validateOAuthUrl('not-a-url')).toBe(false)
|
||||
expect(validateOAuthUrl('')).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,6 +6,46 @@ export function sanitizeAuthRedirect(target: string | null | undefined, fallback
|
||||
return value
|
||||
}
|
||||
|
||||
// 可信的 OAuth 提供商 origin 白名单
|
||||
const TRUSTED_OAUTH_ORIGINS = new Set([
|
||||
// 社交登录提供商
|
||||
'https://github.com',
|
||||
'https://google.com',
|
||||
'https://facebook.com',
|
||||
'https://twitter.com',
|
||||
'https://apple.com',
|
||||
'https://weixin.qq.com',
|
||||
'https://qq.com',
|
||||
'https://alipay.com',
|
||||
'https://douyin.com',
|
||||
])
|
||||
|
||||
/**
|
||||
* 验证 OAuth 授权 URL 的 origin 是否可信
|
||||
* 防止开放重定向攻击
|
||||
*/
|
||||
export function validateOAuthUrl(authUrl: string): boolean {
|
||||
try {
|
||||
const url = new URL(authUrl)
|
||||
|
||||
// 允许同源(当前应用自身作为 OAuth 提供者的情况)
|
||||
if (url.origin === window.location.origin) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查是否在可信 origin 白名单中
|
||||
if (TRUSTED_OAUTH_ORIGINS.has(url.origin)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 拒绝所有其他 origin
|
||||
return false
|
||||
} catch {
|
||||
// 无效的 URL 格式
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function buildOAuthCallbackReturnTo(redirectPath: string): string {
|
||||
const callbackUrl = new URL('/login/oauth/callback', window.location.origin)
|
||||
if (redirectPath && redirectPath !== '/dashboard') {
|
||||
|
||||
Reference in New Issue
Block a user