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:
@@ -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, '"')}", "${(cat.layout_image || '').replace(/"/g, '"')}")'>
|
||||
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, '"')}", "${safeImage.replace(/"/g, '"')}")'>
|
||||
<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, '"')}")'>
|
||||
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, '"')}")'>
|
||||
<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">
|
||||
|
||||
Reference in New Issue
Block a user