# TK-FB-Project 보안 가이드 > 최종 업데이트: 2026-02-04 > 작성자: TK-FB-Project Security Review 이 문서는 TK-FB-Project의 보안 취약점 분석 결과와 개발 시 준수해야 할 보안 가이드라인을 정리한 것입니다. --- ## 목차 1. [보안 취약점 요약](#1-보안-취약점-요약) 2. [수정 완료된 취약점](#2-수정-완료된-취약점) 3. [추가 조치 필요 항목](#3-추가-조치-필요-항목) 4. [백엔드 보안 가이드](#4-백엔드-보안-가이드) 5. [프론트엔드 보안 가이드](#5-프론트엔드-보안-가이드) 6. [배포 보안 체크리스트](#6-배포-보안-체크리스트) 7. [보안 유틸리티 사용법](#7-보안-유틸리티-사용법) --- ## 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() 사용으로 변경 ```javascript // 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'` - **해결**: 폴백 제거 및 환경변수 필수화 ```javascript // Before (취약) process.env.JWT_SECRET || 'your-secret-key' // After (수정됨) process.env.JWT_SECRET // 미설정 시 경고 로그 출력 ``` ### 2.3 [HIGH] 인증 미적용 API 라우트 - **파일들**: - `routes/toolsRoute.js` - `routes/projectRoutes.js` - `routes/notificationRoutes.js` - **해결**: `requireAuth`, `requireMinLevel` 미들웨어 적용 ```javascript // 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, 테이블명 직접 삽입 - **해결**: 화이트리스트 검증 함수 추가 ```javascript // 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.js` - `modern-dashboard.js` - `work-report-calendar.js` - 기타 innerHTML을 사용하는 파일들 (우선순위에 따라 점진적 수정 권장) **수정 패턴**: ```javascript // Before (취약) element.innerHTML = ``; // After (안전) element.innerHTML = ``; // 숫자 값도 검증 onclick="handler(${parseInt(data.id) || 0})" ``` **조치 방법**: 1. `escapeHtml()`은 `api-base.js`에서 전역으로 제공됨 2. 모든 innerHTML에서 사용자/API 데이터에 `escapeHtml()` 적용 3. onclick 핸들러의 ID 값은 `parseInt()` 사용 4. 검색 패턴: `\.innerHTML\s*=.*\$\{` 로 취약점 찾기 ### 3.2 [HIGH] CSRF 보호 ✅ 구현 완료 (비활성화 상태) - **파일**: `middlewares/csrf.js` (신규) - **상태**: 구현 완료, `config/middleware.js`에서 활성화 가능 - **설명**: 토큰 기반 CSRF 보호 구현됨 **활성화 방법** (`config/middleware.js`): ```javascript // 주석 해제하여 활성화 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'] })); ``` **프론트엔드 적용**: ```javascript // 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` (신규) - **상태**: 구현 완료, 주요 업로드 라우트에 적용됨 **구현된 기능**: ```javascript // 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`): ```javascript const { generateSafeFilename, createFileFilter, ALLOWED_IMAGE_EXTENSIONS } = require('../utils/fileUploadSecurity'); ``` ### 3.5 [MEDIUM] Rate Limiting ✅ 수정 완료 - **파일**: `config/middleware.js` - **상태**: 활성화됨 **적용된 설정**: ```javascript // 일반 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` (신규) - **상태**: 구현 완료 **적용된 정책**: ```javascript // 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 인증/인가 ```javascript // 모든 보호된 라우트에 인증 미들웨어 적용 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 방지 ```javascript // 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 입력 검증 ```javascript // 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 에러 처리 ```javascript // 프로덕션에서 스택 트레이스 노출 금지 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 파일 업로드 ```javascript 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 방지 ```javascript // security.js 로드 필수 // BAD - 직접 innerHTML element.innerHTML = `
${userData.name}
`; // GOOD - escapeHtml 사용 element.innerHTML = `
${escapeHtml(userData.name)}
`; // BETTER - textContent 사용 (HTML이 필요 없는 경우) element.textContent = userData.name; // BEST - 안전한 템플릿 함수 사용 SecurityUtils.setHtmlSafe(element, '
{{name}}
', { name: userData.name }); ``` ### 5.2 안전한 이벤트 핸들러 ```javascript // BAD - 인라인 이벤트 핸들러 (onclick 속성) // GOOD - 이벤트 리스너 사용 const button = document.createElement('button'); button.textContent = '삭제'; button.addEventListener('click', () => deleteItem(item.id)); ``` ### 5.3 URL 파라미터 검증 ```javascript // 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 사용 ```javascript // 민감 정보 저장 최소화 // - 토큰: 가능하면 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 호출 보안 ```javascript // 항상 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 ```javascript 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 ``` ```javascript // XSS 방지 const safeHtml = escapeHtml(userInput); element.innerHTML = `${safeHtml}`; // URL 파라미터 안전하게 가져오기 const id = SecurityUtils.getIdParamSafe('id'); // 안전한 JSON 파싱 const data = SecurityUtils.parseJsonSafe(jsonString, {}); // 입력 검증 if (!SecurityUtils.validateEmail(email)) { showToast('올바른 이메일 형식이 아닙니다.', 'error'); return; } // 안전한 HTML 템플릿 SecurityUtils.setHtmlSafe(container, '
{{name}} ({{email}})
', { name: user.name, email: user.email } ); ``` --- ## 부록: 참고 자료 - [OWASP Top 10](https://owasp.org/www-project-top-ten/) - [OWASP Cheat Sheet Series](https://cheatsheetseries.owasp.org/) - [Node.js Security Best Practices](https://nodejs.org/en/docs/guides/security/) - [Express.js Security Best Practices](https://expressjs.com/en/advanced/best-practice-security.html) --- --- ## 새로 추가된 보안 파일 | 파일 경로 | 설명 | |-----------|------| | `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 | 보안 취약점 수정 및 유틸리티 추가 |