자격/SW보안약점진단원

구현단계 보안약점 제거 기준-SQL 삽입

Ignatius Heo 2023. 7. 3. 02:53

작성일: 230703

 

※ 본 게시글은 학습 목적으로 행정안전부·KISA의 소프트웨어 보안약점 진단 가이드, 소프트웨어 개발보안 가이드를 참고하여 작성하였습니다.

 

정리 내용: 소프트웨어 보안약점 진단 가이드(180~193p)

 

구분 - 입력데이터 검증 및 표현
설계단계 - DBMS 조회 및 결과 검증
https://cryptocurrencyclub.tistory.com/89
개요
DB와 연동된 웹 응용프로그램에서 입력된 데이터에 대한 유효성 검증을 하지 않을 경우, 공격자가 입력 폼 및 URL 입력란에 SQL 문을 삽입하여 DB로부터 정보를 열람하거나 조작할 수 있는 보안약점을 말한다.

취약한 웹 응용프로그램에서는 사용자로부터 입력된 값을 필터링 과정 없이 넘겨받아 동적쿼리를 생성하기  때문에 개발자가 의도하지 않은 쿼리가 생성되어 정보유출에 악용될 수 있다.

진단 세부사항
(설계단계)

 ① 애플리케이션에서 DB연결을 수행할 때 최소권한의 계정을 사용해야 한다.
  ㅇ  애플리케이션 별 DB연결 계정 할당 확인 후 해당 계정의 최소 권한 할당 여부 확인 및 점검 테스트


 ② 외부입력값이 삽입되는 SQL질의문을 동적으로 생성해서 실행하지 않도록 해야 한다.
  ㅇ  안전한 쿼리 실행환경을 제공할 수 있는 프레임워크 사용 여부 확인, 정적 쿼리 사용 여부 확인
    → #{variable}로 변수 바인딩하거나 PreparedStatement로 정적쿼리 사용하는지 확인


  ㅇ  쿼리 구조를 변경할 수 있는 입력값의 쿼리 구조 변경 가능성 테스트
    → 특수문자: ' " = & | ! ( ) { } $ % @      (# ^ * _ 빼고 키패드에 있는거 다)
    → 예약어: UNION, SELECT, THEN, IF, INSTANCE, END, COLUMN
    → 함수: DATABASE(), CONCAT(), COUNT(), LOWER()


 ③ 외부 입력값을 이용해 동적으로 SQL질의문을 생성해야 하는 경우, 입력값에 대한 검증을 수행한 뒤 사용해야 한다.
  ㅇ  SQL 필터링 기능의 설계, 외부 라이브러리 사용 여부 확인
  ㅇ  SQL 필터링 적용 방법이 공통인 경우 DB접근을 수행하는 모든 기능에 적용되어 있는지 확인
  ㅇ  쿼리 구조를 변경할 수 있는 입력값의 쿼리 구조 변경 가능성 테스트

보안대책
(구현단계)

PreparedStatement 객체 등을 이용하여 DB에 컴파일 된 쿼리문(상수)을 전달하는 방법을 사용한다.

PreparedStatement를 사용하는 경우에는 DB 쿼리에 사용되는 외부입력값에 대하여 특수문자 및 쿼리 예약어를 필터링하고, 스트러츠(Struts), 스프링(Spring) 등과 같은 프레임워크를 사용하는 경우에는 외부입력값 검증모듈 및 보안모듈을 상황에 맞추어 적절하게 사용한다.

진단방법
(구현단계)

① Statement statement 객체로 쿼리가 실행되는 부분을 확인

② Statement 객체가 Pre-paredStatement 객체인지 확인한다.

③ PreparedStatement 객체를 사용하고 setString등의 메소드로 외부 입력값을 설정하는 경우에는 기본적으로 안전하다고 판정하지만, 쿼리에 사용되는 변수가 외부 입력값인 경우엔 적절한 필터링 모듈이 존재하는지 추가로 확인해야 한다.
즉, 쿼리 생성과 관련된 외부 입력값에 대한 필터링 모듈이 반드시 존재해야 하며, 그 외에도 관련 프레임워크에서 적절한 조치가 이루어질 경우에 안전하다고 판정한다.

 

 

다. 코드예제

 


1: 
2: String gubun = request.getParameter("gubun");
3: ......
4: String sql = "SELECT * FROM board WHERE b_gubun = '" + gubun + "'";
5: Connection con = db.getConnection();
6: Statement stmt = con.createStatement();
7: 
8: ResultSet rs = stmt.executeQuery(sql);

ㅇ 분석

1. row2에서 gubun 변수를 받고, row4에 입력값에 대한 별다른 검증없이 변수를 삽입함.

