Files
TK-FB-Project/api.hyungi.net/utils/passwordValidator.js
Hyungi Ahn 36f110c90a 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>
2026-02-05 06:33:10 +09:00

174 lines
4.5 KiB
JavaScript

/**
* Password Validator - 비밀번호 정책 검증
*
* 강력한 비밀번호 정책:
* - 최소 12자 이상
* - 대문자 포함
* - 소문자 포함
* - 숫자 포함
* - 특수문자 포함
*
* @author TK-FB-Project
* @since 2026-02-04
*/
/**
* 비밀번호 강도 검증
*
* @param {string} password - 검증할 비밀번호
* @param {Object} options - 옵션 (기본값 사용 권장)
* @returns {Object} { valid: boolean, errors: string[], strength: string }
*/
const validatePassword = (password, options = {}) => {
const config = {
minLength: options.minLength || 12,
requireUppercase: options.requireUppercase !== false,
requireLowercase: options.requireLowercase !== false,
requireNumbers: options.requireNumbers !== false,
requireSpecialChars: options.requireSpecialChars !== false,
maxLength: options.maxLength || 128
};
const errors = [];
let strength = 0;
// 필수 검증
if (!password || typeof password !== 'string') {
return {
valid: false,
errors: ['비밀번호를 입력해주세요.'],
strength: 'invalid'
};
}
// 길이 검증
if (password.length < config.minLength) {
errors.push(`비밀번호는 최소 ${config.minLength}자 이상이어야 합니다.`);
} else {
strength += 1;
}
if (password.length > config.maxLength) {
errors.push(`비밀번호는 ${config.maxLength}자를 초과할 수 없습니다.`);
}
// 대문자 검증
if (config.requireUppercase && !/[A-Z]/.test(password)) {
errors.push('대문자를 1개 이상 포함해야 합니다.');
} else if (/[A-Z]/.test(password)) {
strength += 1;
}
// 소문자 검증
if (config.requireLowercase && !/[a-z]/.test(password)) {
errors.push('소문자를 1개 이상 포함해야 합니다.');
} else if (/[a-z]/.test(password)) {
strength += 1;
}
// 숫자 검증
if (config.requireNumbers && !/\d/.test(password)) {
errors.push('숫자를 1개 이상 포함해야 합니다.');
} else if (/\d/.test(password)) {
strength += 1;
}
// 특수문자 검증
const specialChars = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/;
if (config.requireSpecialChars && !specialChars.test(password)) {
errors.push('특수문자를 1개 이상 포함해야 합니다. (!@#$%^&*()_+-=[]{};\':"|,.<>/?)');
} else if (specialChars.test(password)) {
strength += 1;
}
// 공백 검증
if (/\s/.test(password)) {
errors.push('비밀번호에 공백을 포함할 수 없습니다.');
}
// 연속된 문자 검증 (선택적)
if (/(.)\1{2,}/.test(password)) {
errors.push('동일한 문자를 3회 이상 연속 사용할 수 없습니다.');
}
// 강도 계산
let strengthLabel;
if (strength <= 2) {
strengthLabel = 'weak';
} else if (strength <= 3) {
strengthLabel = 'medium';
} else if (strength <= 4) {
strengthLabel = 'strong';
} else {
strengthLabel = 'very_strong';
}
return {
valid: errors.length === 0,
errors,
strength: strengthLabel,
score: strength
};
};
/**
* 간단한 비밀번호 검증 (기존 호환용)
* 모든 조건을 만족하면 true, 아니면 false
*
* @param {string} password - 검증할 비밀번호
* @returns {boolean} 유효 여부
*/
const isValidPassword = (password) => {
return validatePassword(password).valid;
};
/**
* 비밀번호 검증 결과를 한국어 메시지로 반환
*
* @param {string} password - 검증할 비밀번호
* @returns {string|null} 오류 메시지 (유효하면 null)
*/
const getPasswordError = (password) => {
const result = validatePassword(password);
if (result.valid) {
return null;
}
return result.errors.join(' ');
};
/**
* Express 미들웨어: 요청 body의 password 또는 newPassword 필드 검증
*
* @param {string} fieldName - 검증할 필드명 (기본: 'password')
* @returns {Function} Express 미들웨어
*/
const validatePasswordMiddleware = (fieldName = 'password') => {
return (req, res, next) => {
const password = req.body[fieldName] || req.body.newPassword;
if (!password) {
return next(); // 비밀번호 필드가 없으면 다음 미들웨어로
}
const result = validatePassword(password);
if (!result.valid) {
return res.status(400).json({
success: false,
error: '비밀번호가 보안 요구사항을 충족하지 않습니다.',
details: result.errors,
code: 'WEAK_PASSWORD'
});
}
next();
};
};
module.exports = {
validatePassword,
isValidPassword,
getPasswordError,
validatePasswordMiddleware
};