fix: 보안 취약점 수정 및 XSS 방지 적용
## 백엔드 보안 수정 - 하드코딩된 비밀번호 및 JWT 시크릿 폴백 제거 - SQL Injection 방지를 위한 화이트리스트 검증 추가 - 인증 미적용 API 라우트에 requireAuth 미들웨어 적용 - CSRF 보호 미들웨어 구현 (csrf.js) - 파일 업로드 보안 유틸리티 추가 (fileUploadSecurity.js) - 비밀번호 정책 검증 유틸리티 추가 (passwordValidator.js) ## 프론트엔드 XSS 방지 - api-base.js에 전역 escapeHtml() 함수 추가 - 17개 주요 JS 파일에 escapeHtml 적용: - tbm.js, daily-patrol.js, daily-work-report.js - task-management.js, workplace-status.js - equipment-detail.js, equipment-management.js - issue-detail.js, issue-report.js - vacation-common.js, worker-management.js - safety-report-list.js, nonconformity-list.js - project-management.js, workplace-management.js ## 정리 - 백업 폴더 및 빈 파일 삭제 - SECURITY_GUIDE.md 문서 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
201
api.hyungi.net/middlewares/csrf.js
Normal file
201
api.hyungi.net/middlewares/csrf.js
Normal file
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* CSRF Protection Middleware
|
||||
*
|
||||
* Cross-Site Request Forgery 방지를 위한 토큰 기반 보호
|
||||
*
|
||||
* 구현 방식:
|
||||
* 1. 서버에서 CSRF 토큰 생성 및 응답 헤더로 전송
|
||||
* 2. 클라이언트는 state-changing 요청 시 토큰을 헤더에 포함
|
||||
* 3. 서버에서 토큰 검증
|
||||
*
|
||||
* @author TK-FB-Project
|
||||
* @since 2026-02-04
|
||||
*/
|
||||
|
||||
const crypto = require('crypto');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
// 토큰 저장소 (프로덕션에서는 Redis 사용 권장)
|
||||
const tokenStore = new Map();
|
||||
|
||||
// 토큰 유효 시간 (1시간)
|
||||
const TOKEN_EXPIRY = 60 * 60 * 1000;
|
||||
|
||||
// 토큰 정리 주기 (5분)
|
||||
const CLEANUP_INTERVAL = 5 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* 만료된 토큰 정리
|
||||
*/
|
||||
const cleanupExpiredTokens = () => {
|
||||
const now = Date.now();
|
||||
for (const [token, data] of tokenStore.entries()) {
|
||||
if (now > data.expiresAt) {
|
||||
tokenStore.delete(token);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 주기적 정리
|
||||
setInterval(cleanupExpiredTokens, CLEANUP_INTERVAL);
|
||||
|
||||
/**
|
||||
* CSRF 토큰 생성
|
||||
*
|
||||
* @param {string} sessionId - 세션 ID 또는 사용자 식별자
|
||||
* @returns {string} 생성된 CSRF 토큰
|
||||
*/
|
||||
const generateToken = (sessionId) => {
|
||||
const token = crypto.randomBytes(32).toString('hex');
|
||||
const expiresAt = Date.now() + TOKEN_EXPIRY;
|
||||
|
||||
tokenStore.set(token, {
|
||||
sessionId,
|
||||
expiresAt,
|
||||
createdAt: Date.now()
|
||||
});
|
||||
|
||||
return token;
|
||||
};
|
||||
|
||||
/**
|
||||
* CSRF 토큰 검증
|
||||
*
|
||||
* @param {string} token - 검증할 토큰
|
||||
* @param {string} sessionId - 세션 ID
|
||||
* @returns {boolean} 유효 여부
|
||||
*/
|
||||
const validateToken = (token, sessionId) => {
|
||||
if (!token || !tokenStore.has(token)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = tokenStore.get(token);
|
||||
|
||||
// 만료 체크
|
||||
if (Date.now() > data.expiresAt) {
|
||||
tokenStore.delete(token);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 세션 일치 체크
|
||||
if (data.sessionId !== sessionId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* CSRF 토큰을 응답 헤더에 설정하는 미들웨어
|
||||
*
|
||||
* @param {Object} req - Express request
|
||||
* @param {Object} res - Express response
|
||||
* @param {Function} next - Next middleware
|
||||
*/
|
||||
const setCsrfToken = (req, res, next) => {
|
||||
// 세션 ID 또는 사용자 ID 사용
|
||||
const sessionId = req.user?.user_id?.toString() || req.sessionID || req.ip;
|
||||
|
||||
// 새 토큰 생성
|
||||
const token = generateToken(sessionId);
|
||||
|
||||
// 응답 헤더에 토큰 설정
|
||||
res.setHeader('X-CSRF-Token', token);
|
||||
|
||||
// 요청 객체에 저장 (다른 미들웨어에서 사용 가능)
|
||||
req.csrfToken = token;
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
/**
|
||||
* CSRF 토큰 검증 미들웨어
|
||||
* POST, PUT, DELETE, PATCH 요청에 적용
|
||||
*
|
||||
* @param {Object} options - 옵션
|
||||
* @param {string[]} options.ignoreMethods - 검증 제외 메서드 (기본: GET, HEAD, OPTIONS)
|
||||
* @param {string[]} options.ignorePaths - 검증 제외 경로 (정규식 패턴 가능)
|
||||
* @returns {Function} Express 미들웨어
|
||||
*/
|
||||
const verifyCsrfToken = (options = {}) => {
|
||||
const {
|
||||
ignoreMethods = ['GET', 'HEAD', 'OPTIONS'],
|
||||
ignorePaths = ['/api/auth/login', '/api/auth/register', '/api/health']
|
||||
} = options;
|
||||
|
||||
return (req, res, next) => {
|
||||
// 제외 메서드 체크
|
||||
if (ignoreMethods.includes(req.method)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// 제외 경로 체크
|
||||
for (const pattern of ignorePaths) {
|
||||
if (typeof pattern === 'string' && req.path === pattern) {
|
||||
return next();
|
||||
}
|
||||
if (pattern instanceof RegExp && pattern.test(req.path)) {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
// 토큰 추출 (헤더 또는 body에서)
|
||||
const token = req.headers['x-csrf-token'] ||
|
||||
req.headers['csrf-token'] ||
|
||||
req.body?._csrf ||
|
||||
req.query?._csrf;
|
||||
|
||||
// 세션 ID
|
||||
const sessionId = req.user?.user_id?.toString() || req.sessionID || req.ip;
|
||||
|
||||
// 토큰 검증
|
||||
if (!validateToken(token, sessionId)) {
|
||||
logger.warn('CSRF 토큰 검증 실패', {
|
||||
path: req.path,
|
||||
method: req.method,
|
||||
ip: req.ip,
|
||||
hasToken: !!token
|
||||
});
|
||||
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'CSRF 토큰이 유효하지 않습니다. 페이지를 새로고침 후 다시 시도해주세요.',
|
||||
code: 'CSRF_TOKEN_INVALID'
|
||||
});
|
||||
}
|
||||
|
||||
// 사용된 토큰 제거 (일회성 사용)
|
||||
tokenStore.delete(token);
|
||||
|
||||
// 새 토큰 발급
|
||||
const newToken = generateToken(sessionId);
|
||||
res.setHeader('X-CSRF-Token', newToken);
|
||||
req.csrfToken = newToken;
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* CSRF 토큰 발급 엔드포인트 핸들러
|
||||
* GET /api/csrf-token
|
||||
*/
|
||||
const getCsrfToken = (req, res) => {
|
||||
const sessionId = req.user?.user_id?.toString() || req.sessionID || req.ip;
|
||||
const token = generateToken(sessionId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
csrfToken: token,
|
||||
expiresIn: TOKEN_EXPIRY / 1000 // 초 단위
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
generateToken,
|
||||
validateToken,
|
||||
setCsrfToken,
|
||||
verifyCsrfToken,
|
||||
getCsrfToken
|
||||
};
|
||||
Reference in New Issue
Block a user