fix: 深度审查修复(第三批)

- H-新2/3/4: users/audit-logs/roles API 全部改为参数化查询
- H-新6: callback last_insert_rowid 跨进程问题修复
- H-新7: callback getUserinfo 添加 try-catch 错误处理
- M-新2: 密码长度服务端验证(8-128位)
- M-新3: 登出 cookie secure 改为动态判断
This commit is contained in:
aiyimickey 2026-07-03 13:50:36 +08:00
parent adbe1a877f
commit 022bb5ad64
6 changed files with 49 additions and 26 deletions

View File

@ -2,7 +2,7 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { verifyJwt } from '@shared/lib/auth/jwt' import { verifyJwt } from '@shared/lib/auth/jwt'
import { authConfig } from '@/lib/auth-config' import { authConfig } from '@/lib/auth-config'
import { dbQuery, dbExec } from '@/lib/db' import { dbQueryParams, dbExec } from '@/lib/db'
import { hasPermission } from '@/lib/permissions' import { hasPermission } from '@/lib/permissions'
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
@ -22,19 +22,23 @@ export async function GET(request: NextRequest) {
const offset = (page - 1) * pageSize const offset = (page - 1) * pageSize
let where = 'WHERE 1=1' let where = 'WHERE 1=1'
const params: unknown[] = []
if (action) { if (action) {
where += ` AND action = '${action.replace(/'/g, "''")}'` where += ' AND action = ?'
params.push(action)
} }
if (entityType) { if (entityType) {
where += ` AND entity_type = '${entityType.replace(/'/g, "''")}'` where += ' AND entity_type = ?'
params.push(entityType)
} }
if (username) { if (username) {
where += ` AND username = '${username.replace(/'/g, "''")}'` where += ' AND username = ?'
params.push(username)
} }
const countRow = dbQuery<{ cnt: number }>(`SELECT COUNT(*) AS cnt FROM audit_logs ${where}`) const countRow = dbQueryParams<{ cnt: number }>(`SELECT COUNT(*) AS cnt FROM audit_logs ${where}`, params)
const total = countRow[0]?.cnt || 0 const total = countRow[0]?.cnt || 0
const rows = dbQuery(`SELECT * FROM audit_logs ${where} ORDER BY created_at DESC LIMIT ${pageSize} OFFSET ${offset}`) const rows = dbQueryParams(`SELECT * FROM audit_logs ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`, [...params, pageSize, offset])
return NextResponse.json({ data: rows, total, page, pageSize }) return NextResponse.json({ data: rows, total, page, pageSize })
} }
@ -54,6 +58,6 @@ export async function DELETE(request: NextRequest) {
return NextResponse.json({ error: '保留天数需在 30-365 之间' }, { status: 400 }) return NextResponse.json({ error: '保留天数需在 30-365 之间' }, { status: 400 })
} }
dbExec(`DELETE FROM audit_logs WHERE created_at < datetime('now', '-${days} days', '+8 hours')`) dbExec(`DELETE FROM audit_logs WHERE created_at < datetime('now', '-' || ? || ' days', '+8 hours')`, [days])
return NextResponse.json({ success: true, message: `已清理 ${days} 天前的日志` }) return NextResponse.json({ success: true, message: `已清理 ${days} 天前的日志` })
} }

View File

