diff --git a/src/app/api/admin/create-user/route.ts b/src/app/api/admin/create-user/route.ts index 6b952b2..74661d6 100644 --- a/src/app/api/admin/create-user/route.ts +++ b/src/app/api/admin/create-user/route.ts @@ -5,7 +5,7 @@ import { verifySharedJwt } from '@/lib/jwt' import { isLldapAdmin } from '@/lib/ldap' import { sendSetupLinkEmail } from '@/lib/email' import { signSetupToken } from '@/lib/setup-token' -import { execLldapWrite, lldapChangePassword, esc, getAdminPassword } from '@/lib/lldap-db' +import { execLldap, lldapChangePassword, esc, getAdminPassword } from '@/lib/lldap-db' import bcrypt from 'bcryptjs' const INTERNAL_KEY = 'oa-internal-key-tlyq-2026' @@ -75,7 +75,7 @@ export async function POST(request: Request) { const userUuid = crypto.randomUUID() // 1. docker exec lldap 插入用户(LLDAP DELETE 模式不可并发写) - execLldapWrite(`INSERT OR IGNORE INTO users (user_id, email, display_name, creation_date, uuid, lowercase_email, modified_date, password_modified_date) VALUES ('${safeUser}', '${esc(lldapEmail)}', '${safeName}', '${now}', '${userUuid}', LOWER('${esc(lldapEmail)}'), '${now}', '${now}')`) + execLldap(`INSERT OR IGNORE INTO users (user_id, email, display_name, creation_date, uuid, lowercase_email, modified_date, password_modified_date) VALUES ('${safeUser}', '${esc(lldapEmail)}', '${safeName}', '${now}', '${userUuid}', LOWER('${esc(lldapEmail)}'), '${now}', '${now}')`) // 2. bcryptjs 直写 LLDAP 密码(替代 docker exec lldap_set_password) lldapChangePassword(username, password) diff --git a/src/app/api/admin/users/route.ts b/src/app/api/admin/users/route.ts index 9e86d3f..f4fe37b 100644 --- a/src/app/api/admin/users/route.ts +++ b/src/app/api/admin/users/route.ts @@ -3,7 +3,7 @@ import { cookies } from 'next/headers' import { execFileSync } from 'child_process' import { verifySharedJwt } from '@/lib/jwt' import { isLldapAdmin } from '@/lib/ldap' -import { queryLldap, execLldapWrite, esc } from '@/lib/lldap-db' +import { queryLldap, execLldap, esc } from '@/lib/lldap-db' function checkAdmin() { return async () => { @@ -54,7 +54,7 @@ export async function DELETE(request: Request) { } const safeUser = esc(username) - execLldapWrite(`DELETE FROM users WHERE user_id='${safeUser}'`) + execLldap(`DELETE FROM users WHERE user_id='${safeUser}'`) const results: Record = {} for (const [site, dbPath] of Object.entries({ @@ -100,7 +100,7 @@ export async function PATCH(request: Request) { lldapSets.push(`modified_date = '${nowStr()}'`) siteSets.push(`updated_at = datetime('now', '+8 hours')`) - execLldapWrite(`UPDATE users SET ${lldapSets.join(', ')} WHERE user_id = '${safeUser}'`) + execLldap(`UPDATE users SET ${lldapSets.join(', ')} WHERE user_id = '${safeUser}'`) const siteSql = `UPDATE users SET ${siteSets.join(', ')} WHERE username = '${safeUser}'` for (const dbPath of [process.env.ASSETS_DB_PATH || '/data/other-sites/assets/assets.db', process.env.ISSUE_DB_PATH || '/data/other-sites/issue/issue.db']) { diff --git a/src/app/api/auth/me/route.ts b/src/app/api/auth/me/route.ts index 7fd92d4..42809ee 100644 --- a/src/app/api/auth/me/route.ts +++ b/src/app/api/auth/me/route.ts @@ -2,7 +2,7 @@ import { NextResponse } from 'next/server' import { cookies } from 'next/headers' import { verifySharedJwt } from '@/lib/jwt' import { isLldapAdmin } from '@/lib/ldap' -import { queryLldap, execLldapWrite, esc } from '@/lib/lldap-db' +import { queryLldap, execLldap, esc } from '@/lib/lldap-db' import { execFileSync } from 'child_process' async function getLldapInfo(username: string): Promise<{ email: string; displayName: string }> { @@ -56,7 +56,7 @@ export async function PUT(request: Request) { const now = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}:${String(d.getSeconds()).padStart(2,'0')}` // docker exec lldap 更新邮箱(LLDAP DELETE 模式不可并发写) - execLldapWrite(`UPDATE users SET email = '${safeEmail}', lowercase_email = LOWER('${safeEmail}'), modified_date = '${now}' WHERE user_id = '${safeUser}'`) + execLldap(`UPDATE users SET email = '${safeEmail}', lowercase_email = LOWER('${safeEmail}'), modified_date = '${now}' WHERE user_id = '${safeUser}'`) // 同步更新 assets / issue 本地用户表 const assetsDb = process.env.ASSETS_DB_PATH || '/data/other-sites/assets/assets.db' diff --git a/src/lib/lldap-db.ts b/src/lib/lldap-db.ts index 27defac..7ca27d8 100644 --- a/src/lib/lldap-db.ts +++ b/src/lib/lldap-db.ts @@ -1,5 +1,6 @@ -// lib/lldap-db.ts — LLDAP 操作(读直连 SQLite / 写走 docker exec) +// lib/lldap-db.ts — LLDAP 操作(读直连 SQLite / 写走 docker exec stdin) // LLDAP 使用 DELETE journal mode,不可并发写 → 写操作必须通过 docker exec 在 LLDAP 容器内执行 +// SQL 通过 stdin 传入 sqlite3,不经过 shell 解析 → 无命令注入风险 import { execFileSync } from 'child_process' import bcrypt from 'bcryptjs' @@ -10,14 +11,11 @@ export function queryLldap(sql: string): string { return execFileSync('sqlite3', [LLDAP_DB, sql], { timeout: 5000, encoding: 'utf8' }).trim() } -/** docker exec lldap 执行 LLDAP 容器内操作(密码/写入) */ -function dockerExecLldap(cmd: string, timeout = 5000): string { - return execFileSync('docker', ['exec', 'lldap', 'sh', '-c', cmd], { timeout, encoding: 'utf8' }).trim() -} - -/** docker exec lldap sqlite3 写操作 */ -export function execLldapWrite(sql: string): void { - dockerExecLldap(`sqlite3 /data/users.db "${sql.replace(/"/g, '\\"')}"`, 5000) +/** docker exec -i lldap sqlite3,通过 stdin 传入 SQL(无 shell 解析,无命令注入风险) */ +export function execLldap(sql: string, timeout = 5000): string { + return execFileSync('docker', ['exec', '-i', 'lldap', 'sqlite3', '/data/users.db'], { + input: sql, timeout, encoding: 'utf8', + }).trim() } /** 安全的 SQL 字符串转义(SQLite 标准:'' → 单引号) */ @@ -25,12 +23,12 @@ export function esc(val: string): string { return val.replace(/'/g, "''") } -/** 修改 LLDAP 用户密码(docker exec lldap + lldap_set_password) */ +/** 修改 LLDAP 用户密码 */ export function lldapChangePassword(username: string, newPassword: string): void { const hash = bcrypt.hashSync(newPassword, 12) const safeUser = esc(username) const now = new Date().toISOString().replace('T', ' ').slice(0, 19) - dockerExecLldap(`sqlite3 /data/users.db "UPDATE users SET password_hash = '${hash}', password_modified_date = '${now}', modified_date = '${now}' WHERE user_id = '${safeUser}'"`, 5000) + execLldap(`UPDATE users SET password_hash = '${hash}', password_modified_date = '${now}', modified_date = '${now}' WHERE user_id = '${safeUser}';`) } /** 获取 admin 密码 */