ERP-node/src/utils/databaseValidator.ts

385 lines
8.2 KiB
TypeScript

/**
* 데이터베이스 관련 검증 유틸리티
*
* SQL 인젝션 방지 및 데이터 무결성 보장을 위한 검증 함수들
*/
export class DatabaseValidator {
// PostgreSQL 예약어 목록 (주요 키워드만)
private static readonly RESERVED_WORDS = new Set([
"SELECT",
"INSERT",
"UPDATE",
"DELETE",
"FROM",
"WHERE",
"JOIN",
"INNER",
"LEFT",
"RIGHT",
"FULL",
"ON",
"GROUP",
"BY",
"ORDER",
"HAVING",
"LIMIT",
"OFFSET",
"UNION",
"ALL",
"DISTINCT",
"AS",
"AND",
"OR",
"NOT",
"NULL",
"TRUE",
"FALSE",
"CASE",
"WHEN",
"THEN",
"ELSE",
"END",
"IF",
"EXISTS",
"IN",
"BETWEEN",
"LIKE",
"ILIKE",
"SIMILAR",
"TO",
"CREATE",
"DROP",
"ALTER",
"TABLE",
"INDEX",
"VIEW",
"FUNCTION",
"PROCEDURE",
"TRIGGER",
"DATABASE",
"SCHEMA",
"USER",
"ROLE",
"GRANT",
"REVOKE",
"COMMIT",
"ROLLBACK",
"BEGIN",
"TRANSACTION",
"SAVEPOINT",
"RELEASE",
"CONSTRAINT",
"PRIMARY",
"FOREIGN",
"KEY",
"UNIQUE",
"CHECK",
"DEFAULT",
"REFERENCES",
"CASCADE",
"RESTRICT",
"SET",
"ACTION",
"DEFERRABLE",
"INITIALLY",
"DEFERRED",
"IMMEDIATE",
"MATCH",
"PARTIAL",
"SIMPLE",
"FULL",
]);
// 유효한 PostgreSQL 데이터 타입 패턴
private static readonly DATA_TYPE_PATTERNS = [
/^(SMALLINT|INTEGER|BIGINT|DECIMAL|NUMERIC|REAL|DOUBLE\s+PRECISION|SMALLSERIAL|SERIAL|BIGSERIAL)$/i,
/^(MONEY)$/i,
/^(CHARACTER\s+VARYING|VARCHAR|CHARACTER|CHAR|TEXT)(\(\d+\))?$/i,
/^(BYTEA)$/i,
/^(TIMESTAMP|TIME)(\s+(WITH|WITHOUT)\s+TIME\s+ZONE)?(\(\d+\))?$/i,
/^(DATE|INTERVAL)(\(\d+\))?$/i,
/^(BOOLEAN|BOOL)$/i,
/^(POINT|LINE|LSEG|BOX|PATH|POLYGON|CIRCLE)$/i,
/^(CIDR|INET|MACADDR|MACADDR8)$/i,
/^(BIT|BIT\s+VARYING)(\(\d+\))?$/i,
/^(TSVECTOR|TSQUERY)$/i,
/^(UUID)$/i,
/^(XML)$/i,
/^(JSON|JSONB)$/i,
/^(ARRAY|INTEGER\[\]|TEXT\[\]|VARCHAR\[\])$/i,
/^(DECIMAL|NUMERIC)\(\d+,\d+\)$/i,
];
/**
* 테이블명 검증
*/
static validateTableName(tableName: string): boolean {
if (!tableName || typeof tableName !== "string") {
return false;
}
// 길이 제한 (PostgreSQL 최대 63자)
if (tableName.length === 0 || tableName.length > 63) {
return false;
}
// 유효한 식별자 패턴 (문자 또는 밑줄로 시작, 문자/숫자/밑줄만 포함)
const validPattern = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
if (!validPattern.test(tableName)) {
return false;
}
// 예약어 체크
if (this.RESERVED_WORDS.has(tableName.toUpperCase())) {
return false;
}
return true;
}
/**
* 컬럼명 검증
*/
static validateColumnName(columnName: string): boolean {
if (!columnName || typeof columnName !== "string") {
return false;
}
// 길이 제한
if (columnName.length === 0 || columnName.length > 63) {
return false;
}
// JSON 연산자 포함 컬럼명 허용 (예: config->>'type', data->>path)
if (columnName.includes("->") || columnName.includes("->>")) {
const baseName = columnName.split(/->|->>/)[0];
return this.validateColumnName(baseName);
}
// 유효한 식별자 패턴
const validPattern = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
if (!validPattern.test(columnName)) {
return false;
}
// 예약어 체크
if (this.RESERVED_WORDS.has(columnName.toUpperCase())) {
return false;
}
return true;
}
/**
* 데이터 타입 검증
*/
static validateDataType(dataType: string): boolean {
if (!dataType || typeof dataType !== "string") {
return false;
}
const normalizedType = dataType.trim().toUpperCase();
return this.DATA_TYPE_PATTERNS.some((pattern) =>
pattern.test(normalizedType)
);
}
/**
* WHERE 조건 검증
*/
static validateWhereClause(whereClause: Record<string, any>): boolean {
if (!whereClause || typeof whereClause !== "object") {
return false;
}
// 모든 키가 유효한 컬럼명인지 확인
for (const key of Object.keys(whereClause)) {
if (!this.validateColumnName(key)) {
return false;
}
}
return true;
}
/**
* 페이지네이션 파라미터 검증
*/
static validatePagination(page: number, pageSize: number): boolean {
// 페이지 번호는 1 이상
if (!Number.isInteger(page) || page < 1) {
return false;
}
// 페이지 크기는 1 이상 1000 이하
if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 1000) {
return false;
}
return true;
}
/**
* ORDER BY 절 검증
*/
static validateOrderBy(orderBy: string): boolean {
if (!orderBy || typeof orderBy !== "string") {
return false;
}
// 기본 패턴: column_name [ASC|DESC]
const orderPattern = /^[a-zA-Z_][a-zA-Z0-9_]*(\s+(ASC|DESC))?$/i;
// 여러 컬럼 정렬의 경우 콤마로 분리하여 각각 검증
const orderClauses = orderBy.split(",").map((clause) => clause.trim());
return orderClauses.every((clause) => {
return (
orderPattern.test(clause) &&
this.validateColumnName(clause.split(/\s+/)[0])
);
});
}
/**
* UUID 형식 검증
*/
static validateUUID(uuid: string): boolean {
if (!uuid || typeof uuid !== "string") {
return false;
}
const uuidPattern =
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
return uuidPattern.test(uuid);
}
/**
* 이메일 형식 검증
*/
static validateEmail(email: string): boolean {
if (!email || typeof email !== "string") {
return false;
}
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailPattern.test(email) && email.length <= 254;
}
/**
* SQL 인젝션 위험 문자열 검사
*/
static containsSqlInjection(input: string): boolean {
if (!input || typeof input !== "string") {
return false;
}
// 위험한 SQL 패턴들
const dangerousPatterns = [
/('|(\\')|(;)|(--)|(\s+(OR|AND)\s+\d+\s*=\s*\d+)/i,
/(UNION|SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC|EXECUTE)/i,
/(\bxp_\w+|\bsp_\w+)/i, // SQL Server 확장 프로시저
/(script|javascript|vbscript|onload|onerror)/i, // XSS 패턴
];
return dangerousPatterns.some((pattern) => pattern.test(input));
}
/**
* 숫자 범위 검증
*/
static validateNumberRange(
value: number,
min?: number,
max?: number
): boolean {
if (typeof value !== "number" || !Number.isFinite(value)) {
return false;
}
if (min !== undefined && value < min) {
return false;
}
if (max !== undefined && value > max) {
return false;
}
return true;
}
/**
* 문자열 길이 검증
*/
static validateStringLength(
value: string,
minLength?: number,
maxLength?: number
): boolean {
if (typeof value !== "string") {
return false;
}
if (minLength !== undefined && value.length < minLength) {
return false;
}
if (maxLength !== undefined && value.length > maxLength) {
return false;
}
return true;
}
/**
* JSON 형식 검증
*/
static validateJSON(jsonString: string): boolean {
try {
JSON.parse(jsonString);
return true;
} catch {
return false;
}
}
/**
* 날짜 형식 검증 (ISO 8601)
*/
static validateDateISO(dateString: string): boolean {
if (!dateString || typeof dateString !== "string") {
return false;
}
const date = new Date(dateString);
return !isNaN(date.getTime()) && dateString === date.toISOString();
}
/**
* 배열 요소 검증
*/
static validateArray<T>(
array: any[],
validator: (item: T) => boolean,
minLength?: number,
maxLength?: number
): boolean {
if (!Array.isArray(array)) {
return false;
}
if (minLength !== undefined && array.length < minLength) {
return false;
}
if (maxLength !== undefined && array.length > maxLength) {
return false;
}
return array.every((item) => validator(item));
}
}