2. row4에서는 검증없이 받은 변수를 이용하여 board 테이블 쿼리를 생성

3. r8에서 그런 쿼리를 검증없이 받아서 생성, 실행함

4. SQL문의 SELECT * FROM (TABLE) WHERE 1 = 1과 같이 always true 조건절을 입력하게 되면 TABLE 내 모든 데이터가 조회 가능

여기서 gubun의 값을   '' or '1' = '1'   로 입력하면 board값이 나옴

 

 

ㅇ 수정한 코드

1: 
2: String gubun = request.getParameter("gubun");
3: ......
4: String sql = "SELECT * FROM board WHERE b_gubun = ?";
5: Connection con = db.getConnection();
6: PreparedStatement pstmt = con.prepareStatement(sql);
7: pstmt.setString(1, gubun);
8: 
9: ResultSet rs = pstmt.executeQuery();

ㅇ 수정

1. r4에서 sql에 플레이스홀더 ? 사용

2. r6에서 preparedStatement 생성, r7에서 setString 사용해서 ? 위치에 gubun 입력값을 설정함

3. r9에서 쿼리 실행, 결과 반환

 

=> 이렇게 하면 사용자 입력값이 직접 쿼리에 포함되지 않음.

 

 


1: <?xml version="1.0" encoding="UTF-8" ?>
2: <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN“
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
3: ......
4: <select id="boardSearch" parameterType="map" resultType="BoardDto">
5: select * from tbl_board where title like '%${keyword}%' order by pos asc
6: </select>1

ㅇ 분석

1. $ 사용했네? → 쿼리 구조를 변경할 수 있는 입력값이 삽입되었음

=> ex. keyword값을 '' OR '1' = '1' 로 사용

2. $를 #로 변경하여 사용

 

 

ㅇ 수정한 코드

1: <?xml version="1.0" encoding="UTF-8" ?>
2: <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN“
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
3: ......
4: <select id="boardSearch" parameterType="map" resultType="BoardDto">
// method1
5: select * from tbl_board where title like '%'||#{keyword}||'%' order by pos asc
// method2
5: select * from tbl_board where title like concat('%', #{keyword}, '%') order by pos asc
6: </select>1

ㅇ 수정

method1. 바인딩해서 씀

method2. concat해서 묶고 키워드값을 #로 바인딩함

 


1: import org.hibernate.Query
2: import org.hibernate.Session
3: ......
4: String name = request.getParameter("name");
5: Query query = session.createQuery("from Student where studentName = '" + name
+ "' ");

ㅇ 분석

1. name값을 필터 또는 검증 없이 + 바인딩 없이 사용

 

 

ㅇ 수정한 코드

1: import org.hibernate.Query
2: import org.hibernate.Session
3: ......
4: String name = request.getParameter("name");
//method1
5: Query query = session.createQuery("from Student where studentName = :name");
6: query.setParameter("name",name);
//method2
5: Query query = session.createQuery("from Student where studentName = ?");
6: query.setString(0, name);

ㅇ 수정

method1. name변수 앞에 :를 붙이고, setParameter를 이용하여 바인딩함

method2. ?로 플레이스홀더 사용하고 setString으로 바인딩함

 


1: public void ButtonClickBad(object sender, EventArgs e)
2: {
3: string connect = "MyConnString";
4: string usrinput = Request["ID"];
5: string query = "Select * From Products Where ProductID = " + usrinput;
6: using (var conn = new SqlConnection(connect))
7: {
8: using (var cmd = new SqlCommand(query, conn))
9: 
10: {
11: conn.Open();
12: cmd.ExecuteReader(); /* BUG */
13: }
14: }
15: }

ㅇ 분석

1. usrinput 입력값에 대한 검증 작업 없이 쿼리에 적용 ("ID")

 

ㅇ 수정

1: public void ButtonClickBad(object sender, EventArgs e)
2: {

3: string connect = "MyConnString";
4: string usrinput = Request["ID"];
5: string query = "Select * From Products Where ProductID = @usrinput";

6: using (var conn = new SqlConnection(connect))
7: {
8: using (var cmd = new SqlCommand(query, conn))
9: 
10: {
cmd.Parameters.AddWithValue("@usrinput", Convert.ToInt32(Request["usrinput"]););
11: conn.Open();
12: cmd.ExecuteReader();
13: }
14: }
15: }

ㅇ 설명

1. 파라미터 바인딩을 사용하기 위해 우선 쿼리문 변경하고 바인딩, 바인딩값(usrinput)에 대입

 

 


 

 

 

 

 

 

 

 

 

끝.