167 lines
5.0 KiB
TypeScript
Executable File
167 lines
5.0 KiB
TypeScript
Executable File
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, '"')
|
|
.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 = [
|
|
/<script\b[^>]*>/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<T>(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()
|
|
}
|