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>
This commit is contained in:
Hyungi Ahn
2026-02-05 06:33:10 +09:00
parent 7c38c555f5
commit 36f110c90a
97 changed files with 2523 additions and 24267 deletions

View File

@@ -2,6 +2,41 @@
const { getDb } = require('../dbPool');
/**
* SQL Injection 방지를 위한 화이트리스트 검증
*/
const ALLOWED_ORDER_DIRECTIONS = ['ASC', 'DESC'];
const ALLOWED_TABLE_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
const ALLOWED_COLUMN_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_.]*$/;
const validateOrderDirection = (direction) => {
const normalized = (direction || 'DESC').toUpperCase();
if (!ALLOWED_ORDER_DIRECTIONS.includes(normalized)) {
throw new Error(`Invalid order direction: ${direction}`);
}
return normalized;
};
const validateIdentifier = (identifier, type = 'column') => {
if (!identifier || typeof identifier !== 'string') {
throw new Error(`Invalid ${type} name`);
}
if (!ALLOWED_COLUMN_NAME_PATTERN.test(identifier)) {
throw new Error(`Invalid ${type} name: ${identifier}`);
}
return identifier;
};
const validateTableName = (tableName) => {
if (!tableName || typeof tableName !== 'string') {
throw new Error('Invalid table name');
}
if (!ALLOWED_TABLE_NAME_PATTERN.test(tableName)) {
throw new Error(`Invalid table name: ${tableName}`);
}
return tableName;
};
/**
* 페이지네이션 헬퍼
*/
@@ -24,6 +59,10 @@ const executePagedQuery = async (baseQuery, countQuery, params = [], options = {
const { page = 1, limit = 10, orderBy = 'id', orderDirection = 'DESC' } = options;
const { limit: limitNum, offset, page: pageNum } = paginate(page, limit);
// SQL Injection 방지: 컬럼명과 정렬방향 검증
const safeOrderBy = validateIdentifier(orderBy, 'column');
const safeOrderDirection = validateOrderDirection(orderDirection);
try {
const db = await getDb();
@@ -31,8 +70,8 @@ const executePagedQuery = async (baseQuery, countQuery, params = [], options = {
const [countResult] = await db.execute(countQuery, params);
const totalCount = countResult[0]?.total || 0;
// 데이터 조회 (ORDER BY와 LIMIT 추가)
const pagedQuery = `${baseQuery} ORDER BY ${orderBy} ${orderDirection} LIMIT ${limitNum} OFFSET ${offset}`;
// 데이터 조회 (ORDER BY와 LIMIT 추가) - 검증된 값만 사용
const pagedQuery = `${baseQuery} ORDER BY ${safeOrderBy} ${safeOrderDirection} LIMIT ${limitNum} OFFSET ${offset}`;
const [rows] = await db.execute(pagedQuery, params);
// 페이지네이션 메타데이터 계산
@@ -59,14 +98,17 @@ const executePagedQuery = async (baseQuery, countQuery, params = [], options = {
* 인덱스 최적화 제안
*/
const suggestIndexes = async (tableName) => {
// SQL Injection 방지: 테이블명 검증
const safeTableName = validateTableName(tableName);
try {
const db = await getDb();
// 현재 인덱스 조회
const [indexes] = await db.execute(`SHOW INDEX FROM ${tableName}`);
// 현재 인덱스 조회 - 검증된 테이블명 사용
const [indexes] = await db.execute(`SHOW INDEX FROM \`${safeTableName}\``);
// 테이블 구조 조회
const [columns] = await db.execute(`DESCRIBE ${tableName}`);
// 테이블 구조 조회 - 검증된 테이블명 사용
const [columns] = await db.execute(`DESCRIBE \`${safeTableName}\``);
const suggestions = [];
@@ -80,7 +122,7 @@ const suggestIndexes = async (tableName) => {
type: 'INDEX',
column: col.Field,
reason: '외래키 컬럼에 인덱스 추가로 JOIN 성능 향상',
sql: `CREATE INDEX idx_${tableName}_${col.Field} ON ${tableName}(${col.Field});`
sql: `CREATE INDEX idx_${safeTableName}_${col.Field} ON \`${safeTableName}\`(\`${col.Field}\`);`
});
});
@@ -95,12 +137,12 @@ const suggestIndexes = async (tableName) => {
type: 'INDEX',
column: col.Field,
reason: '날짜 범위 검색 성능 향상',
sql: `CREATE INDEX idx_${tableName}_${col.Field} ON ${tableName}(${col.Field});`
sql: `CREATE INDEX idx_${safeTableName}_${col.Field} ON \`${safeTableName}\`(\`${col.Field}\`);`
});
});
return {
tableName,
tableName: safeTableName,
currentIndexes: indexes.map(idx => ({
name: idx.Key_name,
column: idx.Column_name,
@@ -179,6 +221,9 @@ const batchInsert = async (tableName, data, batchSize = 100) => {
throw new Error('삽입할 데이터가 없습니다.');
}
// SQL Injection 방지: 테이블명 검증
const safeTableName = validateTableName(tableName);
try {
const db = await getDb();
const connection = await db.getConnection();
@@ -186,8 +231,11 @@ const batchInsert = async (tableName, data, batchSize = 100) => {
await connection.beginTransaction();
const columns = Object.keys(data[0]);
const placeholders = columns.map(() => '?').join(', ');
const insertQuery = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders})`;
// 컬럼명도 검증
const safeColumns = columns.map(col => validateIdentifier(col, 'column'));
const placeholders = safeColumns.map(() => '?').join(', ');
const columnList = safeColumns.map(col => `\`${col}\``).join(', ');
const insertQuery = `INSERT INTO \`${safeTableName}\` (${columnList}) VALUES (${placeholders})`;
let insertedCount = 0;