Phase 1 CRITICAL XSS: - marked.parse() → DOMPurify.sanitize() (system3 ai-assistant, issues-management) - toast innerHTML에 escapeHtml 적용 (system1 api-base, system3 common-header) - onclick 핸들러 → data 속성 + addEventListener (system2 issue-detail) Phase 2 HIGH 인가: - getUserBalance 본인확인 추가 (tksupport vacationController) Phase 3 HIGH 토큰+CSP: - localStorage 토큰 저장 제거 — 쿠키 전용 (7개 서비스) - unsafe-eval CSP 제거 (system1 security.js) Phase 4 MEDIUM: - nginx 보안 헤더 추가 (8개 서비스) - 500 에러 메시지 마스킹 (5개 API) - path traversal 방지 (system3 file_service.py) - cookie fallback 데드코드 제거 (4개 auth.js) - /login/form rate limiting 추가 (sso-auth) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
99 lines
3.2 KiB
JavaScript
99 lines
3.2 KiB
JavaScript
const jwt = require('jsonwebtoken');
|
|
const mysql = require('mysql2/promise');
|
|
|
|
const JWT_SECRET = process.env.SSO_JWT_SECRET;
|
|
|
|
let pool;
|
|
function getPool() {
|
|
if (!pool) {
|
|
pool = mysql.createPool({
|
|
host: process.env.DB_HOST || 'mariadb',
|
|
port: parseInt(process.env.DB_PORT) || 3306,
|
|
user: process.env.DB_USER || 'hyungi_user',
|
|
password: process.env.DB_PASSWORD,
|
|
database: process.env.DB_NAME || 'hyungi',
|
|
waitForConnections: true,
|
|
connectionLimit: 5,
|
|
queueLimit: 0
|
|
});
|
|
}
|
|
return pool;
|
|
}
|
|
|
|
function extractToken(req) {
|
|
const authHeader = req.headers['authorization'];
|
|
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
return authHeader.split(' ')[1];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function requireAuth(req, res, next) {
|
|
const token = extractToken(req);
|
|
if (!token) {
|
|
return res.status(401).json({ success: false, error: '인증이 필요합니다' });
|
|
}
|
|
try {
|
|
const decoded = jwt.verify(token, JWT_SECRET);
|
|
req.user = decoded;
|
|
next();
|
|
} catch {
|
|
return res.status(401).json({ success: false, error: '유효하지 않은 토큰입니다' });
|
|
}
|
|
}
|
|
|
|
function requireAdmin(req, res, next) {
|
|
const token = extractToken(req);
|
|
if (!token) {
|
|
return res.status(401).json({ success: false, error: '인증이 필요합니다' });
|
|
}
|
|
try {
|
|
const decoded = jwt.verify(token, JWT_SECRET);
|
|
if (!['admin', 'system'].includes((decoded.role || '').toLowerCase())) {
|
|
return res.status(403).json({ success: false, error: '관리자 권한이 필요합니다' });
|
|
}
|
|
req.user = decoded;
|
|
next();
|
|
} catch {
|
|
return res.status(401).json({ success: false, error: '유효하지 않은 토큰입니다' });
|
|
}
|
|
}
|
|
|
|
function requirePage(pageName) {
|
|
return async (req, res, next) => {
|
|
const userId = req.user.user_id || req.user.id;
|
|
const role = (req.user.role || '').toLowerCase();
|
|
if (role === 'admin' || role === 'system') return next();
|
|
|
|
try {
|
|
const db = getPool();
|
|
// 1. 개인 권한
|
|
const [rows] = await db.query(
|
|
'SELECT can_access FROM user_page_permissions WHERE user_id = ? AND page_name = ?',
|
|
[userId, pageName]
|
|
);
|
|
if (rows.length > 0) {
|
|
return rows[0].can_access ? next() : res.status(403).json({ success: false, error: '접근 권한이 없습니다' });
|
|
}
|
|
// 2. 부서 권한
|
|
const [userRows] = await db.query('SELECT department_id FROM sso_users WHERE user_id = ?', [userId]);
|
|
if (userRows.length > 0 && userRows[0].department_id) {
|
|
const [deptRows] = await db.query(
|
|
'SELECT can_access FROM department_page_permissions WHERE department_id = ? AND page_name = ?',
|
|
[userRows[0].department_id, pageName]
|
|
);
|
|
if (deptRows.length > 0) {
|
|
return deptRows[0].can_access ? next() : res.status(403).json({ success: false, error: '접근 권한이 없습니다' });
|
|
}
|
|
}
|
|
// 3. 기본 거부
|
|
return res.status(403).json({ success: false, error: '접근 권한이 없습니다' });
|
|
} catch (err) {
|
|
console.error('Permission check error:', err);
|
|
return res.status(500).json({ success: false, error: '권한 확인 실패' });
|
|
}
|
|
};
|
|
}
|
|
|
|
module.exports = { extractToken, requireAuth, requireAdmin, requirePage };
|