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:
@@ -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>📅</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})">
|
||||
👥 팀 구성
|
||||
</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})">
|
||||
✓ 안전 체크
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user