SQL 인젝션(SQL Injection)이란 무엇이며, 어떻게 방어하나요?

김상욱·2024년 12월 29일
0

SQL 인젝션(SQL Injection)이란 무엇이며, 어떻게 방어하나요?

SQL 인젝션은 악의적인 사용자가 애플리케이션의 입력 필드를 통해 SQL 쿼리에 악성 코드를 삽입하여 데이터베이스에 부적절한 접근을 시도하는 보안 취약점입니다. 이를 통해 민감한 데이터 유출, 데이터 변조, 삭제 등이 발생할 수 있습니다.

예를 들어, 사용자가 로그인할 때 아이디와 비밀번호를 입력한다고 가정해봅시다. 다음과 같은 간단한 SQL 쿼리가 있다고 할 때:

SELECT * FROM users WHERE username = '입력된아이디' AND password = '입력된비밀번호';

만약 사용자가 비밀번호 필드에 다음과 같이 입력한다면

' OR '1'='1

쿼리는 다음과 같이 변형됩니다.

SELECT * FROM users WHERE username = '입력된아이디' AND password = '' OR '1'='1';

이 경우 '1'='1'이 항상 참이 되므로, 인증 없이 로그인에 성공하게 될 수 있습니다.

SQL 인젝션 방어 방법

  1. Prepared Statement 사용하기
    Java의 PreparedStatement를 사용하면 SQL 쿼리와 파라미터를 분리하여 처리할 수 있습니다. 이를 통해 사용자 입력이 SQL 코드로 해석되는 것을 방지할 수 있습니다.
String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, username);
pstmt.setString(2, password);
ResultSet rs = pstmt.executeQuery();

? 플레이스 홀더에 값을 바인딩하면, JDBC가 자동으로 입력값을 이스케이프 처리하여 SQL 인젝션을 방지합니다.

  1. ORM(Object-Relational Mapping) 프레임워크 사용하기
    Spring Data JPA와 같은 ORM 프레임워크를 사용하면, SQL 쿼리를 직접 작성하지 않고도 안전하게 데이터베이스와 상호작용할 수 있습니다. ORM은 내부적으로 Prepared Statement를 사용하여 SQL 인젝션을 방지합니다.
// 예시: Spring Data JPA 리포지토리 인터페이스
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsernameAndPassword(String username, String password);
}
  1. 입력 값 검증(Input Validation) 및 이스케이프 처리
    사용자로부터 입력받은 데이터가 예상한 형식인지 검증하고, 필요에 따라 이스케이프 처리를 합니다. 예를 들어, 입력값에 특수 문자가 포함되어 있는지 확인하고 제거할 수 있습니다.

  2. 최소 권한 원칙 적용
    데이터베이스 사용자에게 필요한 최소한의 권한만 부여하여, 만약 인젝션 공격이 성공하더라도 피해를 최소화할 수 있습니다. 예를 들어, 읽기 전용 사용자 계정을 사용하면 데이터베이스 수정이나 삭제를 방지할 수 있습니다.

  3. 웹 애플리케이션 방화벽(WAF) 사용
    WAF를 사용하면 SQL 인젝션 시도를 실시간으로 탐지하고 차단할 수 있습니다. 이는 추가적인 보안 계층을 제공하여 애플리케이션을 보호합니다.

  4. 보안 테스트 및 코드 리뷰 수행
    정기적으로 보안 테스트를 실시하고 코드 리뷰를 통해 잠재적인 SQL 인젝션 취약점을 찾아 수정합니다. OWASP의 SQL Injection 관련 가이드를 참고하면 도움이 됩니다.


물론입니다! 신입 Java/Spring 백엔드 개발자로서 SQL 인젝션을 이해하고 방어하는 방법을 실습을 통해 체득하는 것은 매우 중요합니다. 아래에 단계별로 따라 할 수 있는 실습 예제를 제공할게요. 이 실습을 통해 SQL 인젝션의 취약점을 직접 경험하고, 이를 방어하는 다양한 방법을 구현해볼 수 있습니다.

