JSP/서블릿 흝어 보기

PreparedStatement

1. 가독성와 유지보수가 좋다.

PreparedStatement( ) 메소드를 통해 PreparedStatement 객체를 생성할 때 인자값으로 실행할 SQL문을 지정하는데, 값을 동적으로 지정해야할 때 ? 기호로 대체할 수 있습니다. 다음을 통해 Statement와 PreparedStatement를 비교해보자

String id = request.getParameter("id");
String pwd = request.getParameter("pwd");

//Statement
Statement stmt = conn.createStatement();
stmt.executeUpdate("insert into test values('"+id+"', '"+pwd+"')");

//PreparedStatement
PreparedStatement pstmt = conn.prepareStatement("insert into test (id, pwd) values(?, ?)");
pstmt.setString(1, id);
pstmt.setString(2, pwd);
pstmt.executeUpdate();

우선 Statement의 executeUpdate( ) 메소드를 사용하여 insert 명령문을 실행하는 부분을 보면 작은 따옴 쉼표를 짝을 맞추어 넣어주어야 한다. 그런데 실제로 이런 쿼리를 작성하다보면 하나를 빠트려 먹어 오류가 발생해서 애를 먹기도 한다. 

stmt.executeUpdate("insert into test values('"+id+"', '"+pwd+"')");

그렇지만 PreparedStatement는 값을 정하는 부분에 ? 기호로 대체하여 이후의 코드에서 아래처럼 setter 메소드를 이용해 ? 자리에 값을 지정하고 있다. 

PreparedStatement pstmt = conn.prepareStatement("insert into test (id, pwd) values(?, ?)");
pstmt.setString(1, id);
pstmt.setString(2, pwd);

이런식의 코드는 가독성이 높고 유지보수하기 좋다.

2. 쿼리 실행계획이 재사용된다. 

재사용 된다니!! 뻘짓이 줄어드는 소리가 들린다!! 그런데.. 우리보다는 DB서버가 쿼리를 처리하는 과정을 반복하지 않고 한번만 처리하여 퍼포먼스를 높이는 것을 의미한다.

일반적인 Statement를 사용하여 SELECT 쿼리를 입력했을 때에는 매번 parser와 fetch까지 모든 과정을 수행합니다. PreaparedStatement 경우에는 효율을 높이기 위해 parse과정을 최초 한번만 수행하고 이후에는 생략할 수 있습니다. parse 과정을 모두 거친 후에 생성된 결과는 메모리 어딘가에 저장 해두고 필요할 때마다 사용하며, 자주 변경되는 부분을 변수로 선언해 두고, 매번 다른 값을 바인딩하여 사용합니다. 

(참고: http://blog.skinfosec.com/220482240245)

다음의 소스코드에서는 세명의 회원정보를 생성하여 리스트에 담습니다. 그리고 이를 DB에 삽입할 쿼리를 PreparedStatement 를 이용해 문법처리과정을 한번만 수행하도록 하고 반복문으로 리스트를 순회하면서  PreparedStatement 객체의 setter 메소드를 이용해 데이터를 바인딩만 해준 뒤 executeUpdate( ) 메소드로 DB에 전달하고 있습니다.

List<MemberBean> memberList = new ArrayList<MemberBean>();

MemberBean m1 = new MemberBean();
m1.setUserId("user1");
m1.setPasswd("123");
memberList.add(m1);

MemberBean m2 = new MemberBean();
m2.setUserId("user2");
m2.setPasswd("456");
memberList.add(m2);

MemberBean m3 = new MemberBean();
m3.setUserId("user3");
m3.setPasswd("789");
memberList.add(m3);

//PreparedStatement
PreparedStatement pstmt = conn.prepareStatement("insert into test (id, pwd) values(?, ?)");

int count = memberList.size();
MemberBean tMember;

for(int i=0;i<count;i++)
{
    tMember = memberList.get(i);
	
	pstmt.setString(1, tMember.getUserId());
	pstmt.setString(2, tMember.getPasswd());
	pstmt.executeUpdate();
}

3. SQL인젝션 취약점을 보완할 수 있다.

아래는 질의문자열로 전달된 아이디와 패스워드가 일치하는 회원을 보여주는 프로그램이다. 먼저 살펴 볼 이 소스코드는 Statement를 사용시 SQL인젝션 취약점을 보여준다.

jdbc_sqlinjection.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ page import="java.sql.*, java.util.*, job.study.beans.MemberBean" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<%
//1. JDBC 드라이버 로딩하기
Class.forName("oracle.jdbc.driver.OracleDriver");

//2. DB 서버 접속하기
String url = "jdbc:oracle:thin:@localhost:1521:xe";
Connection conn = DriverManager.getConnection(url,"scott","tiger");

String userid = request.getParameter("userid");
String pwd = request.getParameter("pwd");
out.println("사용자 정보");
out.println("<hr>");
out.println("+ 아이디 : " + userid+"<br/>");
out.println("+ 패스워드 : " + pwd+"<br/>");
out.println("<hr>");

ResultSet rs;

Statement stmt = conn.createStatement();

String sql = "select * from test where id='"+userid+"' and pwd='"+pwd+"' ";
out.println("<br/>실행쿼리 :<hr>"+sql+"<br/>");
out.println("<hr>");
rs = stmt.executeQuery(sql);

out.println("<br/>검색된 회원 :");
out.println("<hr>");
while(rs.next()){
  out.println(rs.getString("id")+":"+rs.getString(2)+"<br/>");    
}
out.println("<hr>");
rs.close();
stmt.close();
conn.close();

%>
</body>
</html>

다음은 정상적인 질의 문자열을 포함한 요청으로 실행된 화면으로 정확하게 파라미터로 입력한 아이디와 패스워드가 일치하는 회원이 조회되고 있다.

전달된 질의문자열은 userid=aa&pwd=11 이다. 

다음은 자주 들었던 SQL인젝션에 해당하는 공격이다.

고의적으로 SQL을 조작하여 모든 회원의 아이디와 패스워드가 조회되고 있다. 화면에서 실행쿼리를 보면 우리가 작성한 SQL이 프로그램 의도와 다르게 조작된 것을 볼 수 있다. 이것이 SQL인젝션이다.  

전달된 질의문자열은 userid=' or 1=1 -- 이다.  

or 1=1에 의해 id='' 조건 검사와 상관 없이모든 행이 조건에 만족하며  -- 은 주석을 의미하는 것으로 이후에 패스워드를 검사하는 조건절이 실행되지 않는다. 결과적으로 모든 행이 조회가 된다.

만약 질의문자열을 preparedStatement를 이용해 파라미터를 바인딩하는 방법을 선택했다면 공격자의 의도에 따른 의미있는 쿼리로 동작하지 않을 것입니다.

 

댓글

댓글 본문