## 백엔드 보안 수정 - 하드코딩된 비밀번호 및 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>
626 lines
18 KiB
Markdown
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 | 보안 취약점 수정 및 유틸리티 추가 |
|