머나 먼 옛날, 군대를 다니던 시절 소프트웨어 유지보수를 하고있었던 시절이였다.
평화롭던 나날들을 보내고있었는데, 출근을 하였더니, 모든 계정에 대해서 블락이 되었던 때가 있었다. 뭘까? 뭐길래 전 유저들의 로그인 5회 실패시에 뜨는 계정블록이 되었을까?
일단 바로 수습에 들어갔다. 운영 데이터에서 로그인 실패수를 초기화 해주는 쿼리를 돌렸다. 이제 수습은 끝났고, 이제 원인을 찾아서 해당 문제를 해결해야 한다. 로그인 실패 카운트가 올라간 시간을 찾아보니, 같은 시간이였고, 문제가 컸음을 느꼇다. 한번의 시도로 전 유저들의 실패를 올릴수 있는 방법, 실패한 유저의 아이디 부분을 무시하고, 실패 카운트를 올리면 된다.
로그인 쿼리와 실패시 실패 카운트를 올리는 쿼리를 확인해보았다. 아뿔싸 로그인 쿼리는 PreparedStatement인데 반해, 실패 카운트는 Statement이였다. 전임자가 싸둔똥을 찾은 느낌은 매우 더러웠다. 시나리오는 이러했다. 누군가가 공격을 했고, 유저 아이디로 ' or 1=1 #를 이용하여 로그인 시도를 한 것이다.
다른 분들은 위와 같은 문제를 후임자에게 겪게 하지 않았으면 하고자, 이 포스트를 적는다. 이번 포스트는 실험이 위주이므로 적당히 훑어 보시면 된다.
아래의 코드를 준비하자.
package kr.co.sejiwork; import java.sql.*; public class Main { public static void main(String[] args) throws Exception { String url = "jdbc:mysql://localhost/test-user?serverTimezone=UTC"; String id = "tester"; String pw ="0000"; Connection conn = DriverManager.getConnection(url, id, pw); String userId = "tester2"; String userPwd = "5678"; String loginSql = "SELECT *\r\n" + " FROM `test-user`.tb_user\r\n" + " WHERE user_id = '" + userId + "' and user_pw = '" + userPwd + "'"; Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(loginSql); while(rs.next()) { System.out.println("user_id : " + rs.getString("user_id")); System.out.println("user_pw : " + rs.getString("user_pw")); } conn.close(); } }
아주 일반적인 코드이다, 유저테이블을 확인해서 user_id, user_pwd가 외부에서 가져온 파라미터와 같은지 확인하는 코드이다.
데이터를 적당히 넣어두면 위와 같은 결과가 잘 나올 것이다.
자 다음으로 아래의 코드를 준비하자.
package kr.co.sejiwork; import java.sql.*; public class Main { public static void main(String[] args) throws SQLException, ClassNotFoundException { String url = "jdbc:mysql://localhost/test-user?serverTimezone=UTC"; String id = "tester"; String pw = "0000"; Connection conn = DriverManager.getConnection(url, id, pw); String userId = "' or 1=1 #"; String userPwd = "5678"; String loginSql = "SELECT *\r\n" + " FROM `test-user`.tb_user\r\n" + " WHERE user_id = '" + userId + "' and user_pw = '" + userPwd + "'"; Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(loginSql); while (rs.next()) { System.out.println("id : " + rs.getInt("id")); System.out.println("user_id : " + rs.getString("user_id")); System.out.println("user_pw : " + rs.getString("user_pw")); } conn.close(); } }
user_id를 ' or 1=1 #로 바꾸었을 때 아래와 같이 나온다.
위와 같이 로그인이 뚫려버렸다. 자 위코드를 PreparedStatement로 바꾸어 보자 아래 코드를 준비하자.
package kr.co.sejiwork; import java.sql.*; public class Main { public static void main(String[] args) throws SQLException, ClassNotFoundException { String url = "jdbc:mysql://localhost/test-user?serverTimezone=UTC"; String id = "tester"; String pw ="0000"; Connection conn = DriverManager.getConnection(url, id, pw); String userId = "' or 1=1 #"; String userPwd = "5678"; String loginSql = "SELECT *\r\n" + " FROM `test-user`.tb_user\r\n" + " WHERE user_id = ? and user_pw = ?"; PreparedStatement pstmt = conn.prepareStatement(loginSql); pstmt.setString(1, userId); pstmt.setString(2, userPwd); ResultSet rs = pstmt.executeQuery(); while(rs.next()) { System.out.println("id : " + rs.getInt("id")); System.out.println("user_id : " + rs.getString("user_id")); System.out.println("user_pw : " + rs.getString("user_pw")); } System.out.println("end"); conn.close(); } }
실행 결과는 아래와 같다.
와우 Statement를 PreparedStatement로 바꾸니 거짓말 처럼 Sql Injection이 예방이 되었다.
그러니, 유저의 입력을 받을 여지가 있는 쿼리는 제발 PreparedStatement를 이용하여, 구현하자. 제발
댓글
댓글 쓰기