WEB-SER-019: 불충분한 이용자 인증 | 위험도: 4 (높음)
인증 우회 준비 중...
시나리오를 시작하세요
// Java Spring - 취약한 인증 체크
@Controller
public class TransferController {
@GetMapping("/transfer")
public String transferPage(Model model) {
// ❌ 인증 체크 없음!
// 누구나 URL 직접 입력으로 접근 가능
return "transfer";
}
@PostMapping("/transfer")
public ResponseEntity transfer(
@RequestBody TransferRequest request
) {
// ❌ 세션 검증 없음!
transferService.execute(
request.getFromAccount(),
request.getToAccount(),
request.getAmount()
);
return ResponseEntity.ok("송금 완료");
}
}
@Controller
public class PasswordController {
@GetMapping("/change-password")
public String changePasswordPage() {
// ❌ 재인증 없음!
return "change-password";
}
@PostMapping("/change-password")
public ResponseEntity changePassword(
@RequestBody PasswordRequest request
) {
// ❌ 현재 비밀번호 확인 없음!
userService.updatePassword(
request.getUserId(),
request.getNewPassword()
);
return ResponseEntity.ok("변경 완료");
}
}
@Controller
public class AdminController {
@GetMapping("/admin")
public String adminPage(HttpSession session) {
// ❌ 클라이언트 검증만 존재!
return "admin";
}
}
// admin.jsp - 클라이언트 검증
<script>
// ❌ JavaScript로만 관리자 체크!
if (localStorage.getItem('role') !== 'admin') {
alert('관리자만 접근 가능합니다.');
location.href = '/';
}
</script>
<div id="adminPanel">
<h1>관리자 페이지</h1>
<button onclick="deleteAllUsers()">
전체 사용자 삭제
</button>
</div>
// Python Flask - 취약한 인증
@app.route('/transfer', methods=['GET', 'POST'])
def transfer():
# ❌ 세션 체크 없음!
if request.method == 'GET':
return render_template('transfer.html')
# ❌ 인증 없이 송금 처리
from_account = request.json['from_account']
to_account = request.json['to_account']
amount = request.json['amount']
execute_transfer(from_account, to_account, amount)
return jsonify({"status": "success"})
@app.route('/change-password', methods=['POST'])
def change_password():
# ❌ 현재 비밀번호 확인 없음!
user_id = request.json['user_id']
new_password = request.json['new_password']
update_password(user_id, new_password)
return jsonify({"status": "success"})
// JavaScript/Node.js - 취약한 플로우
// ❌ 단계별 검증 없음!
app.post('/register/step2', (req, res) => {
// step1 완료 여부 확인 없음!
const { address } = req.body;
saveAddress(address);
res.json({ next: '/register/step3' });
});
app.post('/register/step3', (req, res) => {
// step2 완료 여부 확인 없음!
const { ssn } = req.body;
saveSsn(ssn);
res.json({ status: 'complete' });
});
인증 검증 없이 처리
// Java Spring - 안전한 인증 체크
// ✅ 방법 1: Spring Security 사용
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(
HttpSecurity http
) throws Exception {
http
.authorizeHttpRequests(auth -> auth
// 인증 필요한 경로 지정
.requestMatchers("/transfer/**")
.authenticated()
.requestMatchers("/change-password")
.authenticated()
.requestMatchers("/admin/**")
.hasRole("ADMIN")
.anyRequest().permitAll()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/")
)
.sessionManagement(session -> session
// 세션 고정 공격 방어
.sessionFixation()
.newSession()
// 세션 타임아웃 10분
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
);
return http.build();
}
}
// ✅ 방법 2: 커스텀 인증 인터셉터
@Component
public class AuthenticationInterceptor
implements HandlerInterceptor {
@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler
) throws Exception {
HttpSession session = request.getSession(false);
// 세션 존재 여부 확인
if (session == null) {
response.sendRedirect("/login");
return false;
}
// 사용자 인증 정보 확인
User user = (User) session.getAttribute("user");
if (user == null) {
response.sendRedirect("/login");
return false;
}
// 세션 타임아웃 체크 (10분)
long lastAccessTime = session.getLastAccessedTime();
long currentTime = System.currentTimeMillis();
long timeout = 10 * 60 * 1000; // 10분
if (currentTime - lastAccessTime > timeout) {
session.invalidate();
response.sendRedirect("/login");
return false;
}
// 관리자 권한 체크 (admin 경로)
String path = request.getRequestURI();
if (path.startsWith("/admin") &&
!"ADMIN".equals(user.getRole())) {
response.sendError(
HttpServletResponse.SC_FORBIDDEN,
"접근 권한이 없습니다."
);
return false;
}
return true;
}
}
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private AuthenticationInterceptor authInterceptor;
@Override
public void addInterceptors(
InterceptorRegistry registry
) {
registry.addInterceptor(authInterceptor)
// 인증 필요한 경로 지정
.addPathPatterns("/transfer/**")
.addPathPatterns("/change-password")
.addPathPatterns("/admin/**");
}
}
// ✅ 안전한 송금 컨트롤러
@Controller
@RequiredArgsConstructor
public class SecureTransferController {
private final TransferService transferService;
@GetMapping("/transfer")
public String transferPage(
HttpSession session,
Model model
) {
// 인증 확인
User user = (User) session.getAttribute("user");
if (user == null) {
return "redirect:/login";
}
model.addAttribute("user", user);
return "transfer";
}
@PostMapping("/transfer")
public ResponseEntity transfer(
@RequestBody TransferRequest request,
HttpSession session
) {
// 1. 세션 검증
User user = (User) session.getAttribute("user");
if (user == null) {
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body("인증이 필요합니다.");
}
// 2. 계좌 소유자 확인
if (!user.getId().equals(
request.getFromAccountOwnerId()
)) {
return ResponseEntity
.status(HttpStatus.FORBIDDEN)
.body("본인 계좌만 사용 가능합니다.");
}
// 3. 추가 인증 (OTP)
if (!otpService.verify(
user.getId(),
request.getOtp()
)) {
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body("OTP 인증에 실패했습니다.");
}
// 4. 송금 실행
transferService.execute(
user.getId(),
request.getFromAccount(),
request.getToAccount(),
request.getAmount()
);
return ResponseEntity.ok("송금 완료");
}
}
// ✅ 안전한 비밀번호 변경
@Controller
@RequiredArgsConstructor
public class SecurePasswordController {
private final UserService userService;
@GetMapping("/change-password")
public String changePasswordPage(
HttpSession session
) {
// 인증 확인
if (session.getAttribute("user") == null) {
return "redirect:/login";
}
// 재인증 플래그 초기화
session.removeAttribute("reauth_verified");
return "change-password";
}
@PostMapping("/verify-current-password")
public ResponseEntity verifyCurrentPassword(
@RequestBody Map request,
HttpSession session
) {
User user = (User) session.getAttribute("user");
String currentPassword = request.get("password");
// 현재 비밀번호 확인
if (userService.verifyPassword(
user.getId(),
currentPassword
)) {
// 재인증 완료 플래그
session.setAttribute(
"reauth_verified",
true
);
session.setAttribute(
"reauth_time",
System.currentTimeMillis()
);
return ResponseEntity.ok("인증 완료");
} else {
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body("비밀번호가 일치하지 않습니다.");
}
}
@PostMapping("/change-password")
public ResponseEntity changePassword(
@RequestBody PasswordRequest request,
HttpSession session
) {
User user = (User) session.getAttribute("user");
// 1. 재인증 확인
Boolean reauthVerified = (Boolean) session
.getAttribute("reauth_verified");
if (reauthVerified == null || !reauthVerified) {
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body("현재 비밀번호를 먼저 확인해주세요.");
}
// 2. 재인증 시간 확인 (5분 이내)
Long reauthTime = (Long) session
.getAttribute("reauth_time");
long currentTime = System.currentTimeMillis();
if (currentTime - reauthTime > 5 * 60 * 1000) {
session.removeAttribute("reauth_verified");
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body("재인증 시간이 만료되었습니다.");
}
// 3. 비밀번호 변경
userService.updatePassword(
user.getId(),
request.getNewPassword()
);
// 4. 재인증 플래그 제거
session.removeAttribute("reauth_verified");
session.removeAttribute("reauth_time");
return ResponseEntity.ok("변경 완료");
}
}
// Python Flask - 안전한 인증
from functools import wraps
from flask import session, redirect, request
# ✅ 인증 데코레이터
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
# 세션 확인
if 'user_id' not in session:
return redirect('/login')
# 세션 타임아웃 체크 (10분)
last_activity = session.get('last_activity')
if last_activity:
elapsed = time.time() - last_activity
if elapsed > 600: # 10분
session.clear()
return redirect('/login')
# 활동 시간 업데이트
session['last_activity'] = time.time()
return f(*args, **kwargs)
return decorated_function
# ✅ 관리자 권한 체크
def admin_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
return redirect('/login')
# 관리자 권한 확인
user = get_user(session['user_id'])
if user.role != 'ADMIN':
return jsonify({
"error": "접근 권한이 없습니다."
}), 403
return f(*args, **kwargs)
return decorated_function
@app.route('/transfer', methods=['GET', 'POST'])
@login_required
def transfer():
if request.method == 'GET':
return render_template('transfer.html')
# 사용자 확인
user_id = session['user_id']
from_account = request.json['from_account']
# 계좌 소유자 확인
account = get_account(from_account)
if account.owner_id != user_id:
return jsonify({
"error": "본인 계좌만 사용 가능합니다."
}), 403
# OTP 확인
otp = request.json.get('otp')
if not verify_otp(user_id, otp):
return jsonify({
"error": "OTP 인증에 실패했습니다."
}), 401
execute_transfer(
from_account,
request.json['to_account'],
request.json['amount']
)
return jsonify({"status": "success"})
@app.route('/change-password', methods=['POST'])
@login_required
def change_password():
user_id = session['user_id']
# 재인증 확인
if not session.get('reauth_verified'):
return jsonify({
"error": "현재 비밀번호를 먼저 확인해주세요."
}), 401
# 재인증 시간 확인 (5분)
reauth_time = session.get('reauth_time')
if time.time() - reauth_time > 300:
session.pop('reauth_verified', None)
return jsonify({
"error": "재인증 시간이 만료되었습니다."
}), 401
# 비밀번호 변경
update_password(
user_id,
request.json['new_password']
)
# 재인증 플래그 제거
session.pop('reauth_verified', None)
session.pop('reauth_time', None)
return jsonify({"status": "success"})
@app.route('/admin')
@admin_required
def admin_page():
return render_template('admin.html')
// JavaScript/Node.js - 안전한 플로우
// ✅ 세션 기반 플로우 관리
app.post('/register/step2', (req, res) => {
const session = req.session;
// step1 완료 확인
if (!session.step1_completed) {
return res.status(400).json({
error: "이전 단계를 먼저 완료해주세요."
});
}
const { address } = req.body;
saveAddress(session.user_id, address);
// step2 완료 표시
session.step2_completed = true;
res.json({ next: '/register/step3' });
});
app.post('/register/step3', (req, res) => {
const session = req.session;
// 이전 단계 확인
if (!session.step1_completed ||
!session.step2_completed) {
return res.status(400).json({
error: "이전 단계를 먼저 완료해주세요."
});
}
const { ssn } = req.body;
saveSsn(session.user_id, ssn);
// 플로우 완료 - 세션 정리
delete session.step1_completed;
delete session.step2_completed;
res.json({ status: 'complete' });
});