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

@@ -69,29 +69,34 @@ function renderDepartmentList() {
return;
}
container.innerHTML = departments.map(dept => `
<div class="department-item ${currentDepartmentId === dept.department_id ? 'active' : ''}"
onclick="selectDepartment(${dept.department_id})">
<div class="department-info">
<span class="department-name">${dept.department_name}</span>
<span class="department-count">${dept.worker_count || 0}명</span>
container.innerHTML = departments.map(dept => {
const safeDeptId = parseInt(dept.department_id) || 0;
const safeDeptName = escapeHtml(dept.department_name || '-');
const workerCount = parseInt(dept.worker_count) || 0;
return `
<div class="department-item ${currentDepartmentId === dept.department_id ? 'active' : ''}"
onclick="selectDepartment(${safeDeptId})">
<div class="department-info">
<span class="department-name">${safeDeptName}</span>
<span class="department-count">${workerCount}명</span>
</div>
<div class="department-actions" onclick="event.stopPropagation()">
<button class="btn-icon" onclick="editDepartment(${safeDeptId})" title="수정">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
</button>
<button class="btn-icon danger" onclick="confirmDeleteDepartment(${safeDeptId})" title="삭제">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
</svg>
</button>
</div>
</div>
<div class="department-actions" onclick="event.stopPropagation()">
<button class="btn-icon" onclick="editDepartment(${dept.department_id})" title="수정">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
</button>
<button class="btn-icon danger" onclick="confirmDeleteDepartment(${dept.department_id})" title="삭제">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
</svg>
</button>
</div>
</div>
`).join('');
`;
}).join('');
}
// 부서 선택
@@ -117,7 +122,10 @@ function updateParentDepartmentSelect() {
select.innerHTML = '<option value="">없음 (최상위 부서)</option>' +
departments
.filter(d => d.department_id !== parseInt(currentId))
.map(d => `<option value="${d.department_id}">${d.department_name}</option>`)
.map(d => {
const safeDeptId = parseInt(d.department_id) || 0;
return `<option value="${safeDeptId}">${escapeHtml(d.department_name || '-')}</option>`;
})
.join('');
}
@@ -316,7 +324,8 @@ function renderWorkerList() {
'leader': '그룹장',
'admin': '관리자'
};
const jobType = jobTypeMap[worker.job_type] || worker.job_type || '-';
const safeJobType = ['worker', 'leader', 'admin'].includes(worker.job_type) ? worker.job_type : '';
const jobType = jobTypeMap[safeJobType] || escapeHtml(worker.job_type || '-');
const isInactive = worker.status === 'inactive';
const isResigned = worker.employment_status === 'resigned';
@@ -332,12 +341,16 @@ function renderWorkerList() {
statusText = '사무직';
}
const safeWorkerId = parseInt(worker.worker_id) || 0;
const safeWorkerName = escapeHtml(worker.worker_name || '');
const firstChar = safeWorkerName ? safeWorkerName.charAt(0) : '?';
return `
<tr style="${isResigned ? 'opacity: 0.6;' : ''}">
<td>
<div class="worker-name-cell">
<div class="worker-avatar">${worker.worker_name.charAt(0)}</div>
<span style="font-weight: 500;">${worker.worker_name}</span>
<div class="worker-avatar">${firstChar}</div>
<span style="font-weight: 500;">${safeWorkerName}</span>
</div>
</td>
<td>${jobType}</td>
@@ -350,8 +363,8 @@ function renderWorkerList() {
</td>
<td>
<div style="display: flex; gap: 0.25rem; justify-content: center;">
<button class="btn-icon" onclick="editWorker(${worker.worker_id})" title="수정"></button>
<button class="btn-icon danger" onclick="confirmDeleteWorker(${worker.worker_id})" title="삭제">🗑</button>
<button class="btn-icon" onclick="editWorker(${safeWorkerId})" title="수정"></button>
<button class="btn-icon danger" onclick="confirmDeleteWorker(${safeWorkerId})" title="삭제">🗑</button>
</div>
</td>
</tr>