실습 목표

  1. 취약한 애플리케이션 만들기: SQL 인젝션 취약점이 있는 간단한 Spring Boot 애플리케이션을 구축합니다.
  2. SQL 인젝션 공격 시도: 취약점을 이용해 애플리케이션을 공격해봅니다.
  3. 방어 메커니즘 구현: PreparedStatement, Spring Data JPA, 입력 값 검증 등을 통해 취약점을 수정합니다.
  4. 보안 테스트 수행: 수정된 애플리케이션에 대한 보안 테스트를 진행합니다.

준비 사항

  • Java Development Kit (JDK) 설치 (버전 8 이상 권장)
  • IDE (IntelliJ IDEA, Eclipse 등)
  • Maven 또는 Gradle (프로젝트 빌드 도구)
  • MySQL 또는 H2 Database (데이터베이스)

1단계: 취약한 Spring Boot 애플리케이션 구축

1.1 프로젝트 생성

  • Spring Initializr(https://start.spring.io/)를 이용해 새로운 Spring Boot 프로젝트를 생성합니다.
  • 필요한 의존성:
    • Spring Web
    • Spring Data JPA
    • MySQL Driver (또는 H2 Database)
    • Thymeleaf (템플릿 엔진, 선택 사항)

1.2 데이터베이스 설정

  • application.properties 파일에 데이터베이스 연결 설정을 추가합니다.
spring.datasource.url=jdbc:mysql://localhost:3306/sql_injection_demo
spring.datasource.username=your_username
spring.datasource.password=your_password
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
  • 데이터베이스 생성:

    CREATE DATABASE sql_injection_demo;
    USE sql_injection_demo;
    
    CREATE TABLE users (
        id INT AUTO_INCREMENT PRIMARY KEY,
        username VARCHAR(50) NOT NULL,
        password VARCHAR(50) NOT NULL
    );
    
    INSERT INTO users (username, password) VALUES ('admin', 'admin123'), ('user', 'user123');

1.3 간단한 로그인 기능 구현 (취약한 코드)

  • User 엔티티 생성:
@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;
    private String password;

    // Getters and Setters
}
  • UserRepository 생성:
public interface UserRepository extends JpaRepository<User, Long> {
    // 기본 CRUD 메서드 제공
}
  • LoginController 생성:
@RestController
public class LoginController {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @GetMapping("/login")
    public String login(@RequestParam String username, @RequestParam String password) {
        String sql = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";
        List<User> users = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class));
        if (!users.isEmpty()) {
            return "로그인 성공!";
        } else {
            return "로그인 실패!";
        }
    }
}

주의: 위 코드는 SQL 인젝션에 취약합니다. 실제 애플리케이션에서는 절대 사용하지 마세요!

2단계: SQL 인젝션 공격 시도

2.1 정상 로그인 시도

  • URL 예시:
    http://localhost:8080/login?username=admin&password=admin123
  • 결과: "로그인 성공!" 출력

2.2 SQL 인젝션 공격 시도

  • URL 예시:
    http://localhost:8080/login?username=admin&password=' OR '1'='1
  • 결과: "로그인 성공!" 출력
  • 설명: ' OR '1'='1이 항상 참이 되어 인증을 우회할 수 있습니다.

3단계: SQL 인젝션 방어 구현

3.1 PreparedStatement 사용하기

  • LoginController 수정:
@RestController
public class LoginController {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @GetMapping("/login-secure")
    public String loginSecure(@RequestParam String username, @RequestParam String password) {
        String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
        List<User> users = jdbcTemplate.query(sql, new Object[]{username, password}, new BeanPropertyRowMapper<>(User.class));
        if (!users.isEmpty()) {
            return "로그인 성공!";
        } else {
            return "로그인 실패!";
        }
    }
}
  • 테스트:
    • 동일한 SQL 인젝션 시도를 했을 때, "로그인 실패!"가 출력되어야 합니다.

3.2 Spring Data JPA 사용하기

  • UserRepository 수정:
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsernameAndPassword(String username, String password);
}
  • LoginController 수정:
@RestController
public class LoginController {

    @Autowired
    private UserRepository userRepository;

    @GetMapping("/login-jpa")
    public String loginJPA(@RequestParam String username, @RequestParam String password) {
        Optional<User> user = userRepository.findByUsernameAndPassword(username, password);
        if (user.isPresent()) {
            return "로그인 성공!";
        } else {
            return "로그인 실패!";
        }
    }
}
  • 테스트:
    • SQL 인젝션 시도 시 "로그인 실패!"가 출력되어야 합니다.

