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'
이 항상 참이 되므로, 인증 없이 로그인에 성공하게 될 수 있습니다.
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 인젝션을 방지합니다.
// 예시: Spring Data JPA 리포지토리 인터페이스
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsernameAndPassword(String username, String password);
}
입력 값 검증(Input Validation) 및 이스케이프 처리
사용자로부터 입력받은 데이터가 예상한 형식인지 검증하고, 필요에 따라 이스케이프 처리를 합니다. 예를 들어, 입력값에 특수 문자가 포함되어 있는지 확인하고 제거할 수 있습니다.
최소 권한 원칙 적용
데이터베이스 사용자에게 필요한 최소한의 권한만 부여하여, 만약 인젝션 공격이 성공하더라도 피해를 최소화할 수 있습니다. 예를 들어, 읽기 전용 사용자 계정을 사용하면 데이터베이스 수정이나 삭제를 방지할 수 있습니다.
웹 애플리케이션 방화벽(WAF) 사용
WAF를 사용하면 SQL 인젝션 시도를 실시간으로 탐지하고 차단할 수 있습니다. 이는 추가적인 보안 계층을 제공하여 애플리케이션을 보호합니다.
보안 테스트 및 코드 리뷰 수행
정기적으로 보안 테스트를 실시하고 코드 리뷰를 통해 잠재적인 SQL 인젝션 취약점을 찾아 수정합니다. OWASP의 SQL Injection 관련 가이드를 참고하면 도움이 됩니다.
물론입니다! 신입 Java/Spring 백엔드 개발자로서 SQL 인젝션을 이해하고 방어하는 방법을 실습을 통해 체득하는 것은 매우 중요합니다. 아래에 단계별로 따라 할 수 있는 실습 예제를 제공할게요. 이 실습을 통해 SQL 인젝션의 취약점을 직접 경험하고, 이를 방어하는 다양한 방법을 구현해볼 수 있습니다.
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');
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
// Getters and Setters
}
public interface UserRepository extends JpaRepository<User, Long> {
// 기본 CRUD 메서드 제공
}
@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 인젝션에 취약합니다. 실제 애플리케이션에서는 절대 사용하지 마세요!
http://localhost:8080/login?username=admin&password=admin123
http://localhost:8080/login?username=admin&password=' OR '1'='1
' OR '1'='1
이 항상 참이 되어 인증을 우회할 수 있습니다.@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 "로그인 실패!";
}
}
}
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsernameAndPassword(String username, String password);
}
@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 "로그인 실패!";
}
}
}
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(".*['\";--].*");
}
}
@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);
}
}
ORM의 장점 탐구하기:
권한 관리 추가하기:
애플리케이션 방화벽(WAF) 설정하기:
로그 모니터링 및 알림 설정하기:
이번 실습을 통해 SQL 인젝션의 개념을 이해하고, 이를 방어하는 다양한 방법을 직접 구현해보았습니다. 이러한 실습은 실제 프로젝트에서 발생할 수 있는 보안 취약점을 예방하고, 안전한 애플리케이션을 개발하는 데 큰 도움이 될 것입니다.
추가 팁:
화이팅입니다! 🚀