fix(security): SSO 데드쿠키 제거 + open redirect 방어 + system2 수정
- SSO auth: 서버측 httpOnly 쿠키 제거 (클라이언트 domain cookie로 대체) - SSO auth: extractToken()에서 미작동 req.cookies 코드 제거 - Gateway login.html: redirect 파라미터 open redirect 취약점 방어 - System 2: 인라인 requireAuth → middlewares/auth.js 사용 - System 2: 404/에러 핸들러 등록 순서 수정 (Express 모범사례) - .gitignore: *.bak* 패턴 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
*.bak*
|
||||||
.env
|
.env
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
uploads/
|
uploads/
|
||||||
|
|||||||
@@ -161,7 +161,12 @@
|
|||||||
|
|
||||||
// redirect 파라미터가 있으면 해당 URL로, 없으면 포털로
|
// redirect 파라미터가 있으면 해당 URL로, 없으면 포털로
|
||||||
var redirect = new URLSearchParams(location.search).get('redirect');
|
var redirect = new URLSearchParams(location.search).get('redirect');
|
||||||
window.location.href = redirect || '/';
|
// Open redirect 방지: 상대 경로 또는 같은 도메인만 허용
|
||||||
|
if (redirect && (redirect.startsWith('/') && !redirect.startsWith('//')) && !redirect.includes('://')) {
|
||||||
|
window.location.href = redirect;
|
||||||
|
} else {
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errEl.textContent = err.message;
|
errEl.textContent = err.message;
|
||||||
errEl.style.display = '';
|
errEl.style.display = '';
|
||||||
|
|||||||
@@ -65,14 +65,8 @@ async function login(req, res, next) {
|
|||||||
{ expiresIn: JWT_REFRESH_EXPIRES_IN }
|
{ expiresIn: JWT_REFRESH_EXPIRES_IN }
|
||||||
);
|
);
|
||||||
|
|
||||||
// SSO 쿠키 설정 (동일 도메인 공유)
|
// SSO 쿠키는 클라이언트(login.html)에서 domain=.technicalkorea.net으로 설정
|
||||||
res.cookie('sso_token', access_token, {
|
// 서버 httpOnly 쿠키는 서브도메인 공유 불가하므로 제거
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
sameSite: 'lax',
|
|
||||||
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
||||||
path: '/'
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -237,14 +231,6 @@ async function refresh(req, res, next) {
|
|||||||
{ expiresIn: JWT_REFRESH_EXPIRES_IN }
|
{ expiresIn: JWT_REFRESH_EXPIRES_IN }
|
||||||
);
|
);
|
||||||
|
|
||||||
res.cookie('sso_token', access_token, {
|
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
sameSite: 'lax',
|
|
||||||
maxAge: 7 * 24 * 60 * 60 * 1000,
|
|
||||||
path: '/'
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
access_token,
|
access_token,
|
||||||
@@ -335,15 +321,11 @@ async function deleteUser(req, res, next) {
|
|||||||
* Bearer 토큰 또는 쿠키에서 토큰 추출
|
* Bearer 토큰 또는 쿠키에서 토큰 추출
|
||||||
*/
|
*/
|
||||||
function extractToken(req) {
|
function extractToken(req) {
|
||||||
// Authorization header
|
// Authorization header (SSO 토큰은 항상 Bearer로 전달)
|
||||||
const authHeader = req.headers['authorization'];
|
const authHeader = req.headers['authorization'];
|
||||||
if (authHeader && authHeader.startsWith('Bearer ')) {
|
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||||
return authHeader.split(' ')[1];
|
return authHeader.split(' ')[1];
|
||||||
}
|
}
|
||||||
// Cookie
|
|
||||||
if (req.cookies && req.cookies.sso_token) {
|
|
||||||
return req.cookies.sso_token;
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,31 +45,8 @@ app.get('/api/health', (req, res) => {
|
|||||||
res.json({ status: 'ok', service: 'system2-report', timestamp: new Date().toISOString() });
|
res.json({ status: 'ok', service: 'system2-report', timestamp: new Date().toISOString() });
|
||||||
});
|
});
|
||||||
|
|
||||||
// JWT Auth middleware (SSO 공유 시크릿)
|
// JWT Auth middleware (middlewares/auth.js 사용)
|
||||||
const jwt = require('jsonwebtoken');
|
const { requireAuth } = require('./middlewares/auth');
|
||||||
const requireAuth = (req, res, next) => {
|
|
||||||
try {
|
|
||||||
const authHeader = req.headers['authorization'];
|
|
||||||
if (!authHeader) {
|
|
||||||
throw new AuthenticationError('Authorization 헤더가 필요합니다');
|
|
||||||
}
|
|
||||||
const token = authHeader.split(' ')[1];
|
|
||||||
if (!token) {
|
|
||||||
throw new AuthenticationError('Bearer 토큰이 필요합니다');
|
|
||||||
}
|
|
||||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
|
||||||
req.user = decoded;
|
|
||||||
next();
|
|
||||||
} catch (err) {
|
|
||||||
if (err.name === 'JsonWebTokenError' || err.name === 'TokenExpiredError') {
|
|
||||||
return res.status(401).json({ success: false, error: '유효하지 않은 토큰입니다' });
|
|
||||||
}
|
|
||||||
if (err instanceof AuthenticationError) {
|
|
||||||
return res.status(401).json({ success: false, error: err.message });
|
|
||||||
}
|
|
||||||
next(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
const workIssueRoutes = require('./routes/workIssueRoutes');
|
const workIssueRoutes = require('./routes/workIssueRoutes');
|
||||||
@@ -77,6 +54,11 @@ const workIssueRoutes = require('./routes/workIssueRoutes');
|
|||||||
// 인증이 필요한 API
|
// 인증이 필요한 API
|
||||||
app.use('/api/work-issues', requireAuth, workIssueRoutes);
|
app.use('/api/work-issues', requireAuth, workIssueRoutes);
|
||||||
|
|
||||||
|
// 404 (에러 핸들러보다 먼저 등록)
|
||||||
|
app.use((req, res) => {
|
||||||
|
res.status(404).json({ success: false, error: 'Not Found', path: req.originalUrl });
|
||||||
|
});
|
||||||
|
|
||||||
// Error handler
|
// Error handler
|
||||||
app.use((err, req, res, next) => {
|
app.use((err, req, res, next) => {
|
||||||
const statusCode = err.statusCode || 500;
|
const statusCode = err.statusCode || 500;
|
||||||
@@ -87,11 +69,6 @@ app.use((err, req, res, next) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 404
|
|
||||||
app.use((req, res) => {
|
|
||||||
res.status(404).json({ success: false, error: 'Not Found', path: req.originalUrl });
|
|
||||||
});
|
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`System 2 (신고) API running on port ${PORT}`);
|
console.log(`System 2 (신고) API running on port ${PORT}`);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user