import { Request, Response, NextFunction } from 'express' /** * 입력값 살균(sanitize) 유틸리티 * SQL 인젝션 및 XSS 공격에 사용되는 위험 문자를 제거/이스케이프 */ // HTML 특수문자 이스케이프 (XSS 방지) export function escapeHtml(str: string): string { return str .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') } // SQL 인젝션에 사용되는 위험 패턴 탐지 const SQL_INJECTION_PATTERNS = [ /(\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC|EXECUTE|UNION|TRUNCATE|GRANT|REVOKE)\b)/i, /(--|#|\/\*|\*\/)/, // SQL 주석 /(\b(OR|AND)\b\s+\d+\s*=\s*\d+)/i, // OR 1=1, AND 1=1 /(;[\s]*$)/, // SQL 종료자 /(\bxp_\w+)/i, // SQL Server xp_ 프로시저 /(\bsp_\w+)/i, // SQL Server sp_ 프로시저 ] // XSS 공격에 사용되는 위험 패턴 탐지 const XSS_PATTERNS = [ /]*>/i, /javascript\s*:/i, /on\w+\s*=/i, // onclick=, onerror= 등 /eval\s*\(/i, /expression\s*\(/i, /vbscript\s*:/i, /data\s*:\s*text\/html/i, ] // 문자열에서 위험 패턴 검사 export function containsSqlInjection(input: string): boolean { return SQL_INJECTION_PATTERNS.some(pattern => pattern.test(input)) } export function containsXss(input: string): boolean { return XSS_PATTERNS.some(pattern => pattern.test(input)) } // 문자열 살균: 위험 HTML 태그와 특수문자 제거 export function sanitizeString(input: string): string { return input .replace(/<[^>]*>/g, '') // HTML 태그 제거 .replace(/[<>"'`;]/g, '') // 위험 특수문자 제거 .trim() } // 숫자 검증 export function isValidNumber(value: unknown, min?: number, max?: number): value is number { if (typeof value !== 'number' || isNaN(value) || !isFinite(value)) return false if (min !== undefined && value < min) return false if (max !== undefined && value > max) return false return true } // 위도 검증 (-90 ~ 90) export function isValidLatitude(lat: unknown): lat is number { return isValidNumber(lat, -90, 90) } // 경도 검증 (-180 ~ 180) export function isValidLongitude(lon: unknown): lon is number { return isValidNumber(lon, -180, 180) } // 허용된 값 목록 검증 (화이트리스트) export function isAllowedValue(value: T, allowedValues: T[]): boolean { return allowedValues.includes(value) } // 문자열 길이 검증 export function isValidStringLength(str: string, maxLength: number): boolean { return typeof str === 'string' && str.length <= maxLength } /** * 요청 본문(body) 살균 미들웨어 * 모든 문자열 필드에서 XSS/SQL 인젝션 패턴을 검사 */ export function sanitizeBody(req: Request, res: Response, next: NextFunction): void { if (req.body && typeof req.body === 'object') { for (const [key, value] of Object.entries(req.body)) { if (typeof value === 'string') { // XSS 패턴 검사 if (containsXss(value)) { res.status(400).json({ error: '유효하지 않은 입력값', field: key, message: '허용되지 않는 문자가 포함되어 있습니다.' }) return } // SQL 인젝션 패턴 검사 if (containsSqlInjection(value)) { res.status(400).json({ error: '유효하지 않은 입력값', field: key, message: '허용되지 않는 문자가 포함되어 있습니다.' }) return } } } } next() } /** * 요청 파라미터(params) 살균 미들웨어 * URL 파라미터에서 위험 문자 검사 */ export function sanitizeParams(req: Request, res: Response, next: NextFunction): void { for (const [key, value] of Object.entries(req.params)) { if (typeof value === 'string') { if (containsXss(value) || containsSqlInjection(value)) { res.status(400).json({ error: '유효하지 않은 파라미터', field: key, message: '허용되지 않는 문자가 포함되어 있습니다.' }) return } } } next() } /** * 요청 쿼리(query) 살균 미들웨어 */ export function sanitizeQuery(req: Request, res: Response, next: NextFunction): void { for (const [key, value] of Object.entries(req.query)) { if (typeof value === 'string') { if (containsXss(value) || containsSqlInjection(value)) { res.status(400).json({ error: '유효하지 않은 쿼리 파라미터', field: key, message: '허용되지 않는 문자가 포함되어 있습니다.' }) return } } } next() } /** * JSON 본문 크기 제한 (보고서 지도 캡처 이미지 포함 대응: 5mb) */ export const BODY_SIZE_LIMIT = '5mb' /** * 응답 헤더에서 서버 정보 제거 */ export function removeServerInfo(req: Request, res: Response, next: NextFunction): void { res.removeHeader('X-Powered-By') next() }