SQL Injection 을 설명하기 위해서 샘플 코드를 만들어보았습니다.
아래는 로그인 승인을 하는 서블릿 샘플 코드입니다.
package kr.co.kcsp.fortify.sample; import java.io.IOException; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * @author kcjang * */ public class LoginServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String userId = request.getParameter("userId"); String userPass = request.getParameter("userPass"); String content=null; ResultSet rs=null; Connection con=null; Statement stmt=null; String url = "jdbc:mysql://localhost/test"; try { Class.forName("com.mysql.jdbc.Driver"); System.out.println("MySQL JDBC Driver loading Success!"); } catch(ClassNotFoundException e) { System.err.println("MySQL JDBC Driver loading error!"); System.err.println(e.toString()); return; } //쿼리문 실행 try{ con = DriverManager.getConnection(url,"cyber","cyber"); stmt= con.createStatement(); String sql="select id,pass from member where id = '"+userId+"' "; sql += " and pass = '"+userPass+"'"; // 쿼리 를 실행 System.out.println("SQL : " + sql); rs = stmt.executeQuery(sql); request.getSession().setAttribute("query", sql); if (rs.next()) { request.getSession().setAttribute("userId", userId); response.sendRedirect(request.getContextPath()+"/login_ok.jsp"); } else { response.sendRedirect(request.getContextPath()+"/login_fail.jsp"); } }catch(Exception e){ System.err.println("rs.next() Error "); System.err.println(e.toString()); return; } finally { try { if (rs != null) rs.close(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { try { if (stmt != null) stmt.close(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { try { if (con != null) con.close(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doGet(req,resp); } }
기능은 간단하게 아이디와 비밀번호를 입력받아 쿼리를 만들고 매칭되는 결과 셋이 있으면
로그인을 승인해주고 없으면 승인을 하지 않는 루틴입니다.
물론 실제로 로그인 승인 기능을 만든다면 ID로 쿼리를 한 결과에서 비밀번호를 읽어와
서블릿에서 String 비교를 수행하는 형태로 코딩을 하겠지만 위와 같은 코딩도 가능하다고 생각합니다.
실제로 옛날에 그렇게 코딩했던 생각도 납니다.
위 소스코드는 많은 위험성을 내포하고 있습니다.
아래 포스팅된 게시물의 SQL Injection 과 같은 < ‘ OR ‘a’=’a > 문자를 비밀번호로 입력한다면
select id,pass from member where id = 'kcjang' and pass = '' OR 'a'='a'
라는 쿼리가 되어서 항상 로그인은 승인되게 됩니다.
이것보다 더한 위험성은 만약 < ‘; delete from member; — > 을 비밀번호로 입력한다면
모든 회원 정보가 날아가 버리는 사태가 발생합니다. 위 소스는 mysql을 사용하므로 그와 같은 쿼리를 허용하지 않지만
(mysql을 잘 사용하지 않는데 실제로 허용하지 않나요? 이건 테스트를 해보고 게시물을 수정하겠습니다.)
Microsoft(R) SQL Server 2000을 포함한 많은 데이터베이스 서버에서 여러 SQL 문을 세미콜론으로 구분하여 한꺼번에 실행하는 것을 허용합니다. 물론 마지막의 “–” 는 이후 쿼리를 전부 주석처리해버리는 역할을 할테고요…
그렇다면 이런 SQL Injection의 위험성을 어떻게 방지할까요?
첫번째는 사용자 입력값의 검증입니다.
Single Quotate나 Dash 같은 문자의 블랙리스트를 만들어 응답을 거부하는 방법이 블랙리스트이고
특정한 길이의 알파벳 문자열만을 받아들이는 등의 화이트리스트 방법도 있겠습니다.
보다 안전한것은 화이트리스트를 이용하는 방법이 되겠죠.
두번째는 매개변수가 있는 SQL문을 사용하는 방법입니다.
매개 변수가 있는 SQL 문은 일반 SQL 문자열을 사용하여 생성되지만, 사용자가 제공하는 데이터를 포함해야 하는 경우에 이후에 삽입되는 데이터의 자리 표시자인 바인딩 매개 변수를 포함합니다. 다시 말해, 바인딩 매개 변수를 사용하여 프로그래머가 명령으로 처리해야 할 것과 데이터로 처리해야 할 것을 데이터베이스에 명시적으로 지정할 수 있습니다. 프로그램이 SQL 문을 실행할 준비가 되면 각 바인딩 매개 변수에 사용할 런타임 값을 데이터베이스에 지정하여 데이터가 명령 수정 코드로 해석될 위험을 피할 수 있습니다.
위의 코드를 수정하면 다음과 같이 되겠네요.
String sql="select id,pass from member where id = ? and pass = ?"; PreparedStatement stmt= con.prepareStatement(); stmt.setString(1,userId); stmt.setString(2,userPass); rs = stmt.execute();
위와 같이 한다면 좀더 안전한 코드를 만들수 있겠죠?
그리고 덧붙여서 위의 서블릿 샘플코드를 Fortify로 돌리면
Hot 3개, Warning 10개, Info 7개 , 초 20개의 위험성이 우루루~~ 쏟아집니다.
댓글 없음:
댓글 쓰기