유형 | 입력데이터 검증 및 표현 |
보안약점 | SQL 삽입 |
개요 | 데이터베이스(DB)와 연동된 웹 응용프로그램에서 입력된 데이터에 대한 유효성 검증을 하지 않을 경우, 공격자가 입력 폼 및 URL 입력란에 SQL 문을 삽입하여 DB로부터 정보를 열람하거나 조작할 수 있는 보안약점을 말한다. 취약한 웹 응용프로그램에서는 사용자로부터 입력된 값을 필터링 과정 없이 넘겨받아 동적쿼리(Dynamic Query)를 생성하기 때문에 개발자가 의도하지 않은 쿼리가 생성되어 정보유출에 악용될 수 있다. |
보안대책 | PreparedStatement 객체 등을 이용하여 DB에 컴파일된 쿼리문(상수)을 전달하는 방법을 사용한다. PreparedStatement를 사용하는 경우에는 DB 쿼리에 사용되는 외부 입력값에 대하여 특수문자 및 쿼리 예약어를 필터링하고, 스트러츠(Struts), 스프링(Spring) 등과 같은 프레임워크를 사용하는 경우에는 외부 입력값 검증모듈 및 보안모듈을 상황에 맞추어 적절하게 사용한다. |
진단방법 | Statement 객체로 쿼리가 실행되는 부분을 확인하고 Statement 객체가 PreparedStatement 객체인지 확인한다. PreparedStatement 객체를 사용하고 setString 등의 메소드로 외부 입력값을 설정하는 경우에는 기본적으로 안전하다고 판정하지만, 쿼리에 사용되는 변수가 외부 입력값인 경우엔 적절한 필터링 모듈이 존재하는지 추가로 확인한다. 즉, 쿼리 생성과 관련된 외부 입력값에 대한 필터링 모듈이 반드시 존재해야 하며, 그 외에도 관련 프레임워크에서 적절한 조치가 이루어질 경우에 안전하다고 판정한다. |
연관된 설계단계 기준 | DBMS 조회 및 결과 검증 |
코드예제
● 안전하지 않은 코드 예 (Java : JDBC API)
//외부로부터 입력받은 값을 검증 없이 사용할 경우 안전하지 않다.
String gubun = request.getParameter("gubun");
String sql = "SELECT * FROM board WHERE b_gubun = '" + gubun + "'";
Connection con = db.getConnection();
Statement stmt = con.createStatement();
//외부로부터 입력받은 값이 검증 또는 처리 없이 쿼리로 수행되어 안전하지 않다.
ResultSet rs = stmt.executeQuery(sql);
gubun 값으로 a' or 'a' = 'a 를 입력하면 조건절이 b_gubun = 'a' or 'a' = 'a'로 변경되어 board 테이블의 모든 내용이 조회된다.
● 안전한 코드 예 (Java : JDBC API)
String gubun = request.getParameter("gubun");
// 1. 사용자에 의해 외부로부터 입력받은 값은 안전하지 않을 수 있으므로,
// PreparedStatement 사용을 위해 ?문자로 바인딩 변수를 사용한다.
String sql = "SELECT * FROM board WHERE b_gubun = ?";
Connection con = db.getConnection();
// 2. PreparedStatement 사용한다.
PreparedStatement pstmt = con.prepareStatement(sql);
// 3.PreparedStatement 객체를 상수 스트링으로 생성하고,
// 파라미터 부분을 setString등의 메소드로 설정하여 안전하다.
pstmt.setString(1, gubun);
ResultSet rs = pstmt.executeQuery();
PreparedStatement 객체를 상수 스트링으로 생성하고, 파라미터 부분을 setString, setParameter 등의 메소드로 설정한다.
● 안전하지 않은 코드 예 (Java : MyBatis)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<select id="boardSearch" parameterType="map" resultType="BoardDto">
// $기호를 사용하는 경우 외부에서 입력된 keyword값을 문자열에 결합한 형태로
// 쿼리에 반영되므로 안전하지 않다.
select * from tbl_board where title like '%${keyword}%' order by pos asc
</select>
외부에서 입력되는 값이 SQL 질의문에 $ 기호로 표현되어 ' '가 자동으로 붙지 않아 파라미터가 그대로 출력되어 SQL 삽입 취약점이 발생할 수 있다.
● 안전한 코드 예 (Java : MyBatis)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<select id="boardSearch" parameterType="map" resultType="BoardDto">
// $ 대신 #기호를 사용하여 변수가 쿼리맵에 바인딩 될 수 있도록 수정하는 것이 안전하다.
select * from tbl_board where title like '%'||#{keyword}||'%' order by pos asc
</select>
MyBatis 쿼리맵에 바인딩할 경우 $ 기호가 아닌 # 기호를 사용해야 한다.
● 안전하지 않은 코드 예 (Java : Hibernate)
import org.hibernate.Query
import org.hibernate.Session
// 외부로부터 입력받은 값을 검증 없이 사용할 경우 안전하지 않다.
String name = request.getParameter("name");
// Hiberate는 기본으로 PreparedStatement를 사용하지만, 파라미터 바인딩 없이 사용 할 경우 안전하지 않다.
Query query = session.createQuery("from Student where studentName = '" + name + "' ");
Hibernate는 기본으로 PreparedStatement를 사용하지만, 파라미터 바인딩 없이 사용할 경우 외부 입력값에 의해 쿼리 구조가 변경될 수 있다.
● 안전한 코드 예 (Java : Hibernate)
import org.hibernate.Query
import org.hibernate.Session
......
String name = request.getParameter("name");
// 1. 파라미터 바인딩을 위해 ?를 사용한다.
Query query = session.createQuery("from Student where studentName = ? ");
// 2. 파라미터 바인딩을 사용하여 외부 입력값에 의해 쿼리 구조 변경을 못 하게 사용하였다.
query.setString(0, name);
import org.hibernate.Query
import org.hibernate.Session
String name = request.getParameter("name");
// 1. 파라미터 바인딩을 위해 명명된 파라미터 변수를 사용한다.
Query query = session.createQuery("from Student where studentName = :name ");
// 2. 파라미터 바인딩을 사용하여 외부 입력값에 의해 쿼리 구조 변경을 못 하게 사용하였다.
query.setParameter("name", name);
외부 입력값이 위치하는 부분을 ? 또는 : 명명된 파라미터 변수로 설정한다.
● 안전하지 않은 코드 예 (C#)
public void ButtonClickBad(object sender, EventArgs e)
{
string connect = "MyConnString";
string usrinput = Request["ID"];
// 외부로부터 입력받은 값을 SQL 쿼리에 직접 사용하는 것은 안전하지 않다.
string query = "Select * From Products Where ProductID = " + usrinput;
using (var conn = new SqlConnection(connect))
{
using (var cmd = new SqlCommand(query, conn))
{
conn.Open();
cmd.ExecuteReader(); /* BUG */
}
}
}
외부 입력값 usrinput을 SQL 쿼리에 직접 사용하고 있다.
● 안전한 코드 예 (C#)
void ButtonClickGood(object sender, EventArgs e)
{
string connect = "MyConnString";
string usrinput = Request["ID"];
// 파라미터 바인딩을 위해 @을 사용합니다. 외부입력 값에 의해 쿼리 구조 변경을 할 수 없습니다.
string query = "Select * From Products Where ProductID = @ProductID";
using (var conn = new SqlConnection(connect))
{
using (var cmd = new SqlCommand(query, conn))
{
cmd.Parameters.AddWithValue("@ProductID", Convert.ToInt32(Request["ProductID"]);
conn.Open();
cmd.ExecuteReader();
}
}
}
ProductID를 파라미터 바인딩으로 처리하고 있다.
진단방법
Statement 객체로 쿼리가 실행되는 부분을 확인하고(①) Statement 객체가 Pre-paredStatement 객체인지 확인한다.(②)
PreparedStatement 객체를 사용하고 setString 등의 메소드로 외부 입력값을 설정하는 경우에는 기본적으로 안전하다고 판정하지만, 쿼리에 사용되는 변수가 외부 입력값인 경우엔 적절한 필터링 모듈이 존재하는지 추가로 확인한다.(③)
즉, 쿼리 생성과 관련된 외부 입력값에 대한 필터링 모듈이 반드시 존재해야 하며, 그 외에도 관련 프레임워크에서 적절한 조치가 이루어질 경우에 안전하다고 판정한다.
● 일반적인 진단의 예
class Login {
public Connection getConnection() throws SQLException {
DriverManager.registerDriver(new com.microsoft.sqlserver.jdbc.SQLServerDriver());
String dbConnection = PropertyManager.getProperty(“db.connection”);
return DriverManager.getConnection(dbConnection);
}
// 외부입력 값(username, password)을 제공받음
public void doPrivilegedAction(String username, char[] password) throws SQLException {
Connection connection = getConnection();
if (connection == null) {
// handle error
}
try {
String pwd = HashUtil.hashPassword(password);
String sqlString = “SELECT * FROM db_user WHERE username = ‘”+ username
"' AND password = '" + pwd + "'"; ················································③
Statement stmt = connection.createStatement(); ···································②
ResultSet rs = stmt.executeQuery(sqlString); ·····································①
if (!rs.next()) {
throw new SecurityException(“User name or password incorrect”);
}
// Authenticated; proceed
} finally {
// …
}
}
}
● 정탐코드
…
String tableName = props.getProperty("jdbc.tableName");
String name = props.getProperty("jdbc.name");
String query = "SELECT * FROM " + tableName + " WHERE Name ='" + name + "'";
stmt = con.prepareStatement(query);
rs = stmt.executeQuery();
…
PreparedStatement 객체를 사용하고 있으나 객체를 상수스트링으로 생성하여 파라미터 부분을 setString 메소드로 설정하지 않아취약하다고 판정한다.
● 정탐코드
// <projectDetail.jsp>
<%
String pid = request.getParameter("projectid") == null ? "" : request.getParameter("projectid");
MashupProjectDao mashupDao = (MashupProjectDao) DAOHelper.getMashupProjectDao(application);
ProjectCommentDao commentDao = (ProjectCommentDao) DAOHelper.getProjectCommentDao(application);
MashupProjectTbl prjTbl = mashupDao.getMashupProjectTbl(pid);
ArrayList commentList = commentDao.getProjectCommentTblByWhere("where projectid=" + pid + " order by writedate desc");
String loginID = session.getAttribute("userid") == null ? "" : (String) session.getAttribute("userid");
int category = prjTbl.getCategory();
String categoryStr = "";
// <ProjectCommentDAO.java>
public ArrayList getProjectCommentTblByWhere(String where) {
String sql_ = this.getMessageSourceAccessor().getMessage("ProjectCommentDao.getProjectCommentTbl");
ArrayList ret = new ArrayList();
sql_ += " " + where;
try {
JdbcTemplate template_ = this.getJdbcTemplate();
List tmp = template_.query(sql_, new ProjectCommentRowMapper());
ret = Util.trimToResize(tmp);
} catch (DataAccessException e) {
projectDetail.jsp 파일에서 사용자의 입력값이 변수 pid에 저장되고 commentDao.getProjectCommentTblByWhere 함수의 파라미터로 사용된다. ProjectCommentDAO.java 파일에서 해당 문자열을 이용해서 쿼리를 실행하므로 취약하다고 판정한다.
[ SQL 삽입 : JDO ]
외부의 입력값에 대한 적절한 검사 과정을 거치지 않고, JDO(Java Data Objects) API의 SQL 또는 JDOQL 쿼리 생성을 위한 문자열로 사용하면, 공격자가 프로그래머가 의도하지 않았던 문자열을 전달함으로써 쿼리의 의미를 왜곡시키거나 그 구조를 변경하여 임의의 쿼리 명령어를 수행할 수 있다.
● 정탐코드
…
public class U9102 implements ContactDAO {
public List<Contact> listContacts() {
PersistenceManager pm = getPersistenceManagerFactory().getPersistenceMan ager();
String query = “select from “ + Contact.class.getName();
try {
Properties props = new Properties();
String fileName = “contacts.txt”;
FileInputStream in = new FileInputStream(fileName);
if (in != null) {
props.load(in);
in.close();
}
// 외부로부터 입력을 받는다
String name = props.getProperty(“name”);
if (name != null) {
query += “ where name = ‘” + name + “’”;
}
} catch (IOException e) {
…
}
// 외부입력 값이 JDO 객체의 인자로 사용된다.
return (List<Contact>) pm.newQuery(query).execute();
}
}
…
공격자가 외부 입력(id)의 값으로 foo'; DELETE FROM MYTABLE; --을 주게 되면, 다음과 같은 쿼리가 실행되어 테이블이 삭제될 수 있으므로 취약하다고 판정한다.
SELECT col1 FROM MYTABLE WHERE name = 'foo' ; DELETE FROM MYTABLE; --'
[ SQL 삽입 : Persistence ]
J2EE Persistence API를 사용하는 응용프로그램에서 외부의 입력값을 검증 없이 쿼리에 그대로 사용하면, 쿼리의 의미를 왜곡시키거나 그 구조를 변경함으로써 임의의 쿼리가 실행될 수 있다.
● 정탐코드
public class U9103 implements ServletContextListener {
public List<?> getAllItemsInWildcardCollection() {
EntityManager em = getEntityManager();
List<U9103> r_type = null;
try {
Properties props = new Properties();
String fileName = "conditions.txt";
FileInputStream in = new FileInputStream(fileName);
props.load(in);
// 외부로부터 입력을 받는다.
String id = props.getProperty("id");
// 외부 입력 값이 query의 인자로 사용이 된다.
Query query = em.createNativeQuery("SELECT OBJECT(i) FROM Item i WHERE i.itemID > " + id);
List<U9103> items = query.getResultList();
…
return r_type;
}
…
공격자가 외부 입력(id)의 값으로 foo'; DELETE FROM MYTABLE; --을 주게 되면, 다음과 같은 쿼리가 실행되어 테이블이 삭제될 수 있으므로 취약하다고 판정한다.
SELECT col1 FROM MYTABLE WHERE name = 'foo' ; DELETE FROM MYTABLE; --'
[ SQL 삽입 : mybatis Data Map ]
외부에서 입력된 값이 쿼리의 인자값으로만 사용되지 않고, 쿼리 명령어에 연결되는 문자열 로 사용되면, 공격자가 의도하지 않았던 문자열을 전달함으로써 쿼리의 의미를 왜곡시키거나, 그 구조를 변경하여 임의의 DB 명령어를 수행할 수 있다.
● 정탐코드
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sqlMap PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN" "http://www.ibatis.com/dtd/sql-map-2.dtd">
<sqlMap namespace="Student">
<resultMap id="StudentResult" class="Student">
<result column="ID" property="id" />
<result column="NAME" property="name" />
</resultMap>
<select id="listStudents" resultMap="StudentResult">
SELECT NUM, NAME
FROM STUDENTS
ORDER BY NUM
</select>
<select id="nameStudent" parameterClass="Integer" resultClass="Student">
SELECT NUM, NAME
FROM STUDENTS
WHERE NUM = #num#
</select>
<!-- dynamic SQL 사용 -->
<delete id="delStudent" parameterClass="Student">
DELETE STUDENTS
WHERE NUM = #num# AND NAME = '$name$'
</delete>
</sqlMap>
위 예제는 mybatis Data Map에서 사용하는 쿼리 설정파일(XML)이다. 정의된 쿼리 중 delStudent 명령어 선언에서 쿼리에 삽입되는 인자들 중 $name$으로 전달되는 문자열 값은 그대로 연결되어 쿼리가 만들어진다. 따라서 만약 NAME의 값으로 ' OR 'x'='x 을 전달하면 다음과 같은 쿼리가 실행되어 테이블의 모든 원소를 삭제할 수 있으므로 취약하다고 판정한다.
DELETE STUDENTS WHERE NUM = #num# and NAME = '' OR 'x'='x'
[ SQL 삽입 : Hibernate ]
외부의 신뢰할 수 없는 입력을 적절한 검사 과정을 거치지 않고 Hibernate API의 SQL 쿼리 생성을 위한 문자열로 사용하면, 공격자가 프로그래머가 의도하지 않았던 문자열을 전달함으로써 쿼리의 의미를 왜곡시키거나 그 구조를 변경하여 임의의 DB 명령어가 수행되도록 할 수 있다.
● 정탐코드
import org.hibernate.Query
import org.hibernate.Session
......
String name = request.getParameter("name");
Query query = session.createQuery("from Student where studentName = '" + name + "' ");
위 예제는 외부의 입력(name)을 아무 검증과정 없이 쿼리에 그대로 사용하고 있다. 만일, 외부의 입력으로 "n' or '1'='1"과 같은 문자열이 입력되면, 다음과 같은 쿼리가 실행되어 테이블 내의 모든 레코드가 반환될 수 있으므로 취약하다고 판정한다.
"from Student where studentName='n' or '1'='1'"
● 오탐코드
import org.hibernate.Query
import org.hibernate.Session
......
String name = request.getParameter("name");
Query query = session.createQuery("from Student where studentName = ? ");
query.setString(0, name);
외부 입력값이 위치하는 부분을 ?로 설정하고, 실행 시에 해당 파라미터가 전달되도록 수정함으로써 외부의 입력(name)이 쿼리의 구조를 변경시키는 것을 방지하고 있어 취약하지 않다.
● 오탐코드
import org.hibernate.Query
import org.hibernate.Session
......
String name = request.getParameter("name");
Query query = session.createQuery("from Student where studentName = :name ");
query.setParameter("name", name);
외부 입력값이 위치하는 부분을 : 명명된 파라미터 변수로 설정하고, 실행 시에 해당 파라미터가 전달되도록 수정함으로써 외부의 입력(name)이 쿼리의 구조를 변경시키는 것을 방지하고 있어 취약하지 않다.
'소프트웨어(SW) 보안약점 진단원 > 구현단계 보안약점 제거 기준' 카테고리의 다른 글
구현단계 보안약점 기준 - 위험한 형식 파일 업로드 (0) | 2025.05.26 |
---|---|
구현단계 보안약점 기준 - 운영체제 명령어 삽입 (2) | 2025.05.21 |
구현단계 보안약점 기준 - 크로스사이트 스크립트 (1) | 2025.05.15 |
구현단계 보안약점 기준 - 경로 조작 및 자원 삽입 (0) | 2024.09.12 |
구현단계 보안약점 기준 - 코드 삽입 (0) | 2024.09.09 |