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

@@ -146,11 +146,17 @@ function renderBasicInfo(d) {
});
};
const validTypes = ['nonconformity', 'safety'];
const safeType = validTypes.includes(d.category_type) ? d.category_type : '';
const reporterName = escapeHtml(d.reporter_full_name || d.reporter_name || '-');
const locationText = escapeHtml(d.custom_location || d.workplace_name || '-');
const factoryText = d.factory_name ? ` (${escapeHtml(d.factory_name)})` : '';
container.innerHTML = `
<div class="info-item">
<div class="info-label">신고 유형</div>
<div class="info-value">
<span class="type-badge ${d.category_type}">${typeNames[d.category_type] || d.category_type}</span>
<span class="type-badge ${safeType}">${typeNames[d.category_type] || escapeHtml(d.category_type || '-')}</span>
</div>
</div>
<div class="info-item">
@@ -159,11 +165,11 @@ function renderBasicInfo(d) {
</div>
<div class="info-item">
<div class="info-label">신고자</div>
<div class="info-value">${d.reporter_full_name || d.reporter_name || '-'}</div>
<div class="info-value">${reporterName}</div>
</div>
<div class="info-item">
<div class="info-label">위치</div>
<div class="info-value">${d.custom_location || d.workplace_name || '-'}${d.factory_name ? ` (${d.factory_name})` : ''}</div>
<div class="info-value">${locationText}${factoryText}</div>
</div>
`;
}
@@ -174,17 +180,20 @@ function renderBasicInfo(d) {
function renderIssueContent(d) {
const container = document.getElementById('issueContent');
const validSeverities = ['critical', 'high', 'medium', 'low'];
const safeSeverity = validSeverities.includes(d.severity) ? d.severity : '';
let html = `
<div class="info-grid" style="margin-bottom: 1rem;">
<div class="info-item">
<div class="info-label">카테고리</div>
<div class="info-value">${d.issue_category_name || '-'}</div>
<div class="info-value">${escapeHtml(d.issue_category_name || '-')}</div>
</div>
<div class="info-item">
<div class="info-label">항목</div>
<div class="info-value">
${d.issue_item_name || '-'}
${d.severity ? `<span class="severity-badge ${d.severity}">${severityNames[d.severity]}</span>` : ''}
${escapeHtml(d.issue_item_name || '-')}
${d.severity ? `<span class="severity-badge ${safeSeverity}">${severityNames[d.severity] || escapeHtml(d.severity)}</span>` : ''}
</div>
</div>
</div>
@@ -262,11 +271,11 @@ function renderProcessInfo(d) {
html += `
<div class="info-item">
<div class="info-label">담당자</div>
<div class="info-value">${d.assigned_full_name || d.assigned_user_name || '-'}</div>
<div class="info-value">${escapeHtml(d.assigned_full_name || d.assigned_user_name || '-')}</div>
</div>
<div class="info-item">
<div class="info-label">담당 부서</div>
<div class="info-value">${d.assigned_department || '-'}</div>
<div class="info-value">${escapeHtml(d.assigned_department || '-')}</div>
</div>
`;
}
@@ -279,7 +288,7 @@ function renderProcessInfo(d) {
</div>
<div class="info-item">
<div class="info-label">처리자</div>
<div class="info-value">${d.resolved_by_name || '-'}</div>
<div class="info-value">${escapeHtml(d.resolved_by_name || '-')}</div>
</div>
`;
}
@@ -402,10 +411,10 @@ function renderStatusTimeline(logs) {
container.innerHTML = logs.map(log => `
<div class="timeline-item">
<div class="timeline-status">
${log.previous_status ? `${statusNames[log.previous_status]}` : ''}${statusNames[log.new_status]}
${log.previous_status ? `${statusNames[log.previous_status] || escapeHtml(log.previous_status)}` : ''}${statusNames[log.new_status] || escapeHtml(log.new_status)}
</div>
<div class="timeline-meta">
${log.changed_by_full_name || log.changed_by_name} | ${formatDate(log.changed_at)}
${escapeHtml(log.changed_by_full_name || log.changed_by_name || '-')} | ${formatDate(log.changed_at)}
${log.change_reason ? `<br><small>${escapeHtml(log.change_reason)}</small>` : ''}
</div>
</div>
@@ -530,7 +539,8 @@ async function openAssignModal() {
if (data.success && data.data) {
data.data.forEach(user => {
select.innerHTML += `<option value="${user.user_id}">${user.name} (${user.username})</option>`;
const safeUserId = parseInt(user.user_id) || 0;
select.innerHTML += `<option value="${safeUserId}">${escapeHtml(user.name || '-')} (${escapeHtml(user.username || '-')})</option>`;
});
}
}