WEB-SER-031: 시스템 운영정보 노출 | 위험도: 4 (높음)
에러 유발 준비 중...
시나리오를 시작하세요
// Java Spring - 취약한 에러 처리
@RestController
public class LoginController {
@PostMapping("/login")
public ResponseEntity login(@RequestBody LoginDTO dto) {
try {
User user = userService.login(
dto.getUsername(),
dto.getPassword()
);
return ResponseEntity.ok(user);
} catch (Exception e) {
// ❌ 전체 스택 트레이스 노출!
e.printStackTrace();
// ❌ 상세 에러 메시지 반환!
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Error: " + e.getMessage() +
"\nCause: " + e.getCause() +
"\nStack: " + e.getStackTrace()[0]);
}
}
}
@Service
public class UserService {
@Autowired
private DataSource dataSource;
public User login(String username, String password) {
try {
// ❌ DB 연결 실패 시 정보 노출
Connection conn = dataSource.getConnection();
String sql = "SELECT * FROM users " +
"WHERE username = ? AND password = ?";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setString(1, username);
stmt.setString(2, password);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
return new User(rs);
} else {
// ❌ 구체적 에러 메시지
throw new Exception(
"Login failed: User not found for username="
+ username
);
}
} catch (SQLException e) {
// ❌ SQL 에러 상세 노출!
throw new RuntimeException(
"Database error: " + e.getMessage() +
"\nSQL State: " + e.getSQLState() +
"\nError Code: " + e.getErrorCode()
);
}
}
}
// Python Flask - 취약한 에러 처리
@app.route('/transfer', methods=['POST'])
def transfer():
try:
from_account = request.json['from_account']
to_account = request.json['to_account']
amount = request.json['amount']
# ❌ DB 연결 에러 노출
conn = mysql.connector.connect(
host='10.1.2.3',
user='bank_admin',
password='BankDB@2024',
database='bank_prod'
)
cursor = conn.cursor()
cursor.execute(
"UPDATE accounts SET balance = balance - %s "
"WHERE account_number = %s",
(amount, from_account)
)
return jsonify({"status": "success"})
except Exception as e:
# ❌ 전체 에러 정보 반환!
return jsonify({
"error": str(e),
"type": type(e).__name__,
"traceback": traceback.format_exc()
}), 500
// JavaScript/Node.js - 취약한 에러 처리
app.post('/api/accounts', async (req, res) => {
try {
const { accountNumber } = req.body;
// ❌ 파일 경로 노출
const configPath = path.join(
__dirname,
'../../config/database.json'
);
const config = JSON.parse(
fs.readFileSync(configPath, 'utf8')
);
const account = await getAccount(accountNumber);
res.json(account);
} catch (error) {
// ❌ 상세 에러 스택 노출!
res.status(500).json({
error: error.message,
stack: error.stack,
// ❌ 개발 정보 노출
env: process.env.NODE_ENV,
version: process.version,
platform: process.platform
});
}
});
// application.properties - 취약한 설정
# ❌ 개발 모드로 운영 (상세 에러 표시)
spring.profiles.active=dev
# ❌ 에러 상세정보 노출
server.error.include-message=always
server.error.include-binding-errors=always
server.error.include-stacktrace=always
server.error.include-exception=true
# ❌ 디버그 모드 활성화
debug=true
logging.level.root=DEBUG
# ❌ SQL 쿼리 로깅
spring.jpa.show-sql=true
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql=TRACE
상세 에러 메시지 노출
// Java Spring - 안전한 에러 처리
// ✅ 전역 예외 처리기
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger =
LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(Exception.class)
public ResponseEntity handleException(
Exception e, HttpServletRequest request
) {
// 내부 로깅 (상세 정보)
logger.error(
"Error occurred: {} at {}",
e.getMessage(),
request.getRequestURI(),
e // 스택 트레이스는 로그에만
);
// 클라이언트 응답 (일반 메시지만)
ErrorResponse response = new ErrorResponse(
"ERR_INTERNAL_SERVER",
"서비스 처리 중 오류가 발생했습니다.",
UUID.randomUUID().toString() // 추적용 ID
);
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(response);
}
@ExceptionHandler(DataAccessException.class)
public ResponseEntity handleDataAccessException(
DataAccessException e, HttpServletRequest request
) {
// 상세 로깅 (내부용)
logger.error(
"Database error at {}: {}",
request.getRequestURI(),
e.getMessage(),
e
);
// 일반적인 메시지만 반환
ErrorResponse response = new ErrorResponse(
"ERR_DATABASE",
"데이터베이스 처리 중 오류가 발생했습니다.",
UUID.randomUUID().toString()
);
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(response);
}
}
// ✅ 표준화된 에러 응답
public class ErrorResponse {
private String errorCode; // 에러 코드
private String message; // 사용자 메시지
private String trackingId; // 추적 ID
private LocalDateTime timestamp;
public ErrorResponse(
String errorCode,
String message,
String trackingId
) {
this.errorCode = errorCode;
this.message = message;
this.trackingId = trackingId;
this.timestamp = LocalDateTime.now();
}
// getter/setter (상세 정보 제외)
}
@Service
public class SecureUserService {
private static final Logger logger =
LoggerFactory.getLogger(SecureUserService.class);
@Autowired
private UserRepository userRepository;
public User login(String username, String password) {
try {
// Repository 패턴 사용 (DB 정보 숨김)
Optional user = userRepository
.findByUsernameAndPassword(username, password);
if (user.isPresent()) {
return user.get();
} else {
// 일반적인 에러 메시지
throw new AuthenticationException(
"아이디 또는 비밀번호가 올바르지 않습니다."
);
}
} catch (DataAccessException e) {
// 상세 정보는 로그에만
logger.error(
"Database error during login for user: {}",
username,
e
);
// 사용자에게는 일반 메시지
throw new ServiceException(
"로그인 처리 중 오류가 발생했습니다."
);
}
}
}
// Python Flask - 안전한 에러 처리
# ✅ 전역 에러 핸들러
@app.errorhandler(Exception)
def handle_exception(e):
# 상세 로깅 (서버측)
app.logger.error(
f"Error: {str(e)}",
exc_info=True # 스택 트레이스 포함
)
# 추적 ID 생성
tracking_id = str(uuid.uuid4())
# 일반 응답 (클라이언트)
return jsonify({
"error_code": "ERR_INTERNAL_SERVER",
"message": "서비스 처리 중 오류가 발생했습니다.",
"tracking_id": tracking_id
}), 500
@app.errorhandler(DatabaseError)
def handle_database_error(e):
# 로그에만 상세 정보
app.logger.error(
f"Database error: {str(e)}",
exc_info=True
)
# 사용자에게는 일반 메시지
return jsonify({
"error_code": "ERR_DATABASE",
"message": "데이터베이스 처리 중 오류가 발생했습니다.",
"tracking_id": str(uuid.uuid4())
}), 500
@app.route('/transfer', methods=['POST'])
def transfer():
try:
from_account = request.json.get('from_account')
to_account = request.json.get('to_account')
amount = request.json.get('amount')
# 입력 검증
if not all([from_account, to_account, amount]):
return jsonify({
"error_code": "ERR_INVALID_INPUT",
"message": "필수 입력값이 누락되었습니다."
}), 400
# DB 연결 (환경변수 사용)
conn = get_db_connection() # 내부 함수
cursor = conn.cursor()
cursor.execute(
"UPDATE accounts SET balance = balance - %s "
"WHERE account_number = %s",
(amount, from_account)
)
conn.commit()
return jsonify({"status": "success"})
except ValueError as e:
# 예상된 에러 (사용자 메시지)
app.logger.warning(f"Invalid input: {str(e)}")
return jsonify({
"error_code": "ERR_INVALID_INPUT",
"message": "입력값이 올바르지 않습니다."
}), 400
except Exception as e:
# 예상치 못한 에러 (일반 메시지)
app.logger.error(f"Transfer error: {str(e)}", exc_info=True)
return jsonify({
"error_code": "ERR_TRANSFER_FAILED",
"message": "송금 처리 중 오류가 발생했습니다."
}), 500
# ✅ 안전한 로깅 설정
import logging
from logging.handlers import RotatingFileHandler
# 파일 로깅 (상세 정보)
file_handler = RotatingFileHandler(
'logs/app.log',
maxBytes=10240000,
backupCount=10
)
file_handler.setLevel(logging.ERROR)
file_handler.setFormatter(logging.Formatter(
'[%(asctime)s] %(levelname)s in %(module)s: %(message)s'
))
app.logger.addHandler(file_handler)
// JavaScript/Node.js - 안전한 에러 처리
// ✅ 전역 에러 미들웨어
app.use((err, req, res, next) => {
// 상세 로깅 (서버측)
console.error('Error:', {
message: err.message,
stack: err.stack,
url: req.url,
method: req.method,
timestamp: new Date().toISOString()
});
// 추적 ID
const trackingId = require('uuid').v4();
// 일반 응답 (클라이언트)
res.status(err.status || 500).json({
error_code: err.code || 'ERR_INTERNAL_SERVER',
message: err.userMessage ||
'서비스 처리 중 오류가 발생했습니다.',
tracking_id: trackingId
});
});
app.post('/api/accounts', async (req, res, next) => {
try {
const { accountNumber } = req.body;
// 입력 검증
if (!accountNumber) {
const error = new Error('Account number required');
error.status = 400;
error.code = 'ERR_INVALID_INPUT';
error.userMessage = '계좌번호를 입력해주세요.';
throw error;
}
// 안전한 DB 접근
const account = await getAccount(accountNumber);
res.json(account);
} catch (error) {
// 에러 핸들러로 전달
next(error);
}
});
// ✅ 커스텀 에러 클래스
class ApplicationError extends Error {
constructor(message, code, status, userMessage) {
super(message);
this.code = code;
this.status = status;
this.userMessage = userMessage;
}
}
// application.properties - 안전한 설정
# ✅ 운영 모드 (최소 정보)
spring.profiles.active=prod
# ✅ 에러 정보 최소화
server.error.include-message=never
server.error.include-binding-errors=never
server.error.include-stacktrace=never
server.error.include-exception=false
# ✅ 운영 환경 로깅
debug=false
logging.level.root=WARN
logging.level.com.xxbank=INFO
# ✅ SQL 로깅 비활성화
spring.jpa.show-sql=false
logging.level.org.hibernate.SQL=WARN
# ✅ 에러 페이지 커스터마이징
server.error.whitelabel.enabled=false
server.error.path=/error
// logback-spring.xml - 안전한 로깅 설정
<configuration>
<!-- 파일 로깅 (상세 정보) -->
<appender name="FILE"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/application.log</file>
<encoder>
<pattern>
%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level
%logger{36} - %msg%n
</pattern>
</encoder>
<rollingPolicy
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>
logs/application-%d{yyyy-MM-dd}.log
</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>
<!-- 콘솔 로깅 (최소 정보) -->
<appender name="CONSOLE"
class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss} %-5level - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="FILE" />
<appender-ref ref="CONSOLE" />
</root>
</configuration>