• Home
  • About
    • Young's Github Pages photo

      一日不作一日不食

    • About
    • Github
  • Posts
    • All Posts
    • All Tags
  • Projects

Java 정리 36

07 Jan 2019

Reading time ~9 minutes

Java 정리 36 - JDBC(3), SQL Injection, PreparedStatement


DB 사용한 로그인

01

  • 비대면 인증방식 중 가장 많이 사용하는 기법
    • 인증 : 나임을 증명하는 것
    • 아이디, 비번으로 인증
  • DB Table
    • login 이란 임시 테이블

02

  • DB에서 유저를 조회하는 쿼리
SELECT name
FROM login
WHERE 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

03

// 사용자 정의 예외처리 클래스
// 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();
  }
}

OWASP (Open Web Application Security Project)

  • OWASP에서는 매년 보안위협 TOP 10을 발표함
    • https://www.owasp.org/index.php/Category:OWASP_Top_Ten_Project * Injection 공격은 항상 상위권

로그인 창 SQLInjection 예시

  • 프로그램 외부에서 쿼리문을 입력하여 정보를 탈취하는 공격
    • 입력할 수 있는 공간을 “Attack Point”라 한다.
  • 쿼리가 어떻게 만들어져 있는가를 예측해서 값을 집어넣는 공격
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 --

04

  • 방어 코드 : 작은 따옴표, 주석, 공백이 못 들어오게 막아야 한다.
// 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 예시

05

  • 지번 동을 입력받아 우편번호, 주소를 출력하는 창
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 ZipcodeVO {
  private String zipcode, sido, gugun, dong, bunji;
  public ZipcodeVO() { }
  
  public ZipcodeVO(String zipcode, String sido, 
    String  gugun, String dong, String bunji) {
        this.zipcode = zipcode;
        this.sido = sido;
        this.gugun = gugun;
        this.dong = dong;
        this.bunji = bunji;
  }
  // 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 --

06

  • 이렇게 뚫리면 오라클 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' ---

07

  • 레코드 검색 SQL Injection
' UNION SELECT id,name,pass,'0','0' FROM test_login --

08

09

  • 방어코드
    • 로그인 때와 동일하게 작은 따옴표, 주석코드를 막음
    • 추가로 필요하다면 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);

10

// 4. bind 변수에 값 넣기
pstmt.setInt(바인드변수인덱스, 정수값); // 정수인 경우
pstmt.setString(인덱스, 문자열); // 문자열인 경우

// 5. 쿼리문을 실행하여 결과 얻기
// PreparedStatement는 Statement와 다르게 
// 쿼리를 실행할 때 쿼리를 파라미터로 넣지 않는다.
boolean flag = pstmt.execute();
ResultSet rs = pstmt.executeQuery();
int cnt = pstmt.executeUpdate();

11

12

오랜만에 숙제

13

  • 접속한 계정의 모든 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();
    }
}

14



Java