Files
TK-FB-Project/docs/SECURITY_GUIDE.md
Hyungi Ahn 36f110c90a 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>
2026-02-05 06:33:10 +09:00

626 lines
18 KiB
Markdown

# 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 = `<option>${data.name}</option>`;
// After (안전)
element.innerHTML = `<option>${escapeHtml(data.name)}</option>`;
// 숫자 값도 검증
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 로드 필수
<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 안전한 이벤트 핸들러
```javascript
// 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 파라미터 검증
```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
<!-- HTML에서 로드 -->
<script src="/js/common/security.js"></script>
```
```javascript
// 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](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 | 보안 취약점 수정 및 유틸리티 추가 |