[JDBC] 2020.12.30. day_47 OWASP , SQL Injection, SQL Injection 방어하기

2020. 12. 30. 18:46Web_Back-end/JDBC

 

◎ OWASP 

  OWASP
  Open Web Application Security Project
  기간 내 발생한 보안사고를 정리한 프로젝트
  owasp.org 으로 접속가능

○ SQL Injection _ Statement를 사용하면 발생할 수 있다

  SQL Injection
zipcode 파일 : 상단글 참조 zipcode 파일을 통해 역으로 테이블, 컬럼들, 정보를 찾아보기
테이블 명 얻기
  '1--
  'union select  tname from tab--
  'union select  tname, '0' , '0', '0', '0'  from tab--
 
컬럼명 얻기
  select * from user_tab_cols;
  ' union select column_name, '0', '0', '0', '0' from user_tab_cols where table_name = 'EMP'--
  이 작업을 통해 컬럼명을 얻을 수 있다
레코드 얻기
  ' union select empno||'~', ename, job, '0', '0' from emp-- 
 

 

※ SQLInjection을 방어하는 코드 작성해보기 

_ 1, PreparedStatement 의 바인드변수를 사용하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
public List<ZipcodeVO> selectStatementZipcode(String dong) throws SQLException {
        List<ZipcodeVO> list = new ArrayList<ZipcodeVO>();
 
        // SQLInjection 방어하기
        // 방어하는 코드 :
        // 1. ', -- 등의 특수문자를 없앤다 (Web에서는 제거가능)
        // 2. 공백을 체거한다
        // 3. 쿼리문에 관련된 문장을 제거한다 (ex: select, from, where,,,)
        
        if(dong.contains("'")||dong.contains("-")||dong.contains(" ")) {
            dong = dong.replace("'"""); // ' 를 제거
            dong = dong.replace("-"""); // - 를 제거        
            dong = dong.replace(" """); // white space를 제거        
            
        }
 
        // 1. 드라이버 로딩
        try {
            Class.forName("oracle.jdbc.OracleDriver");
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
 
        Connection con = null;
        Statement stmt = null//SQLInjection 발생할 수 있다
        ResultSet rs = null;
 
        String url = "jdbc:oracle:thin:@localhost:1521:orcl";
        String id = "scott";
        String pass = "tiger";
        try {
            // 2. 커넥션 얻기
            con = DriverManager.getConnection(url, id, pass);
            // 3. 쿼리문 생성 객체 얻기
            StringBuilder selectZipcode = new StringBuilder();
            
            selectZipcode
            .append("    select    zipcode, sido, gugun, dong, nvl(bunji, ' ') bunji")
            .append("    from    zipcode")
            .append("    where    dong like '")
            .append(dong)
            .append("%'"); 
            stmt = con.createStatement();
            
            // 4. 쿼리 수행 후 결과 얻기
            rs = stmt.executeQuery(selectZipcode.toString());
            
            String zipcode = "", sido = "", gugun = "", db_dong = "", bunji = ""
//매개변수와 dong이 같은이름을 가져서 따로 표기
            ZipcodeVO zv = null;
            
            while(rs.next()) {
                zipcode = rs.getString("zipcode");
                sido = rs.getString("sido");
                gugun = rs.getString("gugun");
                db_dong = rs.getString("dong");
                bunji = rs.getString("bunji");
                
                zv = new ZipcodeVO(zipcode, sido, gugun, db_dong, bunji);
                
                list.add(zv);
            }
            
        } finally {
            // 5. 연결끊기
            if (rs != null) {
                rs.close();
            }
            if (stmt != null) {
                stmt.close();
            }
            if (con != null) {
                con.close();
            }
        }
 
        return list;
    }
cs
 
 
 

 

※ preparedStatement 는 bind변수에 query가 들어갈 수 없기 때문에 SQLInjection이 발생하지 않는다

 


※ id, pass 입력하는 코드를 만들고, SQLInjection 실험해보기

 

▽1.  id, password를 입력하고 로그인 성공여부를 출력하는 클래스 작성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
package day1230;
 
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.sql.SQLException;
 
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPasswordField;
import javax.swing.JTextField;
import javax.swing.border.TitledBorder;
 
 
@SuppressWarnings("serial")
public class IdForm extends JFrame {
 
    private JTextField jtfId;
    private JPasswordField jpfPass;
    private JLabel jlOutput;
 
    private IdForm idFrm;
 
    public IdForm() {
        super("로그인");
        idFrm = this;
        jtfId = new JTextField();
        jpfPass = new JPasswordField();
        jlOutput = new JLabel("출력창");
 
        jtfId.setBorder(new TitledBorder("아이디"));
        jpfPass.setBorder(new TitledBorder("비밀번호"));
        jlOutput.setBorder(new TitledBorder("출력창"));
 
        setLayout(new GridLayout(31));
 
        add(jtfId);
        add(jpfPass);
        add(jlOutput);
 
        // inner class로 이벤트 처리
//        IdForm.EvtInner ei = new this.EvtInner(); 
        EvtInner ei = new EvtInner(); // 위와같이 명시해서 생성해도된다
        jtfId.addActionListener(ei);
        jpfPass.addActionListener(ei);
 
        setBounds(100100300250);
        setVisible(true);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
    }// IdForm
 
    ////////////////// inner class로 Event 처리////////////////////////////
 
    public class EvtInner implements ActionListener {
 
        @Override
        public void actionPerformed(ActionEvent ae) {
            if (ae.getSource() == jtfId) {
                idNull();
            }
            if (ae.getSource() == jpfPass) {
                passNull();
            }
        }
 
        public void idNull() {
            if ("".equals(jtfId.getText().trim())) {
                JOptionPane.showMessageDialog(idFrm, "아이디는 필수입력입니다");
                jtfId.requestFocus();
                return;
            }
            jpfPass.requestFocus();
        }
 
        public void passNull() {
            if ("".equals(new String(jpfPass.getPassword()).trim())) {
                JOptionPane.showMessageDialog(idFrm, "비밀번호는 필수입력입니다");
                jpfPass.requestFocus(); // 커서를 jpfPass로 이동
                return;
            }
            login();
        }
 
        public void login() {
            String id = jtfId.getText().trim();
            String pass = new String(jpfPass.getPassword()).trim();
            
            LoginDAO lDAO = LoginDAO.getInstance();
            try {
                String name = lDAO.login(id, pass);
                
                if("".equals(name)) {//login 실패 - 이름이 존재하지 않음
                    JOptionPane.showMessageDialog(idFrm, "아이디/비밀번호를 확인해주세요");
                }else {
                    jlOutput.setText(name + "님이 로그인 하셨습니다.");
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
 
    }
 
    ////////////////// inner class로 Event 처리////////////////////////////
 
    public static void main(String[] args) {
        new IdForm();
    }// main
 
}// class
 
cs

▽2. Singleton pattern 을 사용한 DAO 클래스

받아온 id, password를 DB와 연동해서 조회하는 method 가 있다 //Statement사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package day1230;
 
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
 
public class LoginDAO {
 
    private static LoginDAO LDAO;
 
    private LoginDAO() {
 
    }
 
    public static LoginDAO getInstance() {
        if (LDAO == null) {
            LDAO = new LoginDAO();
        }
        return LDAO;
    }
 
    public String login(String id, String passwd) throws SQLException {
        String name = "";
        
        ////////// Statement 사용
 
        // 1. 드라이버 로딩
        try {
            Class.forName("oracle.jdbc.OracleDriver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
 
        Connection con = null;
        Statement stmt = null;
        ResultSet rs = null;
 
        String url = "jdbc:oracle:thin:@localhost:1521:orcl";
        String dboId = "scott";
        String dboPass = "tiger";
        
        try {
            // 2. Connection 얻기
            con = DriverManager.getConnection(url, dboId, dboPass);
            // 3. 쿼리문 생성객체 얻기
            stmt = con.createStatement();
            // 4. 쿼리문 수행 후 결과 얻기
            StringBuilder selectId = new StringBuilder();
            selectId
            .append("    select    name    ")
            .append("    from    test_login    ")
            .append("    where    id='").append(id).append("' and pass='")
.append(passwd).append("'");
            
            rs = stmt.executeQuery(selectId.toString());
            
            if(rs.next()) {
                name = rs.getString("name");
            }
            
        } finally {
            // 5. 연결끊기
            if (rs != null) {
                rs.close();
            }
            if (stmt != null) {
                stmt.close();
            }
            if (con != null) {
                con.close();
            }
        }
 
        return name;
    }
 
}
 
cs

 

  무조건 성공하는 방법
값 부분에 select  컬럼명
from   테이블명
where 컬럼명 = '' and 컬럼명 = '값';
' or 1=1 --
을 입력하면,
select  컬럼명
from   테이블명
where 컬럼명 = '' or 1=1 --' and 컬럼명 = '값';
실행 결과 

로그인이 무조건 성공한다

※ SQLInjection 을 방어하는 코드를 작성해서, 방어해보기

_ 2. 코드를 직접 작성 : 쿼리문을 조작

 

위의 loginDAO 클래스의 public static LoginDAO getInstance() 안에 코드 작성
//SQLInjection 방어코드
        id = id.replaceAll("'", "").replaceAll(" ", "");
 
package day1230;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class LoginDAO {

	private static LoginDAO LDAO;

	private LoginDAO() {

	}

	public static LoginDAO getInstance() {
		if (LDAO == null) {
			LDAO = new LoginDAO();
		}
		return LDAO;
	}

	public String login(String id, String passwd) throws SQLException {
		String name = "";
		
		//SQLInjection 방어코드
		id = id.replaceAll("'", "").replaceAll(" ", "");
		
		
		////////// Statement 사용
		// 1. 드라이버 로딩
		try {
			Class.forName("oracle.jdbc.OracleDriver");
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}

		Connection con = null;
		Statement stmt = null;
		ResultSet rs = null;

		String url = "jdbc:oracle:thin:@localhost:1521:orcl";
		String dboId = "scott";
		String dboPass = "tiger";
		
		try {
			// 2. Connection 얻기
			con = DriverManager.getConnection(url, dboId, dboPass);
			// 3. 쿼리문 생성객체 얻기
			stmt = con.createStatement();
			// 4. 쿼리문 수행 후 결과 얻기
			StringBuilder selectId = new StringBuilder();
			selectId
			.append("	select	name	")
			.append("	from	test_login	")
			.append("	where	id='").append(id).append("' and pass='").append(passwd).append("'");
			
			rs = stmt.executeQuery(selectId.toString());
			
			if(rs.next()) {
				name = rs.getString("name");
			}
			
		} finally {
			// 5. 연결끊기
			if (rs != null) {
				rs.close();
			}
			if (stmt != null) {
				stmt.close();
			}
			if (con != null) {
				con.close();
			}
		}

		return name;
	}

}