fix: command injection in lldap-db.ts — use execFileSync stdin instead of sh -c

This commit is contained in:
aiyimickey 2026-07-07 18:10:17 +08:00
parent ed43d7b8f7
commit 8886e12b15
4 changed files with 16 additions and 18 deletions

View File

@ -5,7 +5,7 @@ import { verifySharedJwt } from '@/lib/jwt'
import { isLldapAdmin } from '@/lib/ldap' import { isLldapAdmin } from '@/lib/ldap'
import { sendSetupLinkEmail } from '@/lib/email' import { sendSetupLinkEmail } from '@/lib/email'
import { signSetupToken } from '@/lib/setup-token' 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' import bcrypt from 'bcryptjs'
const INTERNAL_KEY = 'oa-internal-key-tlyq-2026' const INTERNAL_KEY = 'oa-internal-key-tlyq-2026'
@ -75,7 +75,7 @@ export async function POST(request: Request) {
const userUuid = crypto.randomUUID() const userUuid = crypto.randomUUID()
// 1. docker exec lldap 插入用户LLDAP DELETE 模式不可并发写) // 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 // 2. bcryptjs 直写 LLDAP 密码(替代 docker exec lldap_set_password
lldapChangePassword(username, password) lldapChangePassword(username, password)

View File

@ -3,7 +3,7 @@ import { cookies } from 'next/headers'
import { execFileSync } from 'child_process' import { execFileSync } from 'child_process'
import { verifySharedJwt } from '@/lib/jwt' import { verifySharedJwt } from '@/lib/jwt'
import { isLldapAdmin } from '@/lib/ldap' import { isLldapAdmin } from '@/lib/ldap'
import { queryLldap, execLldapWrite, esc } from '@/lib/lldap-db' import { queryLldap, execLldap, esc } from '@/lib/lldap-db'
function checkAdmin() { function checkAdmin() {
return async () => { return async () => {
@ -54,7 +54,7 @@ export async function DELETE(request: Request) {
} }
const safeUser = esc(username) const safeUser = esc(username)
execLldapWrite(`DELETE FROM users WHERE user_id='${safeUser}'`) execLldap(`DELETE FROM users WHERE user_id='${safeUser}'`)
const results: Record<string, boolean> = {} const results: Record<string, boolean> = {}
for (const [site, dbPath] of Object.entries({ for (const [site, dbPath] of Object.entries({
@ -100,7 +100,7 @@ export async function PATCH(request: Request) {
lldapSets.push(`modified_date = '${nowStr()}'`) lldapSets.push(`modified_date = '${nowStr()}'`)
siteSets.push(`updated_at = datetime('now', '+8 hours')`) 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}'` 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']) { 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']) {

View File

@ -2,7 +2,7 @@ import { NextResponse } from 'next/server'
import { cookies } from 'next/headers' import { cookies } from 'next/headers'
import { verifySharedJwt } from '@/lib/jwt' import { verifySharedJwt } from '@/lib/jwt'
import { isLldapAdmin } from '@/lib/ldap' 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' import { execFileSync } from 'child_process'
async function getLldapInfo(username: string): Promise<{ email: string; displayName: string }> { 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')}` 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 模式不可并发写) // 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 本地用户表 // 同步更新 assets / issue 本地用户表
const assetsDb = process.env.ASSETS_DB_PATH || '/data/other-sites/assets/assets.db' const assetsDb = process.env.ASSETS_DB_PATH || '/data/other-sites/assets/assets.db'

View File

@ -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 容器内执行 // LLDAP 使用 DELETE journal mode不可并发写 → 写操作必须通过 docker exec 在 LLDAP 容器内执行
// SQL 通过 stdin 传入 sqlite3不经过 shell 解析 → 无命令注入风险
import { execFileSync } from 'child_process' import { execFileSync } from 'child_process'
import bcrypt from 'bcryptjs' 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() return execFileSync('sqlite3', [LLDAP_DB, sql], { timeout: 5000, encoding: 'utf8' }).trim()
} }
/** docker exec lldap 执行 LLDAP 容器内操作(密码/写入) */ /** docker exec -i lldap sqlite3通过 stdin 传入 SQL无 shell 解析,无命令注入风险) */
function dockerExecLldap(cmd: string, timeout = 5000): string { export function execLldap(sql: string, timeout = 5000): string {
return execFileSync('docker', ['exec', 'lldap', 'sh', '-c', cmd], { timeout, encoding: 'utf8' }).trim() return execFileSync('docker', ['exec', '-i', 'lldap', 'sqlite3', '/data/users.db'], {
} input: sql, timeout, encoding: 'utf8',
}).trim()
/** docker exec lldap sqlite3 写操作 */
export function execLldapWrite(sql: string): void {
dockerExecLldap(`sqlite3 /data/users.db "${sql.replace(/"/g, '\\"')}"`, 5000)
} }
/** 安全的 SQL 字符串转义SQLite 标准:'' → 单引号) */ /** 安全的 SQL 字符串转义SQLite 标准:'' → 单引号) */
@ -25,12 +23,12 @@ export function esc(val: string): string {
return val.replace(/'/g, "''") return val.replace(/'/g, "''")
} }
/** 修改 LLDAP 用户密码docker exec lldap + lldap_set_password */ /** 修改 LLDAP 用户密码 */
export function lldapChangePassword(username: string, newPassword: string): void { export function lldapChangePassword(username: string, newPassword: string): void {
const hash = bcrypt.hashSync(newPassword, 12) const hash = bcrypt.hashSync(newPassword, 12)
const safeUser = esc(username) const safeUser = esc(username)
const now = new Date().toISOString().replace('T', ' ').slice(0, 19) 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 密码 */ /** 获取 admin 密码 */