3.3 입력 값 검증 및 이스케이프 처리

  • LoginController 수정:
import org.springframework.util.StringUtils;

@RestController
public class LoginController {

    @Autowired
    private UserRepository userRepository;

    @GetMapping("/login-validate")
    public String loginValidate(@RequestParam String username, @RequestParam String password) {
        if (!isValid(username) || !isValid(password)) {
            return "입력값에 문제가 있습니다.";
        }

        Optional<User> user = userRepository.findByUsernameAndPassword(username, password);
        if (user.isPresent()) {
            return "로그인 성공!";
        } else {
            return "로그인 실패!";
        }
    }

    private boolean isValid(String input) {
        // 간단한 예시: SQL 특수 문자 포함 여부 체크
        return StringUtils.hasText(input) && !input.matches(".*['\";--].*");
    }
}
  • 테스트:
    • SQL 인젝션 시도가 입력값 검증에서 차단되어 "입력값에 문제가 있습니다."가 출력됩니다.

4단계: 보안 테스트 수행

4.1 자동화 도구 사용하기

  • OWASP ZAP(https://www.zaproxy.org/)과 같은 웹 애플리케이션 보안 테스트 도구를 사용해 애플리케이션을 스캔합니다.
  • 테스트 방법:
    1. OWASP ZAP을 설치하고 실행합니다.
    2. 브라우저를 OWASP ZAP 프록시로 설정합니다.
    3. 위에서 구현한 로그인 엔드포인트를 여러 방식으로 테스트합니다.
    4. 취약점이 발견되는지 확인하고, 수정된 엔드포인트에서는 취약점이 없는지 검증합니다.

4.2 단위 테스트 작성하기

  • JUnit과 Mockito를 사용해 SQL 인젝션 방어 메커니즘을 테스트합니다.
@SpringBootTest
public class LoginControllerTest {

    @Autowired
    private LoginController loginController;

    @Test
    public void testLoginSecure_SQLInjection() {
        String response = loginController.loginSecure("admin", "' OR '1'='1");
        assertEquals("로그인 실패!", response);
    }

    @Test
    public void testLoginJPA_SQLInjection() {
        String response = loginController.loginJPA("admin", "' OR '1'='1");
        assertEquals("로그인 실패!", response);
    }

    @Test
    public void testLoginValidate_SQLInjection() {
        String response = loginController.loginValidate("admin", "' OR '1'='1");
        assertEquals("입력값에 문제가 있습니다.", response);
    }
}

추가 실습 아이디어

  1. ORM의 장점 탐구하기:

    • Spring Data JPA 외에도 Hibernate나 MyBatis와 같은 ORM 프레임워크를 사용해 SQL 인젝션 방어를 구현해봅니다.
  2. 권한 관리 추가하기:

    • 로그인 성공 시 JWT 토큰을 발급하고, 권한에 따라 접근을 제어하는 기능을 추가합니다.
    • 이를 통해 최소 권한 원칙을 적용하는 방법을 배울 수 있습니다.
  3. 애플리케이션 방화벽(WAF) 설정하기:

    • 로컬 환경에서 WAF 역할을 하는 필터를 구현해 SQL 인젝션 시도를 차단해봅니다.
  4. 로그 모니터링 및 알림 설정하기:

    • 보안 이벤트를 로그로 기록하고, 이상 징후가 감지되면 알림을 보내는 시스템을 구축해봅니다.

마무리

이번 실습을 통해 SQL 인젝션의 개념을 이해하고, 이를 방어하는 다양한 방법을 직접 구현해보았습니다. 이러한 실습은 실제 프로젝트에서 발생할 수 있는 보안 취약점을 예방하고, 안전한 애플리케이션을 개발하는 데 큰 도움이 될 것입니다.

추가 팁:

  • OWASP Top Ten을 지속적으로 학습하여 다양한 웹 보안 취약점과 대응 방법을 익히세요.
  • 코드 리뷰정기적인 보안 점검을 통해 애플리케이션의 보안 상태를 유지하세요.
  • 보안 관련 커뮤니티블로그를 팔로우하여 최신 보안 동향을 파악하세요.

화이팅입니다! 🚀

0개의 댓글