SQL Injection 그리고 PreparedStatement 본문

우아한테크코스 4기

SQL Injection 그리고 PreparedStatement

giron 2022. 9. 17. 12:10
728x90

SQL 인젝션이란?

데이터베이스와 연동된 웹 애플리케이션에 공격자가 입력이 가능한 폼에 조작된 질의문 삽입하여 디비 정보 열람 및 정보를 조작하는 공격

사례

단순한 기법이지만 강력한 공격이어서 반드시 주의해야 한다!

예방 방법

에러처리를 잘해서 테이블 정보를 사용자들에게 공개하지 않도록 해야한다. 테이블 정보 노출 및 컬럼 노출로 sql쿼리 작성이 가능하기 때문이다.

방어 방법

파라미터 바인딩! 직접 쿼리를 작성하지 않고 파라미터 바인딩을 사용하면 된다. 예시를 들어 설명해보겠다.

SQL Injection에 취약한 코드

statement = connection.createStatemnt();
String query = "SELECT * FROM USERS WHERE name = '" + loginName + "' AND password = '"+loginPassword="'";
boolen resultSet = statement.execute(query);

일반적으로 sql문법은  ' '안에 파라미터를 넣어 문자를 구별한다. 따라서 텍스트 창에 'OR 1=1-- 와 같이 입력을 하면 

' 으로 앞의 쿼리문을 닫고 or연산으로 1=1과 같이 트루인 로직을 넣어서 공격하는 기법이다.

SQL Injection 방어 코드

String prepareStatement = "SELECT * FROM USERS WHERE name = ? AND password =?;
PreparedStatement preparedStatement = connection.prepareStatement(prepareStatement);
preparedStatement.setString(1, loginName);
preparedStatement.setString(2, loginPassword);

 

이렇게 간단히 막을 수 있다. 그런데 왜 파라미터 바인딩을 사용하면 안전할까? 

파라미터 바인딩을 사용하면 안전한 이유

preparedStatement.setString(); 의 setString내부를 살펴보겠다.

org/h2/jdbc/JdbcPreparedStatement.java

PreparedStatement 내부

quote내부로 들어가면 아래와 같이 StringUtils.quoteJavaString() 메서드가 나온다.

quote

해당 메서드에 들어가면 아래가 나오고 다시 javaEncode에 들어간다.

quote 내부

아래처럼 String으로 들어온 값들을 하나씩 돌면서 java스타일에 맞게 인코딩해준다.

자동 인코딩
자동 인코딩

위와 같이 자동으로 sql injection에 대해서 막아준다. 즉, Prepared Statement를 사용하면 인자를 넣어주기 전의 쿼리를 DBMS가 미리 컴파일하여 대기하므로 이후, 인자에 대해서는 쿼리가 아닌 단순 문자열로 인식하기 때문에 안전하다.

따라서 직접 작성하지 말고 파라미터 바인딩을 이용하자!!

이외에도 에러가 나올 때, 어떤 DBMS를 쓰는지 노출하지 않는 것도 중요하다. DBMS에 따라 쿼리 문법이 조금씩 다르기 때문이다.

Statement vs PreparedStatement

두 개의 차이는 캐시 사용여부이다.

sql 쿼리 처리 순서

statement를 사용하면 매 번 위의 과정을 반복하면서 이루어지지만 preparedStatement는 처음에 모든 과정이 일어날 때, parse의 과정 이후를 캐시에 저장하므로 같은 쿼리문을 재사용할 때, 해당 캐시를 사용하므로 성능상 좋다. (전달되는 파라미터의 값이 달라도 같은 쿼리로 인식해서 처리하므로)

즉, 동일한 sql문을 반복 사용할 때 좋다. 또한 내부적으로 바인딩 처리할 때, sql injection을 막아주므로 좋다.

statement

String sql = "SELECT NAME, AGE FROM TABLE WHERE USERID = " + userID
Statement stmt = conn.credateStatment();
ResultSet result = stmt.executeQuery(sqlstr);

prepared statement

String sql = "SELECT NAME, AGE FROM TABLE WHERE userID = ?"
PreparedStatement stmt = conn.prepareStatement(sql);
pstmt.setInt(1, userID);
ResultSet rst = pstmt.executeQuery();

jpql

jpql을 사용할 때도 아래와 같이 사용한다면 똑같이 SQL Injection위험이 있다.

public List<AccountDTO> unsafeJpaFindAccountsByCustomerId(String customerId) {    
    String jql = "from Account where customerId = '" + customerId + "'";        
    TypedQuery<Account> q = em.createQuery(jql, Account.class);        
    return q.getResultList()
      .stream()
      .map(this::toAccountDTO)
      .collect(Collectors.toList());        
}

따라서 아래와 같이 파라미터 바인딩을 사용하자

String jql = "from Account where customerId = :customerId";
TypedQuery<Account> q = em.createQuery(jql, Account.class)
  .setParameter("customerId", customerId);

그리고 바인딩 문은 values의 값에만 사용할 수 있다. table같은 경우는 할 수없다.

아래와 같이 동적으로 table 명을 바꾸려는 문장은 런타임 에러가 발생한다.

// This WILL NOT WORK !!!
PreparedStatement p = c.prepareStatement("select count(*) from ?");
p.setString(1, tableName);
// This WILL NOT WORK EITHER !!!
String jql = "select count(*) from :tableName";
TypedQuery q = em.createQuery(jql,Long.class)
  .setParameter("tableName", tableName);
return q.getSingleResult();
The main reason behind this is the very nature of a prepared statement: database servers use them to cache the query plan required to pull the result set, which usually is the same for any possible value. This is not true for table names and other constructs available in the SQL language such as columns used in an 
order by clause.

주된 이유는 preparedStatement의 특성 때문이다. DB서버는 이것들을 캐싱하여 사용한다. 하지만 테이블의 이름을 동적으로 두면 캐싱할 수 없기 때문에 table의 이름을 바인딩으로 넣을 순 없다.

 

Reference

https://www.baeldung.com/sql-injection

728x90
Comments