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:
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user