mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-16 15:43:13 +08:00
审计发现修复: 高危——审计日志请求体脱敏不全(audit_logs 沦为明文凭证聚合点): - 键名归一化比对(小写+去分隔符),覆盖 privateKey/apiv3key 等无分隔符与 camelCase 写法 - 程序化并入 SensitiveCredentialKeys 与 providerSensitiveConfigFields 两份权威敏感表,防清单漂移 - 补齐 proxy_key(内嵌代理密码)、custom_key(自设 API Key 明文)精确键 - Codex session 导入路由 body 整体由粘贴的 auth JSON 构成,键级脱敏无法覆盖,整体不入库 - 新增守卫测试:两份权威表的每个键必须被审计脱敏命中;provider_key 等渠道标识保留以便追责 中危——step-up 前端体验: - 备份下载改同页 anchor 导航(预签名 URL 后端强制 attachment disposition), 避免 step-up 弹窗 await 耗尽瞬态激活后 window.open 被浏览器拦截 - useStepUp.run 用户取消时抛 StepUpCancelledError sentinel, 三个调用点静默处理,不再把取消误报为红色错误 toast 低危: - 修正审计中间件挂载位置的陈旧注释(实际挂在认证之后) - 审计 body 捕获改 LimitReader 按 256KB 上限截断读取,超出部分拼接回填, 避免大体积导入请求被完整复制进内存两次 - TOTP step-up 弹窗验证成功后即时清空验证码输入
128 lines
3.5 KiB
TypeScript
128 lines
3.5 KiB
TypeScript
/**
|
|
* Step-up (sudo) 2FA composable.
|
|
*
|
|
* Wraps a sensitive admin action so that when the backend responds with a
|
|
* STEP_UP_REQUIRED error, the caller can prompt for a TOTP code, obtain a
|
|
* short-lived grant, and transparently retry the original action.
|
|
*
|
|
* Usage in a view:
|
|
* const stepUp = useStepUp()
|
|
* async function exportData() {
|
|
* await stepUp.run(() => adminAPI.accounts.exportData(...))
|
|
* }
|
|
* // template: <TotpStepUpDialog :controller="stepUp" />
|
|
*/
|
|
import { ref } from 'vue'
|
|
|
|
/** Error codes the backend uses to signal step-up state. */
|
|
const STEP_UP_REQUIRED = 'STEP_UP_REQUIRED'
|
|
const STEP_UP_TOTP_NOT_ENABLED = 'STEP_UP_TOTP_NOT_ENABLED'
|
|
const STEP_UP_ADMIN_API_KEY_FORBIDDEN = 'STEP_UP_ADMIN_API_KEY_FORBIDDEN'
|
|
|
|
/**
|
|
* Thrown by run() when the user dismisses the TOTP dialog.
|
|
* Callers should treat it as a silent no-op, not an error to toast.
|
|
*/
|
|
export class StepUpCancelledError extends Error {
|
|
readonly code = 'STEP_UP_CANCELLED'
|
|
constructor() {
|
|
super('step-up verification cancelled by user')
|
|
this.name = 'StepUpCancelledError'
|
|
}
|
|
}
|
|
|
|
export function isStepUpCancelled(err: unknown): boolean {
|
|
return err instanceof StepUpCancelledError
|
|
}
|
|
|
|
interface ApiError {
|
|
status?: number
|
|
code?: string | number
|
|
reason?: string
|
|
message?: string
|
|
}
|
|
|
|
/** Extract the semantic error marker from either envelope shape (code or reason). */
|
|
function markerOf(err: unknown): string {
|
|
const e = (err ?? {}) as ApiError
|
|
const candidates = [e.code, e.reason].map((v) => (typeof v === 'string' ? v : ''))
|
|
return candidates.find((v) => v.startsWith('STEP_UP')) || ''
|
|
}
|
|
|
|
export function isStepUpRequired(err: unknown): boolean {
|
|
return markerOf(err) === STEP_UP_REQUIRED
|
|
}
|
|
|
|
export function isStepUpBlocked(err: unknown): boolean {
|
|
const m = markerOf(err)
|
|
return m === STEP_UP_TOTP_NOT_ENABLED || m === STEP_UP_ADMIN_API_KEY_FORBIDDEN
|
|
}
|
|
|
|
export function stepUpBlockReason(err: unknown): string {
|
|
return markerOf(err)
|
|
}
|
|
|
|
export type StepUpController = ReturnType<typeof useStepUp>
|
|
|
|
export function useStepUp() {
|
|
const visible = ref(false)
|
|
const blockedReason = ref<string>('')
|
|
let resolver: ((ok: boolean) => void) | null = null
|
|
|
|
/** Open the TOTP dialog and resolve true once a grant is obtained. */
|
|
function prompt(): Promise<boolean> {
|
|
visible.value = true
|
|
return new Promise<boolean>((resolve) => {
|
|
resolver = resolve
|
|
})
|
|
}
|
|
|
|
function onVerified() {
|
|
visible.value = false
|
|
resolver?.(true)
|
|
resolver = null
|
|
}
|
|
|
|
function onCancel() {
|
|
visible.value = false
|
|
resolver?.(false)
|
|
resolver = null
|
|
}
|
|
|
|
/**
|
|
* Run a sensitive action. On STEP_UP_REQUIRED, prompt for a TOTP code and
|
|
* retry once. STEP_UP_TOTP_NOT_ENABLED / admin-api-key errors are surfaced
|
|
* to the caller (they cannot be resolved by entering a code). If the user
|
|
* cancels the prompt, a StepUpCancelledError is thrown so callers can
|
|
* distinguish "user changed their mind" from real failures.
|
|
*/
|
|
async function run<T>(action: () => Promise<T>): Promise<T> {
|
|
try {
|
|
return await action()
|
|
} catch (err) {
|
|
if (isStepUpBlocked(err)) {
|
|
blockedReason.value = markerOf(err)
|
|
throw err
|
|
}
|
|
if (!isStepUpRequired(err)) {
|
|
throw err
|
|
}
|
|
const ok = await prompt()
|
|
if (!ok) {
|
|
throw new StepUpCancelledError()
|
|
}
|
|
// Retry once now that the session holds a step-up grant.
|
|
return await action()
|
|
}
|
|
}
|
|
|
|
return {
|
|
visible,
|
|
blockedReason,
|
|
prompt,
|
|
onVerified,
|
|
onCancel,
|
|
run
|
|
}
|
|
}
|