금융권 에러 메시지 정보노출 실전 시나리오

WEB-SER-031: 시스템 운영정보 노출 | 위험도: 4 (높음)

🔴 공격자 (해커)

에러 유발 준비 중...

https://xxbank.com/login

시나리오를 시작하세요

공격 진행 단계

시나리오를 시작하세요...

실전 공격 시나리오

취약한 코드 (상세 에러 노출)

문제 원인
// 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

🟠 서버 에러 로그

상세 에러 메시지 노출

[SERVER] Waiting...
서버 상태: RUNNING

안전한 코드 (에러 처리)

해결책
// 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>