SELECT nameFROM loginWHERE id='kim' AND pass='1234';
public boolean login(String id, String pass) { ... StringBuilder sb = new StringBuilder(); sb.append("SELECT name FROM login ") .append("WHERE id='").append(id) .append("' AND pass='").append(pass) .append("'"); ...}
로그인 예제
SQLInjection 예제
test_login DB 생성
CREATE TABLE test_login ( id VARCHAR2(16) CONSTRAINT pk_test_login PRIMARY KEY, pass VARCHAR2(16) NOT NULL, name VARCHAR2(30));INSERT INTO test_login(id, pass, name)VALUES ('jung','1234','정택성');INSERT INTO test_login(id, pass, name)VALUES ('lee','1234','이재현');INSERT INTO test_login(id, pass, name)VALUES ('gong','1234','공선의');INSERT INTO test_login(id, pass, name)VALUES ('kim','1234','김건하');
@SuppressWarnings("serial")public class StatementLogin extends JFrame { private JTextField jtfId; private JPasswordField jpfPass; private JButton jbtLogin; private JButton jbtCancel; public StatementLogin() { super("Statement객체를 사용한 로그인"); jtfId = new JTextField(); jpfPass = new JPasswordField(); jbtLogin = new JButton("로그인"); jbtCancel = new JButton("취소"); setLayout(new GridLayout(3, 1)); JPanel panel = new JPanel(); panel.add(jbtLogin); panel.add(jbtCancel); jtfId.setBorder(new TitledBorder("아이디")); jpfPass.setBorder(new TitledBorder("비밀번호")); add(jtfId); add(jpfPass); add(panel); StatementLoginEvt sle = new StatementLoginEvt(this); jtfId.addActionListener(sle); jpfPass.addActionListener(sle); jbtLogin.addActionListener(sle); jbtCancel.addActionListener(sle); addWindowFocusListener(sle); setBounds(400, 300, 370, 150); setResizable(false); setVisible(true); } public static void main(String[] args) { new StatementLogin(); } // getters
// 사용자 정의 예외처리 클래스// Exception(또는 RuntimeException)을 상속받는다.@SuppressWarnings("serial")public class LoginException extends Exception { public LoginException() { this("로그인 실패"); } public LoginException(String msg) { super(msg); // 예외처리 객체를 사용하여 예외메세지를 출력할 수 있다. }}
public class StatementLoginEvt extends WindowAdapter implements ActionListener { private StatementLogin sl; public StatementLoginEvt(StatementLogin sl) { this.sl = sl; } @Override public void actionPerformed(ActionEvent e) { if (e.getSource() == sl.getJtfId() || e.getSource() == sl.getJpfPass() || e.getSource() == sl.getJbtLogin()) { String id = sl.getJtfId().getText().trim(); String pass = new String(sl.getJpfPass().getPassword()).trim(); if (chkNull(id, pass)) { try { String name = login(id, pass); if (name.isEmpty()) { JOptionPane.showMessageDialog(sl, "아이디나 비밀번호를 확인해주세요."); sl.getJtfId().setText(""); sl.getJpfPass().setText(""); sl.getJtfId().requestFocus(); return; } JDialog jd = new JDialog(sl, "로그인 성공", true); jd.add(new JLabel(name+"님 로그인 하셨습니다.")); jd.setBounds(sl.getX()+100,sl.getY()+100, 300, 200); jd.setResizable(false); jd.setVisible(true); jd.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); } catch (SQLException se) { JOptionPane.showMessageDialog(sl, "DB작업 중 문제가 발생했습니다."); se.printStackTrace(); } } } if (e.getSource() == sl.getJbtCancel()) { sl.getJtfId().setText(""); sl.getJpfPass().setText(""); sl.getJtfId().requestFocus(); } } public String login(String id, String pass) throws SQLException { String name = ""; // 1. 드라이버 로딩 try { Class.forName("oracle.jdbc.OracleDriver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } // 2. Connection 얻기 String url = "jdbc:oracle:thin:@localhost:1521:orcl"; String dbo_id = "scott"; String dbo_pass = "tiger"; Connection con = null; Statement stmt = null; ResultSet rs = null; try { con = DriverManager.getConnection(url, dbo_id, dbo_pass); // 3. 쿼리문 생성객체 얻기 stmt = con.createStatement(); StringBuilder selectName = new StringBuilder(); selectName .append("SELECT name ") .append("FROM test_login ") .append("WHERE id='").append(id).append("' AND ") .append("pass='").append(pass).append("'"); // 4. 쿼리문 수행 후 결과 얻기 rs = stmt.executeQuery(selectName.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; } public boolean chkNull(String id, String pass) { // ***** boolean flag = false; try { if (id.equals("")) { JOptionPane.showMessageDialog(sl, "아이디는 필수 입력"); sl.getJtfId().requestFocus(); flag = true; // 예외를 강제로 발생, throw new 예외처리클래스명(); ***** throw new LoginException("아이디 공백"); } if (pass.equals("")) { JOptionPane.showMessageDialog(sl, "비밀번호는 필수 입력"); sl.getJpfPass().requestFocus(); flag = true; throw new LoginException("비밀번호 공백"); } } catch (LoginException le) { le.printStackTrace(); } return !flag; } @Override public void windowClosing(WindowEvent e) { sl.dispose(); }}
String sql = "SELECT name FROM login WHERE id='"+id+"' AND pass='"+pass+"'";
아래값을 입력하면 비밀번호 무시하고 뚫림
쿼리문을 항상 참으로 만드는 WHERE 1=1
-- id는 항상 참으로, pw 주석으로 없애는 SQL Injection-- 변경된 SQL : SELECT name FROM login WHERE id = '' OR 1=1 -- ' or 1=1 --
방어 코드 : 작은 따옴표, 주석, 공백이 못 들어오게 막아야 한다.
// SQL Injection 방어코드// 회원가입 받을 때 부터 공백, 작은 따옴표, 하이픈 못 쓰게 막아야 한다.public String blockSqlInjection(String data) { return data.replaceAll(" ", "").replaceAll("'","").replaceAll("--", "");}
StringBuilder selectName = new StringBuilder();selectName.append("SELECT name ").append("FROM test_login ").append("WHERE id='").append(blockSqlInjection(id)).append("' AND ").append("pass='").append(blockSqlInjection(pass)).append("'");
또 다른 SQL Injection 예시
지번 동을 입력받아 우편번호, 주소를 출력하는 창
String dong = jtf.getText().trim();sql = "SELECT zipcode, sido, gugun, dong, bunji FROM zipcode WHERE dong LIKE '"+dong+"%'";
@SuppressWarnings("serial")public class ZipcodeView extends JFrame { private JTextField jtfDong; private JButton jbtSearch; private DefaultTableModel dtmZipcode; public ZipcodeView() { super("지번주소 검색"); jtfDong = new JTextField(10); jbtSearch = new JButton("검색"); String[] columnNames = { "우편번호", "주소" }; dtmZipcode = new DefaultTableModel(columnNames,0); JTable tabZipcode = new JTable(dtmZipcode); tabZipcode.getColumnModel().getColumn(0).setPreferredWidth(50); tabZipcode.getColumnModel().getColumn(1).setPreferredWidth(400); // 컬럼 이동 막기 tabZipcode.getTableHeader().setReorderingAllowed(false); // 행의 높이 조절 tabZipcode.setRowHeight(24); JScrollPane jsp = new JScrollPane(tabZipcode); JPanel panelNorth = new JPanel(); panelNorth.add(new JLabel("동 이름")); panelNorth.add(jtfDong); panelNorth.add(jbtSearch); add(BorderLayout.NORTH, panelNorth); add(BorderLayout.CENTER, jsp); ZipcodeViewEvt zve = new ZipcodeViewEvt(this); jtfDong.addActionListener(zve); jbtSearch.addActionListener(zve); addWindowListener(zve); setBounds(400, 300, 600, 500); setResizable(false); setVisible(true); } public static void main(String[] args) { new ZipcodeView(); } // getters}
public class ZipcodeViewEvt extends WindowAdapter implements ActionListener { private ZipcodeView zv; public ZipcodeViewEvt(ZipcodeView zv) { this.zv = zv; } public List<ZipcodeVO> selectZipcode(String dong) throws SQLException { List<ZipcodeVO> list = new ArrayList<ZipcodeVO>(); // 1. 드라이버 로딩 try { Class.forName("oracle.jdbc.OracleDriver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } Connection con = null; Statement stmt = null; ResultSet rs = null; // 2. Connection 얻기 try { String url = "jdbc:oracle:thin:@localhost:1521:orcl"; String id = "scott"; String pass = "tiger"; con = DriverManager.getConnection(url, id, pass); // 3. 쿼리문 생성객체 얻기 stmt = con.createStatement(); StringBuilder selectZipcode = new StringBuilder(); selectZipcode .append(" SELECT zipcode, sido, gugun, dong, NVL(bunji,' ') bunji ") .append(" FROM zipcode ") .append(" WHERE dong LIKE '").append(dong).append("%'"); // 4. 쿼리 수행 후 결과 얻기 rs = stmt.executeQuery(selectZipcode.toString()); ZipcodeVO zv = null; while(rs.next()) { // 조회된 레코드가 존재한다면 // VO에 값을 할당하고 zv = new ZipcodeVO(rs.getString("zipcode"), rs.getString("sido") , rs.getString("gugun"), rs.getString("dong"), rs.getString("bunji")); // 같은 이름의 객체를 여러개 관리하기 위해 List에 추가 list.add(zv); } } finally { // 5. 연결끊기 if (rs != null) { rs.close(); } if (stmt != null) { stmt.close(); } if (con != null) { con.close(); } } return list; } public void searchZipcode(String dong) { try { // DB에서 조회한 결과를 받아서 List<ZipcodeVO> listZip = selectZipcode(dong); // DefaultTableModel에 추가 => JTable에 반영된다. DefaultTableModel dtm = zv.getDtmZipcode(); // D.T.M 초기화 dtm.setRowCount(0); String[] rowData = null; // D.T.M에 행(Row : 우편번호, 주소 ) 추가 // 조회된 결과를 가지고 for(ZipcodeVO zv : listZip) { // 배열에 넣고 rowData = new String[2]; rowData[0] = zv.getZipcode(); rowData[1] = zv.getSido()+" "+zv.getGugun() +" "+zv.getDong()+" "+zv.getBunji(); dtm.addRow(rowData); } } catch (SQLException se) { JOptionPane.showMessageDialog(zv, "DB에서 문제 발생"); se.printStackTrace(); } } @Override public void actionPerformed(ActionEvent e) { String dong = zv.getJtfDong().getText().trim(); if (!dong.equals("")) { searchZipcode(dong); } } @Override public void windowClosing(WindowEvent e) { zv.dispose(); }}
발생 가능한 SQL Injection
UNION을 사용하여 공격을 수행.
*** UNION은 컬럼의 수와 데이터형이 맞지않으면 에러남**
컬럼수를 계속 늘려가면서 시도한다.
테이블 이름을 얻는 SQL Injection
' UNION SELECT tname, '0', '0', '0', '0' FROM tab --
이렇게 뚫리면 오라클 DB란 의미
여러 dictionary들을 알 수 있게됨
테이블의 이름을 알 수 있는 쿼리
select column_name from user_tab_cols where table_name='TEST_LOGIN';
특정 테이블의 컬럼명 얻는 SQL Injection
' UNION SELECT column_name,data_type,'0','0','0' FROM user_tab_cols WHERE table_name='TEST_LOGIN' ---
레코드 검색 SQL Injection
' UNION SELECT id,name,pass,'0','0' FROM test_login --
방어코드
로그인 때와 동일하게 작은 따옴표, 주석코드를 막음
추가로 필요하다면 SQL관련된 명령을 막는다(SELECT, UNION 등)
public String blockSqlInjection(String sql) { return sql.replaceAll("'", "").replaceAll("--", "");}
selectZipcode.append(" SELECT zipcode, sido, gugun, dong, NVL(bunji,' ') bunji ").append(" FROM zipcode ").append(" WHERE dong LIKE '").append(blockSqlInjection(dong)).append("%'");
단위테스트 하기
메소드 동작을 확인하는 단위테스트
메인메소드 생성하여 작동여부 확인하는 작업
**QA(Quality Assurance)**가 하는 업무
public void insertCpEmp2(CpEmp2VO cevo) throws SQLException { Connection con = null; PreparedStatement pstmt = null; try { // 1. 드라이버 로딩 // 2. Connection 얻기 con = getCon(); // 3. 쿼리문 생성객체 얻기 : PreparedStatement객체는 실행되는 쿼리문을 알고 있다. String insertCpEmp2 = "INSERT INTO cp_emp2(empno,ename,hiredate,sal) VALUES(?,?,SYSDATE,?)"; pstmt = con.prepareStatement(insertCpEmp2); // 4. 바인드 변수에 값 설정 pstmt.setInt(1, cevo.getEmpno()); pstmt.setString(2, cevo.getEname()); pstmt.setInt(3, cevo.getSal()); // 5. 쿼리 수행 후 결과 얻기 pstmt.executeUpdate(); } finally { // 6. 연결끊기 if (pstmt != null) { pstmt.close(); } if (con != null) { con.close(); } }}public static void main(String[] args) { UsePreparedStatementDAO u = new UsePreparedStatementDAO(); CpEmp2VO c = new CpEmp2VO(3,5000,"범이"); try { u.insertCpEmp2(c); } catch (SQLException e) { e.printStackTrace(); }}
화이트박스 테스트, 블랙박스 테스트
화이트박스 테스트
직접 코드를 보며 분석하는 테스트
블랙박스 테스트
제시된 체크리스트 보며 작동여부를 확인하는 테스트
PreparedStatement
쿼리문을 객체가 알고있다.
쿼리문이 만들어진 상태에서 실행
반복 수행 시 Statement보다 속도가 빠르다.
SQL Injection 공격을 할 수 없다.
bind변수를 사용할 때 '를 넣을 수 없다.
bind변수란 SQL문 내에 값을 대체하여 들어가는 변수
쿼리문의 작성이 단순해진다.
// ? - bind 변수String sql1 = "INSERT INTO 테이블명 VALUES(?, ?, ?)";String sql2 = "SELECT name FROM 테이블명 WHERE 컬럼명=?";
컬럼명, 테이블명에는 bind변수를 사용할 수 없다.
바인드변수의 인덱스는 1번부터 시작한다.
사용법
PreparedStatement 얻기
// 3. 쿼리문 생성 객체 얻기PreparedStatement pstmt = con.prepareStatement(sql);
// 4. bind 변수에 값 넣기pstmt.setInt(바인드변수인덱스, 정수값); // 정수인 경우pstmt.setString(인덱스, 문자열); // 문자열인 경우// 5. 쿼리문을 실행하여 결과 얻기// PreparedStatement는 Statement와 다르게 // 쿼리를 실행할 때 쿼리를 파라미터로 넣지 않는다.boolean flag = pstmt.execute();ResultSet rs = pstmt.executeQuery();int cnt = pstmt.executeUpdate();
오랜만에 숙제
접속한 계정의 모든 Table명을 JComboBox에 출력
public class RunSelectTable { public JComboBox<String> setTab() throws SQLException { JComboBox<String> jcbTab = new JComboBox<String>(); jcbTab.addItem("테이블명"); try { Class.forName("oracle.jdbc.OracleDriver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } Connection con = null; Statement stmt = null; ResultSet rs = null; try { String url = "jdbc:oracle:thin:@localhost:1521:orcl"; String id = "scott"; String pwd = "tiger"; con = DriverManager.getConnection(url, id, pwd); stmt = con.createStatement(); String sql = "SELECT tname FROM tab"; rs = stmt.executeQuery(sql); while (rs.next()) { jcbTab.addItem(rs.getString("tname")); } } finally { if (rs != null) { rs.close(); } if (stmt != null) { stmt.close(); } if (con != null) { con.close(); } } return jcbTab; } public static void main(String[] args) { RunSelectTable rst = new RunSelectTable(); try { new SelectTableView(rst.setTab()); } catch (SQLException e) { System.out.println("DB 접속 실패"); e.printStackTrace(); } }}
@SuppressWarnings("serial")public class SelectTableView extends JFrame { private JButton jbSelect; private JComboBox<String> jcbTab; public SelectTableView(JComboBox<String> jcbTab) { super("테이블 선택"); this.jcbTab = jcbTab; JLabel jl = new JLabel("테이블 선택"); jbSelect = new JButton("선택"); JPanel panel = new JPanel(); panel.add(jl); panel.add(jcbTab); panel.add(jbSelect); add(BorderLayout.CENTER, panel); SelectTableViewEvt stve = new SelectTableViewEvt(this); jbSelect.addActionListener(stve); addWindowListener(stve); setBounds(400, 300, 350, 100); setResizable(false); setVisible(true); }}
public class SelectTableViewEvt extends WindowAdapter implements ActionListener { private SelectTableView stv; public SelectTableViewEvt(SelectTableView stv) { this.stv = stv; } @Override public void actionPerformed(ActionEvent e) { // 선택했을 때 이벤트 처리 } @Override public void windowClosing(WindowEvent e) { stv.dispose(); }}