95 lines
2.6 KiB
TypeScript
95 lines
2.6 KiB
TypeScript
|
|
/**
|
||
|
|
* queryUtils.ts
|
||
|
|
*
|
||
|
|
* SQL 쿼리 관련 순수 유틸 함수 모음.
|
||
|
|
*
|
||
|
|
* [사용처]
|
||
|
|
* - QueryManager.tsx : 쿼리 탭에서 안전성 검증 + 파라미터 자동 감지
|
||
|
|
* - modals/QuerySettingsTab.tsx : 인캔버스 설정 모달의 데이터 연결 탭
|
||
|
|
*
|
||
|
|
* Phase 5-A에서 QueryManager의 인라인 함수를 이곳으로 추출.
|
||
|
|
* 두 소비처가 동일 로직을 공유할 수 있도록 단일 소스로 유지한다.
|
||
|
|
*/
|
||
|
|
|
||
|
|
/** SQL 안전성 검증 결과 */
|
||
|
|
export interface QueryValidationResult {
|
||
|
|
isValid: boolean;
|
||
|
|
errorMessage: string | null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* SELECT 전용 SQL 안전성 검증.
|
||
|
|
* DML/DDL 키워드를 포함하거나 다중 쿼리를 감지하면 오류를 반환한다.
|
||
|
|
*/
|
||
|
|
export const validateQuerySafety = (sql: string): QueryValidationResult => {
|
||
|
|
if (!sql || sql.trim() === "") {
|
||
|
|
return { isValid: false, errorMessage: "쿼리를 입력해주세요." };
|
||
|
|
}
|
||
|
|
|
||
|
|
const dangerousKeywords = [
|
||
|
|
"DELETE",
|
||
|
|
"DROP",
|
||
|
|
"TRUNCATE",
|
||
|
|
"INSERT",
|
||
|
|
"UPDATE",
|
||
|
|
"ALTER",
|
||
|
|
"CREATE",
|
||
|
|
"REPLACE",
|
||
|
|
"MERGE",
|
||
|
|
"GRANT",
|
||
|
|
"REVOKE",
|
||
|
|
"EXECUTE",
|
||
|
|
"EXEC",
|
||
|
|
"CALL",
|
||
|
|
];
|
||
|
|
|
||
|
|
const upperSql = sql.toUpperCase().trim();
|
||
|
|
|
||
|
|
for (const keyword of dangerousKeywords) {
|
||
|
|
const regex = new RegExp(`\\b${keyword}\\b`, "i");
|
||
|
|
if (regex.test(upperSql)) {
|
||
|
|
return {
|
||
|
|
isValid: false,
|
||
|
|
errorMessage: `보안상의 이유로 ${keyword} 명령어는 사용할 수 없습니다. SELECT 쿼리만 허용됩니다.`,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!upperSql.startsWith("SELECT") && !upperSql.startsWith("WITH")) {
|
||
|
|
return {
|
||
|
|
isValid: false,
|
||
|
|
errorMessage: "SELECT 쿼리만 허용됩니다. 데이터 조회 용도로만 사용할 수 있습니다.",
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
const semicolonCount = (sql.match(/;/g) || []).length;
|
||
|
|
if (semicolonCount > 1 || (semicolonCount === 1 && !sql.trim().endsWith(";"))) {
|
||
|
|
return {
|
||
|
|
isValid: false,
|
||
|
|
errorMessage: "보안상의 이유로 여러 개의 쿼리를 동시에 실행할 수 없습니다.",
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
return { isValid: true, errorMessage: null };
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* SQL에서 파라미터 플레이스홀더($1, $2 …) 추출.
|
||
|
|
* 문자열 리터럴(단일따옴표) 내부는 제외하며, 등장 순서를 유지한다.
|
||
|
|
*/
|
||
|
|
export const detectParameters = (sql: string): string[] => {
|
||
|
|
const withoutStrings = sql.replace(/'[^']*'/g, "");
|
||
|
|
const matches = withoutStrings.match(/\$\d+/g);
|
||
|
|
if (!matches) return [];
|
||
|
|
|
||
|
|
const seen = new Set<string>();
|
||
|
|
const result: string[] = [];
|
||
|
|
for (const match of matches) {
|
||
|
|
if (!seen.has(match)) {
|
||
|
|
seen.add(match);
|
||
|
|
result.push(match);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return result;
|
||
|
|
};
|