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

@@ -843,22 +843,22 @@ window.addManualWorkRow = function() {
<td>
<select class="form-input-compact" id="worker_${manualIndex}" style="width: 120px;" required>
<option value="">작업자 선택</option>
${workers.map(w => `<option value="${w.worker_id}">${w.worker_name} (${w.job_type || '-'})</option>`).join('')}
${workers.map(w => `<option value="${escapeHtml(String(w.worker_id))}">${escapeHtml(w.worker_name)} (${escapeHtml(w.job_type || '-')})</option>`).join('')}
</select>
</td>
<td>
<input type="date" class="form-input-compact" id="date_${manualIndex}" value="${getKoreaToday()}" required style="width: 130px;">
<input type="date" class="form-input-compact" id="date_${manualIndex}" value="${escapeHtml(getKoreaToday())}" required style="width: 130px;">
</td>
<td>
<select class="form-input-compact" id="project_${manualIndex}" style="width: 120px;" required>
<option value="">프로젝트 선택</option>
${projects.map(p => `<option value="${p.project_id}">${p.project_name}</option>`).join('')}
${projects.map(p => `<option value="${escapeHtml(String(p.project_id))}">${escapeHtml(p.project_name)}</option>`).join('')}
</select>
</td>
<td>
<select class="form-input-compact" id="workType_${manualIndex}" style="width: 120px;" required onchange="loadTasksForWorkType('${manualIndex}')">
<option value="">공정 선택</option>
${workTypes.map(wt => `<option value="${wt.id}">${wt.name}</option>`).join('')}
${workTypes.map(wt => `<option value="${escapeHtml(String(wt.id))}">${escapeHtml(wt.name)}</option>`).join('')}
</select>
</td>
<td>
@@ -969,7 +969,7 @@ window.loadTasksForWorkType = async function(manualIndex) {
taskSelect.disabled = false;
taskSelect.innerHTML = `
<option value="">작업 선택</option>
${tasks.map(task => `<option value="${task.task_id}">${task.task_name}</option>`).join('')}
${tasks.map(task => `<option value="${escapeHtml(String(task.task_id))}">${escapeHtml(task.task_name)}</option>`).join('')}
`;
} else {
taskSelect.disabled = true;
@@ -1022,12 +1022,17 @@ window.openWorkplaceMapForManual = async function(manualIndex) {
const modal = document.getElementById('workplaceModal');
const categoryList = document.getElementById('workplaceCategoryList');
categoryList.innerHTML = categories.map(cat => `
<button type="button" class="btn btn-secondary" style="width: 100%; text-align: left;" onclick='selectWorkplaceCategory(${cat.category_id}, "${cat.category_name.replace(/"/g, '&quot;')}", "${(cat.layout_image || '').replace(/"/g, '&quot;')}")'>
categoryList.innerHTML = categories.map(cat => {
const safeId = parseInt(cat.category_id) || 0;
const safeName = escapeHtml(cat.category_name);
const safeImage = escapeHtml(cat.layout_image || '');
return `
<button type="button" class="btn btn-secondary" style="width: 100%; text-align: left;" onclick='selectWorkplaceCategory(${safeId}, "${safeName.replace(/"/g, '&quot;')}", "${safeImage.replace(/"/g, '&quot;')}")'>
<span style="margin-right: 0.5rem;">🏭</span>
${cat.category_name}
${safeName}
</button>
`).join('');
`;
}).join('');
// 카테고리 선택 화면 표시
document.getElementById('categorySelectionArea').style.display = 'block';
@@ -1071,12 +1076,16 @@ window.selectWorkplaceCategory = async function(categoryId, categoryName, layout
// 리스트 항상 표시
const workplaceListArea = document.getElementById('workplaceListArea');
workplaceListArea.innerHTML = workplaces.map(wp => `
<button type="button" id="workplace-${wp.workplace_id}" class="btn btn-secondary" style="width: 100%; text-align: left;" onclick='selectWorkplaceFromList(${wp.workplace_id}, "${wp.workplace_name.replace(/"/g, '&quot;')}")'>
workplaceListArea.innerHTML = workplaces.map(wp => {
const safeId = parseInt(wp.workplace_id) || 0;
const safeName = escapeHtml(wp.workplace_name);
return `
<button type="button" id="workplace-${safeId}" class="btn btn-secondary" style="width: 100%; text-align: left;" onclick='selectWorkplaceFromList(${safeId}, "${safeName.replace(/"/g, '&quot;')}")'>
<span style="margin-right: 0.5rem;">📍</span>
${wp.workplace_name}
${safeName}
</button>
`).join('');
`;
}).join('');
} catch (error) {
console.error('작업장소 로드 오류:', error);
@@ -1270,8 +1279,8 @@ window.confirmWorkplaceSelection = function() {
<span>작업장소 선택됨</span>
</div>
<div style="font-size: 0.8rem; color: #111827; font-weight: 500;">
<div style="color: #6b7280; font-size: 0.7rem; margin-bottom: 2px;">🏭 ${selectedWorkplaceCategoryName}</div>
<div>📍 ${selectedWorkplaceName}</div>
<div style="color: #6b7280; font-size: 0.7rem; margin-bottom: 2px;">🏭 ${escapeHtml(selectedWorkplaceCategoryName)}</div>
<div>📍 ${escapeHtml(selectedWorkplaceName)}</div>
</div>
`;
displayDiv.style.background = '#ecfdf5';
@@ -1482,49 +1491,49 @@ function renderCompletedReports(reports) {
<div class="completed-report-card">
<div class="report-header">
<div>
<h4>${report.worker_name || '작업자'}</h4>
<h4>${escapeHtml(report.worker_name || '작업자')}</h4>
${report.tbm_session_id ? '<span class="tbm-badge">TBM 연동</span>' : '<span class="manual-badge">수동 입력</span>'}
</div>
<span class="report-date">${formatDate(report.report_date)}</span>
<span class="report-date">${escapeHtml(formatDate(report.report_date))}</span>
</div>
<div class="report-info">
<div class="info-row">
<span class="label">프로젝트:</span>
<span class="value">${report.project_name || '-'}</span>
<span class="value">${escapeHtml(report.project_name || '-')}</span>
</div>
<div class="info-row">
<span class="label">공정:</span>
<span class="value">${report.work_type_name || '-'}</span>
<span class="value">${escapeHtml(report.work_type_name || '-')}</span>
</div>
<div class="info-row">
<span class="label">작업시간:</span>
<span class="value">${report.total_hours || report.work_hours || 0}시간</span>
<span class="value">${parseFloat(report.total_hours || report.work_hours || 0)}시간</span>
</div>
${report.regular_hours !== undefined && report.regular_hours !== null ? `
<div class="info-row">
<span class="label">정규 시간:</span>
<span class="value">${report.regular_hours}시간</span>
<span class="value">${parseFloat(report.regular_hours)}시간</span>
</div>
` : ''}
${report.error_hours && report.error_hours > 0 ? `
<div class="info-row">
<span class="label">부적합 처리:</span>
<span class="value" style="color: #dc2626;">${report.error_hours}시간</span>
<span class="value" style="color: #dc2626;">${parseFloat(report.error_hours)}시간</span>
</div>
<div class="info-row">
<span class="label">부적합 원인:</span>
<span class="value">${report.error_type_name || '-'}</span>
<span class="value">${escapeHtml(report.error_type_name || '-')}</span>
</div>
` : ''}
<div class="info-row">
<span class="label">작성자:</span>
<span class="value">${report.created_by_name || '-'}</span>
<span class="value">${escapeHtml(report.created_by_name || '-')}</span>
</div>
${report.start_time && report.end_time ? `
<div class="info-row">
<span class="label">작업 시간:</span>
<span class="value">${report.start_time} ~ ${report.end_time}</span>
<span class="value">${escapeHtml(report.start_time)} ~ ${escapeHtml(report.end_time)}</span>
</div>
` : ''}
</div>
@@ -1972,10 +1981,10 @@ function addWorkEntry() {
</div>
<select class="form-select project-select" required>
<option value="">프로젝트를 선택하세요</option>
${projects.map(p => `<option value="${p.project_id}">${p.project_name}</option>`).join('')}
${projects.map(p => `<option value="${escapeHtml(String(p.project_id))}">${escapeHtml(p.project_name)}</option>`).join('')}
</select>
</div>
<div class="form-field-group">
<div class="form-field-label">
<span class="form-field-icon">⚙️</span>
@@ -1983,7 +1992,7 @@ function addWorkEntry() {
</div>
<select class="form-select work-type-select" required>
<option value="">작업 유형을 선택하세요</option>
${workTypes.map(wt => `<option value="${wt.id}">${wt.name}</option>`).join('')}
${workTypes.map(wt => `<option value="${escapeHtml(String(wt.id))}">${escapeHtml(wt.name)}</option>`).join('')}
</select>
</div>
</div>
@@ -1996,7 +2005,7 @@ function addWorkEntry() {
</div>
<select class="form-select work-status-select" required>
<option value="">업무 상태를 선택하세요</option>
${workStatusTypes.map(ws => `<option value="${ws.id}">${ws.name}</option>`).join('')}
${workStatusTypes.map(ws => `<option value="${escapeHtml(String(ws.id))}">${escapeHtml(ws.name)}</option>`).join('')}
</select>
</div>
</div>
@@ -2422,7 +2431,7 @@ function displayMyDailyWorkers(data, date) {
const headerHtml = `
<div class="daily-workers-header">
<h4>📊 내가 입력한 오늘(${date}) 작업 현황 - 총 ${totalWorkers}명, ${totalWorks}개 작업</h4>
<h4>📊 내가 입력한 오늘(${escapeHtml(date)}) 작업 현황 - 총 ${parseInt(totalWorkers) || 0}명, ${parseInt(totalWorks) || 0}개 작업</h4>
<button class="refresh-btn" onclick="refreshTodayWorkers()">
🔄 새로고침
</button>
@@ -2436,12 +2445,12 @@ function displayMyDailyWorkers(data, date) {
// 개별 작업 항목들 (수정/삭제 버튼 포함)
const individualWorksHtml = works.map((work) => {
const projectName = work.project_name || '미지정';
const workTypeName = work.work_type_name || '미지정';
const workStatusName = work.work_status_name || '미지정';
const workHours = work.work_hours || 0;
const errorTypeName = work.error_type_name || null;
const workId = work.id;
const projectName = escapeHtml(work.project_name || '미지정');
const workTypeName = escapeHtml(work.work_type_name || '미지정');
const workStatusName = escapeHtml(work.work_status_name || '미지정');
const workHours = parseFloat(work.work_hours || 0);
const errorTypeName = work.error_type_name ? escapeHtml(work.error_type_name) : null;
const workId = parseInt(work.id) || 0;
return `
<div class="individual-work-item">
@@ -2484,8 +2493,8 @@ function displayMyDailyWorkers(data, date) {
return `
<div class="worker-status-item">
<div class="worker-header">
<div class="worker-name">👤 ${workerName}</div>
<div class="worker-total-hours">총 ${totalHours}시간</div>
<div class="worker-name">👤 ${escapeHtml(workerName)}</div>
<div class="worker-total-hours">총 ${parseFloat(totalHours)}시간</div>
</div>
<div class="individual-works-container">
${individualWorksHtml}
@@ -2981,8 +2990,8 @@ function renderInlineDefectList(index) {
let html = `
<div class="defect-issue-section">
<div class="defect-issue-header">
<span class="defect-issue-title">📋 ${workerWorkplaceName || '작업장소'} 관련 부적합</span>
<span class="defect-issue-count">${nonconformityIssues.length}건</span>
<span class="defect-issue-title">📋 ${escapeHtml(workerWorkplaceName || '작업장소')} 관련 부적합</span>
<span class="defect-issue-count">${parseInt(nonconformityIssues.length) || 0}건</span>
</div>
<div class="defect-issue-list">
`;
@@ -2999,23 +3008,24 @@ function renderInlineDefectList(index) {
itemText = itemText ? `${itemText} - ${issue.additional_description}` : issue.additional_description;
}
const safeReportId = parseInt(issue.report_id) || 0;
html += `
<div class="defect-issue-item ${isSelected ? 'selected' : ''}" data-issue-id="${issue.report_id}">
<div class="defect-issue-item ${isSelected ? 'selected' : ''}" data-issue-id="${safeReportId}">
<div class="defect-issue-checkbox">
<input type="checkbox"
id="issueCheck_${index}_${issue.report_id}"
id="issueCheck_${index}_${safeReportId}"
${isSelected ? 'checked' : ''}
onchange="toggleIssueDefect('${index}', ${issue.report_id}, this.checked)">
onchange="toggleIssueDefect('${index}', ${safeReportId}, this.checked)">
</div>
<div class="defect-issue-info">
<span class="defect-issue-category">${issue.issue_category_name || '부적합'}</span>
<span class="defect-issue-item-name">${itemText || '-'}</span>
<span class="defect-issue-location">${issue.workplace_name || issue.custom_location || ''}</span>
<span class="defect-issue-category">${escapeHtml(issue.issue_category_name || '부적합')}</span>
<span class="defect-issue-item-name">${escapeHtml(itemText || '-')}</span>
<span class="defect-issue-location">${escapeHtml(issue.workplace_name || issue.custom_location || '')}</span>
</div>
<div class="defect-issue-time ${isSelected ? 'active' : ''}">
<div class="defect-time-input ${isSelected ? '' : 'disabled'}"
onclick="${isSelected ? `openIssueDefectTimePicker('${index}', ${issue.report_id})` : ''}">
<span class="defect-time-value" id="issueDefectTime_${index}_${issue.report_id}">${defectHours}</span>
onclick="${isSelected ? `openIssueDefectTimePicker('${index}', ${safeReportId})` : ''}">
<span class="defect-time-value" id="issueDefectTime_${index}_${safeReportId}">${parseFloat(defectHours) || 0}</span>
<span class="defect-time-unit">시간</span>
</div>
</div>
@@ -3052,7 +3062,7 @@ function renderInlineDefectList(index) {
} else {
// 이슈가 없으면 레거시 UI (error_types 선택)
const noIssueMsg = workerWorkplaceName
? `${workerWorkplaceName}에 신고된 부적합이 없습니다.`
? `${escapeHtml(workerWorkplaceName)}에 신고된 부적합이 없습니다.`
: '신고된 부적합이 없습니다.';
listContainer.innerHTML = `
<div class="defect-no-issues">