fix(GOV-012): 修复 Schema 定义安全与错误语义

This commit is contained in:
2026-08-18 02:46:50 +08:00
parent 3d12c83621
commit 874796ab65
3 changed files with 79 additions and 13 deletions
+41 -12
View File
@@ -136,9 +136,9 @@ async function queryCatalogRows(connection, sql, database, limits) {
values: [database],
timeout: limits.queryTimeoutMs,
});
} catch {
} catch (error) {
// mysql2 错误可能包含连接 URL、用户名或密码,目录读取的公共接口一律不透传原始文本。
throw new SchemaExportError('DATABASE_QUERY_FAILED', '开发库 Schema 查询失败');
sanitizeDatabaseQueryError(error);
}
if (!Array.isArray(result) || !Array.isArray(result[0])) {
throw new SchemaExportError('CATALOG_SHAPE_INVALID', 'Schema 对象目录查询结果无效');
@@ -302,6 +302,19 @@ function maskStringLiterals(sql) {
return String(sql).replace(/'(?:''|\\.|[^'\\])*'/gs, "''");
}
function removeTableAutoIncrementOutsideLiterals(sql) {
const literals = [];
const masked = String(sql).replace(/'(?:''|\\.|[^'\\])*'/gs, (literal) => {
const index = literals.push(literal) - 1;
return `\u0001${index}\u0002`;
});
const cleaned = masked.replace(
/\sAUTO_INCREMENT=\d+(?=\s(?:DEFAULT\s+)?CHARSET=)/i,
'',
);
return cleaned.replace(/\u0001(\d+)\u0002/g, (_match, index) => literals[Number(index)]);
}
/**
* SHOW CREATE 的格式由服务端决定,这里只移除与结构无关的账号绑定、当前表自增值、
* 行尾分号和换行差异,不重排对象体,也不删除字符串字面量中的业务文本。
@@ -313,20 +326,26 @@ export function normalizeCreateDefinition(definition) {
/^(CREATE\s+(?:ALGORITHM=\S+\s+)?)DEFINER=`(?:``|[^`])+`@`(?:`(?:``|[^`])+`|'(?:''|[^'])+'|\S+)\s+/i,
'$1',
)
.replace(/\sAUTO_INCREMENT=\d+(?=\s(?:DEFAULT\s+)?CHARSET=)/i, '')
.trim()
.replace(/;+\s*$/, '');
const inspectable = maskStringLiterals(normalized);
const withoutCurrentAutoIncrement = removeTableAutoIncrementOutsideLiterals(normalized);
const inspectable = maskStringLiterals(withoutCurrentAutoIncrement);
if (
/\bDEFINER\s*=/i.test(inspectable) ||
/\b(?:password|passwd|pwd|api[_-]?key|api[_-]?token|access[_-]?token|client[_-]?secret|secret)\s*[:=]/i.test(
inspectable,
) ||
/\b(?:mysql|mariadb):\/\/[^\s'"`]+/i.test(inspectable)
/\b(?:mysql|mariadb):\/\/[^\s'"`]+/i.test(inspectable) ||
/(?:--|\/\*)\s*(?:host|hostname|server)\s*[:=]\s*[^\s*]+/i.test(inspectable)
) {
throw new SchemaExportError('UNSAFE_DEFINITION', 'Schema 对象定义包含禁止内容');
}
return normalized;
return withoutCurrentAutoIncrement;
}
function sanitizeDatabaseQueryError(error) {
if (error instanceof SchemaExportError) throw error;
throw new SchemaExportError('DATABASE_QUERY_FAILED', '开发库 Schema 查询失败');
}
export async function readMysql57ServerMetadata(
@@ -334,11 +353,16 @@ export async function readMysql57ServerMetadata(
database,
limits = DEFAULT_EXPORT_LIMITS,
) {
const [rows] = await connection.query({
sql: SERVER_METADATA_QUERY,
values: [database],
timeout: limits.queryTimeoutMs,
});
let rows;
try {
[rows] = await connection.query({
sql: SERVER_METADATA_QUERY,
values: [database],
timeout: limits.queryTimeoutMs,
});
} catch (error) {
sanitizeDatabaseQueryError(error);
}
if (!Array.isArray(rows) || rows.length !== 1) {
throw new SchemaExportError('DATABASE_METADATA_INVALID', '开发库元数据查询结果无效');
}
@@ -365,7 +389,12 @@ function getCreateDescriptors(catalog) {
async function readCreateDefinition(connection, descriptor, limits) {
const sql = `SHOW CREATE ${descriptor.type} ${quoteIdentifier(DEVELOPMENT_DATABASE)}.${quoteIdentifier(descriptor.name)}`;
const [rows] = await connection.query({ sql, timeout: limits.queryTimeoutMs });
let rows;
try {
[rows] = await connection.query({ sql, timeout: limits.queryTimeoutMs });
} catch (error) {
sanitizeDatabaseQueryError(error);
}
if (!Array.isArray(rows) || rows.length === 0) {
throw new SchemaExportError('SHOW_CREATE_EMPTY', 'SHOW CREATE 未返回对象定义');
}