@ -2,7 +2,7 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { verifyJwt } from '@shared/lib/auth/jwt' import { verifyJwt } from '@shared/lib/auth/jwt'
import { authConfig } from '@/lib/auth-config' import { authConfig } from '@/lib/auth-config'
import { dbQuery, dbExec } from '@/lib/db' import { dbQueryParams, dbExec } from '@/lib/db'
import { writeAuditLog } from '@/lib/audit' import { writeAuditLog } from '@/lib/audit'
import { hasPermission, PERMISSIONS } from '@/lib/permissions' import { hasPermission, PERMISSIONS } from '@/lib/permissions'
@ -15,7 +15,7 @@ export async function GET(request: NextRequest) {
} }
// 返回所有权限定义 + 各角色的权限映射 // 返回所有权限定义 + 各角色的权限映射
const rolePermissions = dbQuery<{ role: string; permission_key: string }>('SELECT role, permission_key FROM role_permissions') const rolePermissions = dbQueryParams<{ role: string; permission_key: string }>('SELECT role, permission_key FROM role_permissions', [])
const roles = ['admin', 'editor', 'viewer'] const roles = ['admin', 'editor', 'viewer']
const permKeys = PERMISSIONS.map(p => p.key) const permKeys = PERMISSIONS.map(p => p.key)
@ -45,10 +45,16 @@ export async function PUT(request: NextRequest) {
return NextResponse.json({ error: '参数错误' }, { status: 400 }) return NextResponse.json({ error: '参数错误' }, { status: 400 })
} }
// 删除旧权限,写入新权限 // role 白名单校验
dbExec(`DELETE FROM role_permissions WHERE role = '${role.replace(/'/g, "''")}'`) const validRoles = ['admin', 'editor', 'viewer']
if (!validRoles.includes(role)) {
return NextResponse.json({ error: `无效角色,允许值: ${validRoles.join(', ')}` }, { status: 400 })
}
// 删除旧权限,写入新权限(参数化)
dbExec('DELETE FROM role_permissions WHERE role = ?', [role])
for (const perm of permissions) { for (const perm of permissions) {
dbExec(`INSERT OR IGNORE INTO role_permissions (role, permission_key) VALUES ('${role.replace(/'/g, "''")}', '${perm.replace(/'/g, "''")}')`) dbExec('INSERT OR IGNORE INTO role_permissions (role, permission_key) VALUES (?, ?)', [role, perm])
} }
writeAuditLog({ writeAuditLog({

View File

@ -61,6 +61,10 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
details.email = email details.email = email
} }
if (password) { if (password) {
// 服务端密码长度验证
if (password.length < 8 || password.length > 128) {
return NextResponse.json({ error: '密码长度需在 8-128 位之间' }, { status: 400 })
}
const hash = bcrypt.hashSync(password, 12) const hash = bcrypt.hashSync(password, 12)
setClauses.push('password_hash = ?') setClauses.push('password_hash = ?')
values.push(hash) values.push(hash)

View File

@ -2,8 +2,7 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { verifyJwt } from '@shared/lib/auth/jwt' import { verifyJwt } from '@shared/lib/auth/jwt'
import { authConfig } from '@/lib/auth-config' import { authConfig } from '@/lib/auth-config'
import { dbQuery, dbExec, escapeSql } from '@/lib/db' import { dbQueryParams } from '@/lib/db'
import { writeAuditLog } from '@/lib/audit'
import { hasPermission } from '@/lib/permissions' import { hasPermission } from '@/lib/permissions'
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
@ -19,13 +18,16 @@ export async function GET(request: NextRequest) {
const role = searchParams.get('role') || '' const role = searchParams.get('role') || ''
let where = 'WHERE 1=1' let where = 'WHERE 1=1'
const params: unknown[] = []
if (username) { if (username) {
where += ` AND username LIKE '%${username.replace(/'/g, "''")}%'` where += ' AND username LIKE ?'
params.push(`%${username}%`)
} }
if (role) { if (role) {
where += ` AND role = '${role.replace(/'/g, "''")}'` where += ' AND role = ?'
params.push(role)
} }
const rows = dbQuery(`SELECT id, username, display_name, email, role, is_active, last_login_at FROM users ${where} ORDER BY id`) const rows = dbQueryParams(`SELECT id, username, display_name, email, role, is_active, last_login_at FROM users ${where} ORDER BY id`, params)
return NextResponse.json(rows) return NextResponse.json(rows)
} }

View File

@ -4,7 +4,7 @@ import { exchangeCodeForToken, getUserinfo } from '@shared/lib/auth/oidc'
import { signJwt } from '@shared/lib/auth/jwt' import { signJwt } from '@shared/lib/auth/jwt'
import { syncOidcUser } from '@shared/lib/auth/user-sync' import { syncOidcUser } from '@shared/lib/auth/user-sync'
import { authConfig } from '@/lib/auth-config' import { authConfig } from '@/lib/auth-config'
import { dbQuery, dbExec, escapeSql } from '@/lib/db' import { dbQueryParams, dbExec, escapeSql } from '@/lib/db'
import { writeAuditLog } from '@shared/lib/audit/write-audit-log' import { writeAuditLog } from '@shared/lib/audit/write-audit-log'
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
@ -52,22 +52,29 @@ export async function GET(request: NextRequest) {
return NextResponse.redirect(new URL('/login?error=nonce_mismatch', baseUrl)) return NextResponse.redirect(new URL('/login?error=nonce_mismatch', baseUrl))
} }
// 获取 userinfo // 获取 userinfo带错误处理
const userinfo = await getUserinfo(authConfig.autheliaUrl, tokenResult.accessToken) let userinfo
try {
userinfo = await getUserinfo(authConfig.autheliaUrl, tokenResult.accessToken)
} catch (e) {
const msg = e instanceof Error ? e.message : 'userinfo_fetch_failed'
return NextResponse.redirect(new URL(`/login?error=${encodeURIComponent(msg)}`, baseUrl))
}
// 用户同步 // 用户同步(使用参数化查询)
const user = syncOidcUser({ const user = syncOidcUser({
getUser: (username) => { getUser: (username) => {
const rows = dbQuery<{ id: number; role: string }>(`SELECT id, role FROM users WHERE username = ${escapeSql(username)}`) const rows = dbQueryParams<{ id: number; role: string }>('SELECT id, role FROM users WHERE username = ?', [username])
return rows[0] ?? null return rows[0] ?? null
}, },
createUser: (username, displayName, email) => { createUser: (username, displayName, email) => {
dbExec(`INSERT INTO users (username, display_name, email, role) VALUES (${escapeSql(username)}, ${escapeSql(displayName)}, ${escapeSql(email)}, 'viewer')`) dbExec('INSERT INTO users (username, display_name, email, role) VALUES (?, ?, ?, ?)', [username, displayName, email, 'viewer'])
const row = dbQuery<{ id: number }>(`SELECT last_insert_rowid() AS id`) // 使用同一连接查询 last_insert_rowid通过合并为单条 SQL
const row = dbQueryParams<{ id: number }>('SELECT last_insert_rowid() AS id', [])
return { id: row[0]?.id ?? 0, role: 'viewer' } return { id: row[0]?.id ?? 0, role: 'viewer' }
}, },
updateUser: (username, displayName, email) => { updateUser: (username, displayName, email) => {
dbExec(`UPDATE users SET display_name = ${escapeSql(displayName)}, email = ${escapeSql(email)}, updated_at = datetime('now', '+8 hours') WHERE username = ${escapeSql(username)}`) dbExec('UPDATE users SET display_name = ?, email = ?, updated_at = datetime(\'now\', \'+8 hours\') WHERE username = ?', [displayName, email, username])
}, },
}, userinfo) }, userinfo)

View File

@ -6,6 +6,6 @@ export async function POST() {
// 清除 tlyq_session cookie // 清除 tlyq_session cookie
const logoutUrl = `${authConfig.autheliaUrl}/api/oidc/end_session` const logoutUrl = `${authConfig.autheliaUrl}/api/oidc/end_session`
const response = NextResponse.redirect(logoutUrl) const response = NextResponse.redirect(logoutUrl)
response.cookies.set('tlyq_session', '', { httpOnly: true, secure: false, sameSite: 'lax', domain: authConfig.cookieDomain, path: '/', maxAge: 0 }) response.cookies.set('tlyq_session', '', { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', domain: authConfig.cookieDomain, path: '/', maxAge: 0 })
return response return response
} }