쿼리 에서 cud 명령어 막기 구현
This commit is contained in:
@@ -22,6 +22,61 @@ import { DatabaseConnectorFactory } from "../database/DatabaseConnectorFactory";
|
|||||||
import { ExternalDbConnectionService } from "./externalDbConnectionService";
|
import { ExternalDbConnectionService } from "./externalDbConnectionService";
|
||||||
|
|
||||||
export class ReportService {
|
export class ReportService {
|
||||||
|
/**
|
||||||
|
* SQL 쿼리 검증 (SELECT만 허용)
|
||||||
|
*/
|
||||||
|
private validateQuerySafety(sql: string): void {
|
||||||
|
// 위험한 SQL 명령어 목록
|
||||||
|
const dangerousKeywords = [
|
||||||
|
"DELETE",
|
||||||
|
"DROP",
|
||||||
|
"TRUNCATE",
|
||||||
|
"INSERT",
|
||||||
|
"UPDATE",
|
||||||
|
"ALTER",
|
||||||
|
"CREATE",
|
||||||
|
"REPLACE",
|
||||||
|
"MERGE",
|
||||||
|
"GRANT",
|
||||||
|
"REVOKE",
|
||||||
|
"EXECUTE",
|
||||||
|
"EXEC",
|
||||||
|
"CALL",
|
||||||
|
];
|
||||||
|
|
||||||
|
// SQL을 대문자로 변환하여 검사
|
||||||
|
const upperSql = sql.toUpperCase().trim();
|
||||||
|
|
||||||
|
// 위험한 키워드 검사
|
||||||
|
for (const keyword of dangerousKeywords) {
|
||||||
|
// 단어 경계를 고려하여 검사 (예: DELETE, DELETE FROM 등)
|
||||||
|
const regex = new RegExp(`\\b${keyword}\\b`, "i");
|
||||||
|
if (regex.test(upperSql)) {
|
||||||
|
throw new Error(
|
||||||
|
`보안상의 이유로 ${keyword} 명령어는 사용할 수 없습니다. SELECT 쿼리만 허용됩니다.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SELECT 쿼리인지 확인
|
||||||
|
if (!upperSql.startsWith("SELECT") && !upperSql.startsWith("WITH")) {
|
||||||
|
throw new Error(
|
||||||
|
"SELECT 쿼리만 허용됩니다. 데이터 조회 용도로만 사용할 수 있습니다."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 세미콜론으로 구분된 여러 쿼리 방지
|
||||||
|
const semicolonCount = (sql.match(/;/g) || []).length;
|
||||||
|
if (
|
||||||
|
semicolonCount > 1 ||
|
||||||
|
(semicolonCount === 1 && !sql.trim().endsWith(";"))
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"보안상의 이유로 여러 개의 쿼리를 동시에 실행할 수 없습니다."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 리포트 목록 조회
|
* 리포트 목록 조회
|
||||||
*/
|
*/
|
||||||
@@ -674,6 +729,9 @@ export class ReportService {
|
|||||||
connectionId = queryResult.external_connection_id;
|
connectionId = queryResult.external_connection_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SQL 쿼리 안전성 검증 (SELECT만 허용)
|
||||||
|
this.validateQuerySafety(sql_query);
|
||||||
|
|
||||||
// 파라미터 배열 생성 ($1, $2 순서대로)
|
// 파라미터 배열 생성 ($1, $2 순서대로)
|
||||||
const paramArray: any[] = [];
|
const paramArray: any[] = [];
|
||||||
for (const param of queryParameters) {
|
for (const param of queryParameters) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -16,6 +16,65 @@ import { reportApi } from "@/lib/api/reportApi";
|
|||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import type { ExternalConnection } from "@/types/report";
|
import type { ExternalConnection } from "@/types/report";
|
||||||
|
|
||||||
|
// SQL 쿼리 안전성 검증 함수 (컴포넌트 외부에 선언)
|
||||||
|
const validateQuerySafety = (sql: string): { isValid: boolean; errorMessage: string | null } => {
|
||||||
|
if (!sql || sql.trim() === "") {
|
||||||
|
return { isValid: false, errorMessage: "쿼리를 입력해주세요." };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 위험한 SQL 명령어 목록
|
||||||
|
const dangerousKeywords = [
|
||||||
|
"DELETE",
|
||||||
|
"DROP",
|
||||||
|
"TRUNCATE",
|
||||||
|
"INSERT",
|
||||||
|
"UPDATE",
|
||||||
|
"ALTER",
|
||||||
|
"CREATE",
|
||||||
|
"REPLACE",
|
||||||
|
"MERGE",
|
||||||
|
"GRANT",
|
||||||
|
"REVOKE",
|
||||||
|
"EXECUTE",
|
||||||
|
"EXEC",
|
||||||
|
"CALL",
|
||||||
|
];
|
||||||
|
|
||||||
|
// SQL을 대문자로 변환하여 검사
|
||||||
|
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 쿼리만 허용됩니다.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 };
|
||||||
|
};
|
||||||
|
|
||||||
export function QueryManager() {
|
export function QueryManager() {
|
||||||
const { queries, setQueries, reportId, setQueryResult, getQueryResult } = useReportDesigner();
|
const { queries, setQueries, reportId, setQueryResult, getQueryResult } = useReportDesigner();
|
||||||
const [selectedQueryId, setSelectedQueryId] = useState<string | null>(null);
|
const [selectedQueryId, setSelectedQueryId] = useState<string | null>(null);
|
||||||
@@ -29,6 +88,12 @@ export function QueryManager() {
|
|||||||
const selectedQuery = queries.find((q) => q.id === selectedQueryId);
|
const selectedQuery = queries.find((q) => q.id === selectedQueryId);
|
||||||
const testResult = selectedQuery ? getQueryResult(selectedQuery.id) : null;
|
const testResult = selectedQuery ? getQueryResult(selectedQuery.id) : null;
|
||||||
|
|
||||||
|
// 선택된 쿼리의 안전성 검증 결과
|
||||||
|
const queryValidation = useMemo(
|
||||||
|
() => (selectedQuery ? validateQuerySafety(selectedQuery.sqlQuery) : { isValid: false, errorMessage: null }),
|
||||||
|
[selectedQuery],
|
||||||
|
);
|
||||||
|
|
||||||
// 외부 DB 연결 목록 조회
|
// 외부 DB 연결 목록 조회
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchConnections = async () => {
|
const fetchConnections = async () => {
|
||||||
@@ -139,6 +204,17 @@ export function QueryManager() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SQL 쿼리 안전성 검증
|
||||||
|
const validation = validateQuerySafety(selectedQuery.sqlQuery);
|
||||||
|
if (!validation.isValid) {
|
||||||
|
toast({
|
||||||
|
title: "쿼리 검증 실패",
|
||||||
|
description: validation.errorMessage || "잘못된 쿼리입니다.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsTestRunning(true);
|
setIsTestRunning(true);
|
||||||
try {
|
try {
|
||||||
// new 리포트는 임시 ID 사용하고 SQL 쿼리 직접 전달
|
// new 리포트는 임시 ID 사용하고 SQL 쿼리 직접 전달
|
||||||
@@ -381,13 +457,21 @@ export function QueryManager() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* SQL 검증 경고 메시지 */}
|
||||||
|
{!queryValidation.isValid && queryValidation.errorMessage && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertDescription className="text-xs">{queryValidation.errorMessage}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 테스트 실행 */}
|
{/* 테스트 실행 */}
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="default"
|
variant="default"
|
||||||
className="w-full bg-red-500 hover:bg-red-600"
|
className="w-full bg-red-500 hover:bg-red-600"
|
||||||
onClick={handleTestQuery}
|
onClick={handleTestQuery}
|
||||||
disabled={!selectedQuery.sqlQuery || isTestRunning || !isAllParametersFilled()}
|
disabled={!queryValidation.isValid || isTestRunning || !isAllParametersFilled()}
|
||||||
>
|
>
|
||||||
<Play className="mr-2 h-4 w-4" />
|
<Play className="mr-2 h-4 w-4" />
|
||||||
{isTestRunning ? "실행 중..." : "실행"}
|
{isTestRunning ? "실행 중..." : "실행"}
|
||||||
|
|||||||
Reference in New Issue
Block a user