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