wing-ops/backend/src/middleware/security.ts
jeonghyo.k c4f11423aa feat(reports): 보고서 확산예측 지도 캡처 기능 추가
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 18:23:42 +09:00

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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;')
}
// 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()
}