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

18 KiB

TK-FB-Project 보안 가이드

최종 업데이트: 2026-02-04 작성자: TK-FB-Project Security Review

이 문서는 TK-FB-Project의 보안 취약점 분석 결과와 개발 시 준수해야 할 보안 가이드라인을 정리한 것입니다.


목차

  1. 보안 취약점 요약
  2. 수정 완료된 취약점
  3. 추가 조치 필요 항목
  4. 백엔드 보안 가이드
  5. 프론트엔드 보안 가이드
  6. 배포 보안 체크리스트
  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() 사용으로 변경
// 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.js
    • routes/projectRoutes.js
    • routes/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.js
  • modern-dashboard.js
  • work-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})"

조치 방법:

  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):

// 주석 해제하여 활성화
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 }
);

부록: 참고 자료



새로 추가된 보안 파일

파일 경로 설명
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 보안 취약점 수정 및 유틸리티 추가