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:
@@ -146,11 +146,17 @@ function renderBasicInfo(d) {
|
||||
});
|
||||
};
|
||||
|
||||
const validTypes = ['nonconformity', 'safety'];
|
||||
const safeType = validTypes.includes(d.category_type) ? d.category_type : '';
|
||||
const reporterName = escapeHtml(d.reporter_full_name || d.reporter_name || '-');
|
||||
const locationText = escapeHtml(d.custom_location || d.workplace_name || '-');
|
||||
const factoryText = d.factory_name ? ` (${escapeHtml(d.factory_name)})` : '';
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="info-item">
|
||||
<div class="info-label">신고 유형</div>
|
||||
<div class="info-value">
|
||||
<span class="type-badge ${d.category_type}">${typeNames[d.category_type] || d.category_type}</span>
|
||||
<span class="type-badge ${safeType}">${typeNames[d.category_type] || escapeHtml(d.category_type || '-')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
@@ -159,11 +165,11 @@ function renderBasicInfo(d) {
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">신고자</div>
|
||||
<div class="info-value">${d.reporter_full_name || d.reporter_name || '-'}</div>
|
||||
<div class="info-value">${reporterName}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">위치</div>
|
||||
<div class="info-value">${d.custom_location || d.workplace_name || '-'}${d.factory_name ? ` (${d.factory_name})` : ''}</div>
|
||||
<div class="info-value">${locationText}${factoryText}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -174,17 +180,20 @@ function renderBasicInfo(d) {
|
||||
function renderIssueContent(d) {
|
||||
const container = document.getElementById('issueContent');
|
||||
|
||||
const validSeverities = ['critical', 'high', 'medium', 'low'];
|
||||
const safeSeverity = validSeverities.includes(d.severity) ? d.severity : '';
|
||||
|
||||
let html = `
|
||||
<div class="info-grid" style="margin-bottom: 1rem;">
|
||||
<div class="info-item">
|
||||
<div class="info-label">카테고리</div>
|
||||
<div class="info-value">${d.issue_category_name || '-'}</div>
|
||||
<div class="info-value">${escapeHtml(d.issue_category_name || '-')}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">항목</div>
|
||||
<div class="info-value">
|
||||
${d.issue_item_name || '-'}
|
||||
${d.severity ? `<span class="severity-badge ${d.severity}">${severityNames[d.severity]}</span>` : ''}
|
||||
${escapeHtml(d.issue_item_name || '-')}
|
||||
${d.severity ? `<span class="severity-badge ${safeSeverity}">${severityNames[d.severity] || escapeHtml(d.severity)}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -262,11 +271,11 @@ function renderProcessInfo(d) {
|
||||
html += `
|
||||
<div class="info-item">
|
||||
<div class="info-label">담당자</div>
|
||||
<div class="info-value">${d.assigned_full_name || d.assigned_user_name || '-'}</div>
|
||||
<div class="info-value">${escapeHtml(d.assigned_full_name || d.assigned_user_name || '-')}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">담당 부서</div>
|
||||
<div class="info-value">${d.assigned_department || '-'}</div>
|
||||
<div class="info-value">${escapeHtml(d.assigned_department || '-')}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -279,7 +288,7 @@ function renderProcessInfo(d) {
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">처리자</div>
|
||||
<div class="info-value">${d.resolved_by_name || '-'}</div>
|
||||
<div class="info-value">${escapeHtml(d.resolved_by_name || '-')}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -402,10 +411,10 @@ function renderStatusTimeline(logs) {
|
||||
container.innerHTML = logs.map(log => `
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-status">
|
||||
${log.previous_status ? `${statusNames[log.previous_status]} → ` : ''}${statusNames[log.new_status]}
|
||||
${log.previous_status ? `${statusNames[log.previous_status] || escapeHtml(log.previous_status)} → ` : ''}${statusNames[log.new_status] || escapeHtml(log.new_status)}
|
||||
</div>
|
||||
<div class="timeline-meta">
|
||||
${log.changed_by_full_name || log.changed_by_name} | ${formatDate(log.changed_at)}
|
||||
${escapeHtml(log.changed_by_full_name || log.changed_by_name || '-')} | ${formatDate(log.changed_at)}
|
||||
${log.change_reason ? `<br><small>${escapeHtml(log.change_reason)}</small>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
@@ -530,7 +539,8 @@ async function openAssignModal() {
|
||||
|
||||
if (data.success && data.data) {
|
||||
data.data.forEach(user => {
|
||||
select.innerHTML += `<option value="${user.user_id}">${user.name} (${user.username})</option>`;
|
||||
const safeUserId = parseInt(user.user_id) || 0;
|
||||
select.innerHTML += `<option value="${safeUserId}">${escapeHtml(user.name || '-')} (${escapeHtml(user.username || '-')})</option>`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user