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

@@ -493,13 +493,14 @@ function createSessionCard(session) {
}[session.status] || '';
// 작업 책임자 표시 (leader_name이 있으면 표시, 없으면 created_by_name 표시)
const leaderName = session.leader_name || session.created_by_name || '작업 책임자';
const leaderRole = session.leader_name
const leaderName = escapeHtml(session.leader_name || session.created_by_name || '작업 책임자');
const leaderRole = escapeHtml(session.leader_name
? (session.leader_job_type || '작업자')
: '관리자';
: '관리자');
const safeSessionId = parseInt(session.session_id) || 0;
return `
<div class="tbm-session-card" onclick="viewTbmSession(${session.session_id})">
<div class="tbm-session-card" onclick="viewTbmSession(${safeSessionId})">
<div class="tbm-card-header">
<div class="tbm-card-header-top">
<div>
@@ -512,7 +513,7 @@ function createSessionCard(session) {
</div>
<div class="tbm-card-date">
<span>&#128197;</span>
${formatDate(session.session_date)} ${session.start_time ? '| ' + session.start_time : ''}
${escapeHtml(formatDate(session.session_date))} ${session.start_time ? '| ' + escapeHtml(session.start_time) : ''}
</div>
</div>
@@ -520,29 +521,29 @@ function createSessionCard(session) {
<div class="tbm-card-info-grid">
<div class="tbm-card-info-item">
<span class="tbm-card-info-label">프로젝트</span>
<span class="tbm-card-info-value">${session.project_name || '-'}</span>
<span class="tbm-card-info-value">${escapeHtml(session.project_name || '-')}</span>
</div>
<div class="tbm-card-info-item">
<span class="tbm-card-info-label">공정</span>
<span class="tbm-card-info-value">${session.work_type_name || '-'}</span>
<span class="tbm-card-info-value">${escapeHtml(session.work_type_name || '-')}</span>
</div>
<div class="tbm-card-info-item">
<span class="tbm-card-info-label">작업장</span>
<span class="tbm-card-info-value">${session.work_location || '-'}</span>
<span class="tbm-card-info-value">${escapeHtml(session.work_location || '-')}</span>
</div>
<div class="tbm-card-info-item">
<span class="tbm-card-info-label">팀원</span>
<span class="tbm-card-info-value">${session.team_member_count || 0}명</span>
<span class="tbm-card-info-value">${parseInt(session.team_member_count) || 0}명</span>
</div>
</div>
</div>
${session.status === 'draft' ? `
<div class="tbm-card-footer">
<button class="tbm-btn tbm-btn-primary tbm-btn-sm" onclick="event.stopPropagation(); openTeamCompositionModal(${session.session_id})">
<button class="tbm-btn tbm-btn-primary tbm-btn-sm" onclick="event.stopPropagation(); openTeamCompositionModal(${safeSessionId})">
&#128101; 팀 구성
</button>
<button class="tbm-btn tbm-btn-secondary tbm-btn-sm" onclick="event.stopPropagation(); openSafetyCheckModal(${session.session_id})">
<button class="tbm-btn tbm-btn-secondary tbm-btn-sm" onclick="event.stopPropagation(); openSafetyCheckModal(${safeSessionId})">
&#10003; 안전 체크
</button>
</div>
@@ -604,8 +605,8 @@ function populateLeaderSelect() {
// 작업자와 연결된 경우: 자동으로 선택하고 비활성화
const worker = allWorkers.find(w => w.worker_id === currentUser.worker_id);
if (worker) {
const jobTypeText = worker.job_type ? ` (${worker.job_type})` : '';
leaderSelect.innerHTML = `<option value="${worker.worker_id}" selected>${worker.worker_name}${jobTypeText}</option>`;
const jobTypeText = worker.job_type ? ` (${escapeHtml(worker.job_type)})` : '';
leaderSelect.innerHTML = `<option value="${escapeHtml(worker.worker_id)}" selected>${escapeHtml(worker.worker_name)}${jobTypeText}</option>`;
leaderSelect.disabled = true;
console.log('✅ 입력자 자동 설정:', worker.worker_name);
} else {
@@ -621,8 +622,8 @@ function populateLeaderSelect() {
leaderSelect.innerHTML = '<option value="">입력자 선택...</option>' +
leaders.map(w => {
const jobTypeText = w.job_type ? ` (${w.job_type})` : '';
return `<option value="${w.worker_id}">${w.worker_name}${jobTypeText}</option>`;
const jobTypeText = w.job_type ? ` (${escapeHtml(w.job_type)})` : '';
return `<option value="${escapeHtml(w.worker_id)}">${escapeHtml(w.worker_name)}${jobTypeText}</option>`;
}).join('');
leaderSelect.disabled = false;
console.log('✅ 관리자: 입력자 선택 가능');
@@ -636,7 +637,7 @@ function populateProjectSelect() {
projectSelect.innerHTML = '<option value="">프로젝트 선택...</option>' +
allProjects.map(p => `
<option value="${p.project_id}">${p.project_name} (${p.job_no})</option>
<option value="${escapeHtml(p.project_id)}">${escapeHtml(p.project_name)} (${escapeHtml(p.job_no)})</option>
`).join('');
}
@@ -647,7 +648,7 @@ function populateWorkTypeSelect() {
workTypeSelect.innerHTML = '<option value="">공정 선택...</option>' +
allWorkTypes.map(wt => `
<option value="${wt.id}">${wt.name}${wt.category ? ' (' + wt.category + ')' : ''}</option>
<option value="${escapeHtml(wt.id)}">${escapeHtml(wt.name)}${wt.category ? ' (' + escapeHtml(wt.category) + ')' : ''}</option>
`).join('');
}
@@ -658,7 +659,7 @@ function populateWorkplaceSelect() {
workLocationSelect.innerHTML = '<option value="">작업장 선택...</option>' +
allWorkplaces.map(wp => `
<option value="${wp.workplace_name}">${wp.workplace_name}${wp.location ? ' - ' + wp.location : ''}</option>
<option value="${escapeHtml(wp.workplace_name)}">${escapeHtml(wp.workplace_name)}${wp.location ? ' - ' + escapeHtml(wp.location) : ''}</option>
`).join('');
}
@@ -683,7 +684,7 @@ function loadTasksByWorkType() {
taskSelect.disabled = false;
taskSelect.innerHTML = '<option value="">작업 선택...</option>' +
filteredTasks.map(task => `
<option value="${task.task_id}">${task.task_name}</option>
<option value="${escapeHtml(String(task.task_id))}">${escapeHtml(task.task_name)}</option>
`).join('');
if (filteredTasks.length === 0) {
@@ -872,12 +873,12 @@ function renderWorkerTaskList() {
<!-- 작업자 헤더 -->
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem; padding-bottom: 0.75rem; border-bottom: 1px solid #e5e7eb;">
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span style="font-weight: 600; font-size: 1.1rem; color: #1f2937;">👤 ${workerData.worker_name}</span>
<span style="font-weight: 600; font-size: 1.1rem; color: #1f2937;">👤 ${escapeHtml(workerData.worker_name)}</span>
<span style="padding: 0.125rem 0.5rem; background: #dbeafe; color: #1e40af; border-radius: 0.25rem; font-size: 0.75rem;">
${workerData.job_type || '작업자'}
${escapeHtml(workerData.job_type || '작업자')}
</span>
</div>
<button type="button" onclick="removeWorkerFromList(${workerIndex})" class="btn btn-sm btn-danger">
<button type="button" onclick="removeWorkerFromList(${parseInt(workerIndex) || 0})" class="btn btn-sm btn-danger">
<span style="font-size: 1rem;">✕ 작업자 제거</span>
</button>
</div>
@@ -887,7 +888,7 @@ function renderWorkerTaskList() {
<!-- 작업 추가 버튼 -->
<div style="margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px dashed #d1d5db;">
<button type="button" onclick="addTaskLineToWorker(${workerIndex})" class="btn btn-sm btn-secondary" style="width: 100%;">
<button type="button" onclick="addTaskLineToWorker(${parseInt(workerIndex) || 0})" class="btn btn-sm btn-secondary" style="width: 100%;">
<span style="font-size: 1.2rem; margin-right: 0.25rem;">+</span>
이 작업자의 추가 작업 등록
</button>
@@ -902,12 +903,14 @@ function renderTaskLine(workerData, workerIndex, taskLine, taskIndex) {
const project = allProjects.find(p => p.project_id === taskLine.project_id);
const workType = allWorkTypes.find(wt => wt.id === taskLine.work_type_id);
const task = allTasks.find(t => t.task_id === taskLine.task_id);
const safeWorkerIndex = parseInt(workerIndex) || 0;
const safeTaskIndex = parseInt(taskIndex) || 0;
const projectText = project ? project.project_name : '프로젝트 선택';
const workTypeText = workType ? workType.name : '공정 선택 *';
const taskText = task ? task.task_name : '작업 선택 *';
const projectText = escapeHtml(project ? project.project_name : '프로젝트 선택');
const workTypeText = escapeHtml(workType ? workType.name : '공정 선택 *');
const taskText = escapeHtml(task ? task.task_name : '작업 선택 *');
const workplaceText = taskLine.workplace_name
? `${taskLine.workplace_category_name || ''}${taskLine.workplace_name}`
? escapeHtml(`${taskLine.workplace_category_name || ''}${taskLine.workplace_name}`)
: '작업장 선택 *';
return `
@@ -915,7 +918,7 @@ function renderTaskLine(workerData, workerIndex, taskLine, taskIndex) {
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.5rem; margin-bottom: 0.5rem;">
<!-- 프로젝트 선택 -->
<button type="button"
onclick="openItemSelect('project', ${workerIndex}, ${taskIndex})"
onclick="openItemSelect('project', ${safeWorkerIndex}, ${safeTaskIndex})"
class="btn btn-sm ${project ? 'btn-primary' : 'btn-secondary'}"
style="text-align: left; justify-content: flex-start; font-size: 0.875rem;">
📁 ${projectText}
@@ -923,7 +926,7 @@ function renderTaskLine(workerData, workerIndex, taskLine, taskIndex) {
<!-- 작업장 선택 -->
<button type="button"
onclick="openWorkplaceSelect(${workerIndex}, ${taskIndex})"
onclick="openWorkplaceSelect(${safeWorkerIndex}, ${safeTaskIndex})"
class="btn btn-sm ${taskLine.workplace_id ? 'btn-primary' : 'btn-secondary'}"
style="text-align: left; justify-content: flex-start; font-size: 0.875rem;">
📍 ${workplaceText}
@@ -931,7 +934,7 @@ function renderTaskLine(workerData, workerIndex, taskLine, taskIndex) {
<!-- 공정 선택 -->
<button type="button"
onclick="openItemSelect('workType', ${workerIndex}, ${taskIndex})"
onclick="openItemSelect('workType', ${safeWorkerIndex}, ${safeTaskIndex})"
class="btn btn-sm ${workType ? 'btn-primary' : 'btn-secondary'}"
style="text-align: left; justify-content: flex-start; font-size: 0.875rem;">
⚙️ ${workTypeText}
@@ -939,7 +942,7 @@ function renderTaskLine(workerData, workerIndex, taskLine, taskIndex) {
<!-- 작업 선택 -->
<button type="button"
onclick="openItemSelect('task', ${workerIndex}, ${taskIndex})"
onclick="openItemSelect('task', ${safeWorkerIndex}, ${safeTaskIndex})"
class="btn btn-sm ${task ? 'btn-primary' : 'btn-secondary'}"
style="text-align: left; justify-content: flex-start; font-size: 0.875rem;"
${!taskLine.work_type_id ? 'disabled' : ''}>
@@ -949,7 +952,7 @@ function renderTaskLine(workerData, workerIndex, taskLine, taskIndex) {
<!-- 작업 라인 제거 버튼 -->
${workerData.tasks.length > 1 ? `
<button type="button" onclick="removeTaskLine(${workerIndex}, ${taskIndex})"
<button type="button" onclick="removeTaskLine(${safeWorkerIndex}, ${safeTaskIndex})"
class="btn btn-sm btn-danger" style="width: 100%; font-size: 0.8rem;">
<span style="margin-right: 0.25rem;"></span> 이 작업 라인 제거
</button>
@@ -971,16 +974,17 @@ function openWorkerSelectionModal() {
workerCardGrid.innerHTML = allWorkers.map(worker => {
const isAdded = addedWorkerIds.has(worker.worker_id);
const safeWorkerId = parseInt(worker.worker_id) || 0;
return `
<div id="worker-card-${worker.worker_id}"
onclick="toggleWorkerSelection(${worker.worker_id})"
<div id="worker-card-${safeWorkerId}"
onclick="toggleWorkerSelection(${safeWorkerId})"
style="padding: 1rem; border: 2px solid ${isAdded ? '#d1d5db' : '#e5e7eb'}; border-radius: 0.5rem; cursor: ${isAdded ? 'not-allowed' : 'pointer'}; background: ${isAdded ? '#f3f4f6' : 'white'}; opacity: ${isAdded ? '0.5' : '1'}; transition: all 0.2s;">
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
${isAdded ? '✓' : '☐'}
<span style="font-weight: 600; font-size: 0.95rem;">${worker.worker_name}</span>
<span style="font-weight: 600; font-size: 0.95rem;">${escapeHtml(worker.worker_name)}</span>
</div>
<div style="font-size: 0.8rem; color: #6b7280;">
${worker.job_type || '작업자'}${worker.department ? ' · ' + worker.department : ''}
${escapeHtml(worker.job_type || '작업자')}${worker.department ? ' · ' + escapeHtml(worker.department) : ''}
</div>
${isAdded ? '<div style="font-size: 0.75rem; color: #9ca3af; margin-top: 0.25rem;">이미 추가됨</div>' : ''}
</div>