## 백엔드 보안 수정 - 하드코딩된 비밀번호 및 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>
18 KiB
18 KiB
TK-FB-Project 보안 가이드
최종 업데이트: 2026-02-04 작성자: TK-FB-Project Security Review
이 문서는 TK-FB-Project의 보안 취약점 분석 결과와 개발 시 준수해야 할 보안 가이드라인을 정리한 것입니다.
목차
1. 보안 취약점 요약
1.1 심각도별 분류
| 심각도 | 발견 | 수정됨 | 미수정 |
|---|---|---|---|
| CRITICAL | 2 | 2 | 0 |
| HIGH | 14 | 11 | 3 |
| MEDIUM | 9 | 3 | 6 |
| 총계 | 25 | 16 | 9 |
1.2 카테고리별 분류
| 카테고리 | 백엔드 | 프론트엔드 |
|---|---|---|
| 인증/인가 | 3 | 0 |
| SQL Injection | 1 | 0 |
| XSS | 0 | 3 |
| 민감정보 노출 | 2 | 2 |
| 파일 업로드 | 2 | 0 |
| CSRF | 1 | 1 |
| 입력 검증 | 2 | 2 |
| 세션/토큰 | 2 | 1 |
| 기타 | 2 | 1 |
2. 수정 완료된 취약점
2.1 [CRITICAL] 하드코딩된 테스트 비밀번호
- 파일:
api.hyungi.net/routes/auth.js - 문제:
password === 'password'하드코딩 - 해결: bcrypt.compare() 사용으로 변경
// Before (취약)
const isValid = password === 'password'; // 임시
// After (수정됨)
const isValid = await bcrypt.compare(password, user.password);
2.2 [CRITICAL] JWT 시크릿 폴백 값
- 파일:
api.hyungi.net/routes/auth.js - 문제:
process.env.JWT_SECRET || 'your-secret-key' - 해결: 폴백 제거 및 환경변수 필수화
// Before (취약)
process.env.JWT_SECRET || 'your-secret-key'
// After (수정됨)
process.env.JWT_SECRET // 미설정 시 경고 로그 출력
2.3 [HIGH] 인증 미적용 API 라우트
- 파일들:
routes/toolsRoute.jsroutes/projectRoutes.jsroutes/notificationRoutes.js
- 해결:
requireAuth,requireMinLevel미들웨어 적용
// Before (취약)
router.get('/', controller.getAll);
router.post('/', controller.create);
// After (수정됨)
router.get('/', requireAuth, controller.getAll);
router.post('/', requireAuth, requireMinLevel('group_leader'), controller.create);
2.4 [HIGH] SQL Injection 취약점
- 파일:
utils/queryOptimizer.js - 문제: ORDER BY, 테이블명 직접 삽입
- 해결: 화이트리스트 검증 함수 추가
// Before (취약)
const pagedQuery = `${baseQuery} ORDER BY ${orderBy} ${orderDirection}...`;
// After (수정됨)
const safeOrderBy = validateIdentifier(orderBy, 'column');
const safeOrderDirection = validateOrderDirection(orderDirection);
const pagedQuery = `${baseQuery} ORDER BY ${safeOrderBy} ${safeOrderDirection}...`;
3. 추가 조치 필요 항목
3.1 [HIGH] XSS 취약점 - innerHTML 사용 (대부분 수정됨)
수정 완료 (총 17개 파일):
api-base.js에 전역escapeHtml()함수 추가됨tbm.js- 세션 카드, 작업자 목록, 작업 라인, 드롭다운 등 주요 렌더링daily-patrol.js- 공장 카드, 점검 현황, 작업장 지도, 체크리스트, 물품 섹션daily-work-report.js- 완료 보고서, 작업자 현황, 부적합 목록, 드롭다운 등task-management.js- 작업 탭, 작업 카드, 공정 선택workplace-status.js- 작업 현황, 설비 상태, 작업자/방문자 탭equipment-detail.js- 설비 정보, 사진, 수리 이력, 외부 반출, 이동 이력issue-detail.js- 기본 정보, 신고 내용, 처리 정보, 상태 타임라인, 담당자 배정vacation-common.js- 휴가 신청 목록, 액션 버튼equipment-management.js- 설비 목록, 작업장/유형 필터worker-management.js- 부서 목록, 작업자 목록safety-report-list.js- 안전신고 목록 렌더링nonconformity-list.js- 부적합 목록 렌더링project-management.js- 프로젝트 카드 렌더링issue-report.js- 작업 선택 모달, 위치 정보 표시
추가 수정 권장 파일 (70+ 파일에 innerHTML 사용):
admin-settings.jsmodern-dashboard.jswork-report-calendar.js- 기타 innerHTML을 사용하는 파일들 (우선순위에 따라 점진적 수정 권장)
수정 패턴:
// Before (취약)
element.innerHTML = `<option>${data.name}</option>`;
// After (안전)
element.innerHTML = `<option>${escapeHtml(data.name)}</option>`;
// 숫자 값도 검증
onclick="handler(${parseInt(data.id) || 0})"
조치 방법:
escapeHtml()은api-base.js에서 전역으로 제공됨- 모든 innerHTML에서 사용자/API 데이터에
escapeHtml()적용 - onclick 핸들러의 ID 값은
parseInt()사용 - 검색 패턴:
\.innerHTML\s*=.*\$\{로 취약점 찾기
3.2 [HIGH] CSRF 보호 ✅ 구현 완료 (비활성화 상태)
- 파일:
middlewares/csrf.js(신규) - 상태: 구현 완료,
config/middleware.js에서 활성화 가능 - 설명: 토큰 기반 CSRF 보호 구현됨
활성화 방법 (config/middleware.js):
// 주석 해제하여 활성화
const { verifyCsrfToken, getCsrfToken } = require('../middlewares/csrf');
app.get('/api/csrf-token', getCsrfToken);
app.use('/api/', verifyCsrfToken({
ignorePaths: ['/api/auth/login', '/api/auth/register', '/api/health', '/api/csrf-token']
}));
프론트엔드 적용:
// CSRF 토큰 발급 받기
const response = await fetch('/api/csrf-token');
const { csrfToken } = await response.json();
// 요청에 토큰 포함
headers: { 'X-CSRF-Token': csrfToken }
3.3 [HIGH] JWT localStorage 저장
- 문제: XSS 공격 시 토큰 탈취 가능
- 현재:
localStorage.setItem('token', token)
권장 조치:
- 서버에서 HttpOnly 쿠키로 JWT 전송
- 또는 메모리에만 저장하고 refresh token 사용
3.4 [HIGH] 파일 업로드 보안 ✅ 수정 완료
- 파일:
utils/fileUploadSecurity.js(신규) - 상태: 구현 완료, 주요 업로드 라우트에 적용됨
구현된 기능:
// utils/fileUploadSecurity.js
const FILE_SIGNATURES = {
'ffd8ff': { mime: 'image/jpeg', ext: ['.jpg', '.jpeg'] },
'89504e47': { mime: 'image/png', ext: ['.png'] },
'47494638': { mime: 'image/gif', ext: ['.gif'] },
'25504446': { mime: 'application/pdf', ext: ['.pdf'] }
};
// Magic number로 파일 유형 검증
const validateFileByMagicNumber = async (filePath, allowedMimes) => { ... };
// 안전한 파일명 생성
const generateSafeFilename = (originalFilename) => {
const sanitized = originalFilename.replace(/[^a-zA-Z0-9.-]/g, '_');
const uniquePrefix = crypto.randomBytes(8).toString('hex');
return `${uniquePrefix}_${sanitized}`;
};
// Multer 필터로 사용
const createFileFilter = (allowedExtensions, allowedMimes) => { ... };
적용 예시 (routes/workplaceRoutes.js):
const { generateSafeFilename, createFileFilter, ALLOWED_IMAGE_EXTENSIONS } = require('../utils/fileUploadSecurity');
3.5 [MEDIUM] Rate Limiting ✅ 수정 완료
- 파일:
config/middleware.js - 상태: 활성화됨
적용된 설정:
// 일반 API: 15분당 200 요청
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 200,
message: { success: false, error: '너무 많은 요청입니다...', code: 'RATE_LIMIT_EXCEEDED' }
});
// 로그인: 15분당 10회 (브루트포스 방지)
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 10,
message: { success: false, error: '로그인 시도 횟수를 초과했습니다...', code: 'LOGIN_RATE_LIMIT_EXCEEDED' }
});
app.use('/api/', apiLimiter);
app.use('/api/auth/login', loginLimiter);
3.6 [MEDIUM] 비밀번호 정책 ✅ 수정 완료
- 파일:
utils/passwordValidator.js(신규) - 상태: 구현 완료
적용된 정책:
// utils/passwordValidator.js
const validatePassword = (password, options = {}) => {
const config = {
minLength: options.minLength || 12,
requireUppercase: options.requireUppercase !== false,
requireLowercase: options.requireLowercase !== false,
requireNumbers: options.requireNumbers !== false,
requireSpecialChars: options.requireSpecialChars !== false,
maxLength: options.maxLength || 128
};
// ... 검증 로직
};
// 미들웨어로 사용
const { passwordValidationMiddleware } = require('../utils/passwordValidator');
router.post('/register', passwordValidationMiddleware(), controller.register);
4. 백엔드 보안 가이드
4.1 인증/인가
// 모든 보호된 라우트에 인증 미들웨어 적용
const { requireAuth, requireMinLevel, requireRole } = require('../middlewares/auth');
// 읽기 작업: 인증만
router.get('/', requireAuth, controller.getAll);
// 쓰기 작업: 권한 체크
router.post('/', requireAuth, requireMinLevel('group_leader'), controller.create);
// 삭제 작업: 관리자 권한
router.delete('/:id', requireAuth, requireRole('admin'), controller.delete);
4.2 SQL Injection 방지
// BAD - 직접 문자열 삽입
const query = `SELECT * FROM users WHERE name = '${userName}'`;
// GOOD - 파라미터화된 쿼리
const query = 'SELECT * FROM users WHERE name = ?';
const [rows] = await db.execute(query, [userName]);
// 동적 컬럼명/테이블명이 필요한 경우
const allowedColumns = ['name', 'email', 'created_at'];
if (!allowedColumns.includes(sortColumn)) {
throw new Error('Invalid column name');
}
4.3 입력 검증
// express-validator 사용
const { body, param, validationResult } = require('express-validator');
router.post('/users',
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 12 }),
body('name').trim().escape().isLength({ min: 2, max: 50 }),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// 처리 로직
}
);
4.4 에러 처리
// 프로덕션에서 스택 트레이스 노출 금지
app.use((err, req, res, next) => {
logger.error(err.stack);
const response = {
error: err.message || '서버 오류가 발생했습니다.'
};
// 개발 환경에서만 상세 정보
if (process.env.NODE_ENV === 'development') {
response.stack = err.stack;
}
res.status(err.status || 500).json(response);
});
4.5 파일 업로드
const multer = require('multer');
const path = require('path');
const crypto = require('crypto');
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, path.join(__dirname, '../uploads'));
},
filename: (req, file, cb) => {
// 랜덤 파일명 생성 (원본 파일명 사용 금지)
const ext = path.extname(file.originalname).toLowerCase();
const randomName = crypto.randomBytes(16).toString('hex');
cb(null, `${randomName}${ext}`);
}
});
const fileFilter = (req, file, cb) => {
const allowedMimes = ['image/jpeg', 'image/png', 'image/gif'];
const allowedExts = ['.jpg', '.jpeg', '.png', '.gif'];
const ext = path.extname(file.originalname).toLowerCase();
const mimeOk = allowedMimes.includes(file.mimetype);
const extOk = allowedExts.includes(ext);
if (mimeOk && extOk) {
cb(null, true);
} else {
cb(new Error('허용되지 않는 파일 형식입니다.'), false);
}
};
const upload = multer({
storage,
fileFilter,
limits: { fileSize: 5 * 1024 * 1024 } // 5MB
});
5. 프론트엔드 보안 가이드
5.1 XSS 방지
// security.js 로드 필수
<script src="/js/common/security.js"></script>
// BAD - 직접 innerHTML
element.innerHTML = `<div>${userData.name}</div>`;
// GOOD - escapeHtml 사용
element.innerHTML = `<div>${escapeHtml(userData.name)}</div>`;
// BETTER - textContent 사용 (HTML이 필요 없는 경우)
element.textContent = userData.name;
// BEST - 안전한 템플릿 함수 사용
SecurityUtils.setHtmlSafe(element, '<div>{{name}}</div>', { name: userData.name });
5.2 안전한 이벤트 핸들러
// BAD - 인라인 이벤트 핸들러 (onclick 속성)
<button onclick="deleteItem(${item.id})">삭제</button>
// GOOD - 이벤트 리스너 사용
const button = document.createElement('button');
button.textContent = '삭제';
button.addEventListener('click', () => deleteItem(item.id));
5.3 URL 파라미터 검증
// BAD - 검증 없이 사용
const id = new URLSearchParams(location.search).get('id');
fetch(`/api/items/${id}`);
// GOOD - 검증 후 사용
const id = SecurityUtils.getIdParamSafe('id');
if (id === null) {
showToast('잘못된 요청입니다.', 'error');
return;
}
fetch(`/api/items/${id}`);
5.4 localStorage 사용
// 민감 정보 저장 최소화
// - 토큰: 가능하면 HttpOnly 쿠키 사용
// - 사용자 정보: 필수 정보만 저장
// BAD
localStorage.setItem('user', JSON.stringify(fullUserObject));
// GOOD
localStorage.setItem('user', JSON.stringify({
user_id: user.user_id,
name: user.name
// 민감 정보 제외: email, access_level 등
}));
5.5 API 호출 보안
// 항상 HTTPS 사용 (개발 환경 제외)
// CSRF 토큰 포함 (구현 시)
async function apiCall(endpoint, method = 'GET', data = null) {
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
// 'X-CSRF-Token': getCsrfToken() // CSRF 구현 시
};
const options = { method, headers };
if (data && method !== 'GET') {
options.body = JSON.stringify(data);
}
const response = await fetch(endpoint, options);
// 401 응답 시 로그인 페이지로 이동
if (response.status === 401) {
localStorage.removeItem('token');
window.location.href = '/pages/login.html';
return;
}
return response.json();
}
6. 배포 보안 체크리스트
6.1 환경변수
.env파일이.gitignore에 포함되어 있는가?- 모든 시크릿이 환경변수로 관리되는가?
- 프로덕션과 개발 환경의 시크릿이 분리되어 있는가?
- 기본/폴백 시크릿 값이 코드에 없는가?
6.2 인증/인가
- 모든 API 엔드포인트에 인증이 적용되어 있는가?
- 관리자 기능에 적절한 권한 체크가 있는가?
- 비밀번호 정책이 충분히 강력한가? (최소 12자, 복잡도)
- 로그인 시도 제한(Rate Limiting)이 활성화되어 있는가?
6.3 데이터 보호
- SQL Injection 방지가 모든 쿼리에 적용되어 있는가?
- XSS 방지가 모든 사용자 입력에 적용되어 있는가?
- 민감 정보가 로그에 기록되지 않는가?
- HTTPS가 강제되는가?
6.4 파일 업로드
- 파일 타입 검증이 서버에서 이루어지는가?
- 업로드 파일 크기 제한이 있는가?
- 업로드 경로가 웹 루트 외부인가?
- 실행 파일 업로드가 차단되는가?
6.5 헤더 및 설정
- 보안 헤더가 설정되어 있는가? (CSP, X-Frame-Options 등)
- CORS가 필요한 도메인만 허용하는가?
- 에러 메시지에 시스템 정보가 노출되지 않는가?
- 디버그 모드가 비활성화되어 있는가?
7. 보안 유틸리티 사용법
7.1 백엔드 - queryOptimizer.js
const {
executePagedQuery,
validateIdentifier,
validateTableName
} = require('../utils/queryOptimizer');
// 페이지네이션 쿼리 (자동 검증)
const result = await executePagedQuery(
'SELECT * FROM users WHERE status = ?',
'SELECT COUNT(*) as total FROM users WHERE status = ?',
['active'],
{ page: 1, limit: 10, orderBy: 'created_at', orderDirection: 'DESC' }
);
7.2 프론트엔드 - security.js
<!-- HTML에서 로드 -->
<script src="/js/common/security.js"></script>
// XSS 방지
const safeHtml = escapeHtml(userInput);
element.innerHTML = `<span>${safeHtml}</span>`;
// URL 파라미터 안전하게 가져오기
const id = SecurityUtils.getIdParamSafe('id');
// 안전한 JSON 파싱
const data = SecurityUtils.parseJsonSafe(jsonString, {});
// 입력 검증
if (!SecurityUtils.validateEmail(email)) {
showToast('올바른 이메일 형식이 아닙니다.', 'error');
return;
}
// 안전한 HTML 템플릿
SecurityUtils.setHtmlSafe(container,
'<div class="user">{{name}} ({{email}})</div>',
{ name: user.name, email: user.email }
);
부록: 참고 자료
- OWASP Top 10
- OWASP Cheat Sheet Series
- Node.js Security Best Practices
- Express.js Security Best Practices
새로 추가된 보안 파일
| 파일 경로 | 설명 |
|---|---|
api.hyungi.net/utils/passwordValidator.js |
비밀번호 강도 검증 유틸리티 |
api.hyungi.net/utils/fileUploadSecurity.js |
파일 업로드 보안 (Magic number 검증) |
api.hyungi.net/middlewares/csrf.js |
CSRF 보호 미들웨어 |
web-ui/js/common/security.js |
프론트엔드 보안 유틸리티 (상세 버전) |
수정된 파일
| 파일 | 수정 내용 |
|---|---|
api.hyungi.net/routes/auth.js |
bcrypt 비교 적용, 폴백 시크릿 제거 |
api.hyungi.net/routes/authRoutes.js |
강화된 비밀번호 정책 적용 |
api.hyungi.net/routes/toolsRoute.js |
인증 미들웨어 추가 |
api.hyungi.net/routes/projectRoutes.js |
인증 미들웨어 추가 |
api.hyungi.net/routes/notificationRoutes.js |
인증 미들웨어 추가 |
api.hyungi.net/routes/workplaceRoutes.js |
안전한 파일 업로드 적용 |
api.hyungi.net/routes/uploadBgRoutes.js |
파일 검증 및 인증 추가 |
api.hyungi.net/utils/queryOptimizer.js |
SQL Injection 방지 검증 추가 |
api.hyungi.net/config/middleware.js |
Rate Limiting 활성화 |
web-ui/js/api-base.js |
escapeHtml 전역 함수 추가 |
web-ui/js/tbm.js |
XSS 방지 escapeHtml 적용 |
변경 이력
| 날짜 | 버전 | 변경 내용 |
|---|---|---|
| 2026-02-04 | 1.0 | 최초 작성 |
| 2026-02-04 | 1.1 | 보안 취약점 수정 및 유틸리티 추가 |