[시큐어코딩 가이드] SQL 삽입
- 정보보안/시큐어코딩 가이드
- 2022. 8. 8.
■ 입력데이터 검증 및 표현
프로그램 입력 값에 대한 검증 누락 또는 부적절한 검증, 데이터의 잘못된 형식지정, 일관되지 않은 언어셋 사용 등으로 인해 발생되는 보안약점으로 SQL 삽입, 크로스사이트 스크립트(XSS) 등의 공격을 유발할 수 있다.
SQL 삽입
■ 개요
데이터베이스(DB)와 연동된 웹 응용프로그램에서 입력된 데이터에 대한 유효성 검증을 하지 않을 경우, 공격자가 입력 폼 및 URL 입력란에 SQL 문을 삽입하여 DB로부터 정보를 열람하거나 조작할 수 있는 보안취약점을 말한다.
취약한 웹 응용프로그램에서는 사용자로부터 입력된 값을 검증 없이 넘겨받아 동적쿼리(Dynamic Query)를 생성하기 때문에 개발자가 의도하지 않은 쿼리가 실행되어 정보유출에 악용될 수 있다.
Python에서는 데이터베이스에 엑세스에 사용되는 다양한 Python 모듈간의 일관성을 장려하기 위해 DB-API를 정의 하고있고 각 데이터베이스마다 별도의 DB 모듈을 이용해 데이터베이스에 엑세스하게 된다. DB-API 외에도, 파이썬에서는 Django, SQLAlchemy, Storm등의 ORM을 사용하여 데이터베이스에 엑세스할 수 있다.
Python에서 지원하는 다양한 ORM(Object Relational Mapping)을 이용하여 보다 안전하게 DB를 사용할 수 있지만 일부 복잡한 조건의 쿼리문 생성 어려움, 성능저하 등의 이유로 쿼리의 튜닝이 필요한 경우 직접 원시 SQL 실행이 필요한 경우가 있다. ORM 대신 원시 쿼리를 사용하는 경우 검증되지 않은 외부 입력값으로 인해 SQL 삽입 공격이 발생할 수 있다.
■ 안전한 코딩 기법
DB-API를 사용할 때에는 매개변수화된 쿼리(Parameterized query)를 사용하여 외부 입력값을 바인딩해서 사용하면 SQL 삽입 공격으로부터 안전하게 사용할 수 있다.
Python에서 많이 사용되는 ORM프레임워크로는 Django의 querySets, SQLAlchemy, Storm등이 있다. ORM 프레임워크는 내부적으로 사용되는 쿼리 모든 곳에서 매개변수화된 명령문을 사용하므로 SQL 삽입 공격으로부터 보호된다.
ORM 프레임워크의 원시 SQL을 사용할 경우에도 안전하게 사용하려면 외부 입력 값을 매개변수화된 쿼리문의 바인딩 변수로 사용한다.
코드예제
■ DB-API 사용 예제
다음은 MySql, PostgreSQL의 DB-API를 사용하여 입력 값을 받아서 처리 하는 안전하지 않은 코드이다. 이 경우에는 외부 입력값을 8, 9 라인에서 입력 받아 변수 name과 content_id에 할당했다. 12 ~ 15라인에서는 외부에서 입력 받은 name과 content_id값을 검증 없이 쿼리문의 인자 값으로 사용하는데 단순 문자열 결합을 통해 쿼리를 생성하고 있다.
이 경우 content_id 값으로 ‘a’ or ‘a’ = ‘a와 같은 공격 문자열을 입력하면 조건 절 content_id = ‘a’ or ‘a’ = ‘a’로 바뀌고, 그 결과 board 테이블 전체 레코드의 name 컬럼이 공격자로부터 입력 받은 name의 값으로 변경된다.
안전하지 않은 코드의 예 |
from django.shortcuts import render def update_board(request): ...... with dbconn.cursor() as curs: # 외부로부터 입력받은 값을 검증 없이 사용할 경우 안전하지 않다. name = request.POST.get('name', '') content_id = request.POST.get('content_id', '') # 사용자의 검증되지 않은 입력으로 부터 동적으로 쿼리문 생성 sql_query = "update board set name='" + name + "' where content_id='“ + content_id + "'" # 외부 입력값이 검증 없이 쿼리로 수행되어 안전하지 않다. curs.execute(sql_query) curs.commit() return render(request, '/success.html') |
다음은 이를 안전한 코드로 변환한 예제이다. 7, 8라인에서 입력받은 외부 입력값을 그대로 사용하지 않고, 15라인의 execute()메서드의 두 번째 인자 값으로 바인딩 해서 쿼리 문을 실행하였다.
11라인과 같이 매개변수 바인딩을 통해 execute() 함수를 호출하면 공격자가 쿼리를 변조하는 값을 삽입하더라도 해당 값이 바인딩된 매개변수의 값으로만 사용되지 때문에 안전하다.
안전한 코드의 예 |
from django.shortcuts import render def update_board(request): ...... with dbconn.cursor() as curs: name = request.POST.get('name', '') content_id = request.POST.get('content_id', '') # 외부 입력값으로 부터 안전한 매개변수 화된 쿼리를 생성 한다. sql_query = 'update board set name=%s where content_id=%s' # 사용자의 입력 값을 매개변수 화된 쿼리에 바인딩 하여 실행되므로 # 안전하다. curs.execute(sql_query, (name, content_id)) curs.commit() return render(request, '/success.html') |
SQLite DB-API 사용에서도 동일하게 정적인 쿼리문을 사전에 생성하고 사용자 입력을 바인딩 하여 안전하게 사용하여야 한다. SQLite에서 매개변수 화된 쿼리(Parameterized Query)를 만들기 위해 “?”를 Placeholder로 사용하거나 “:name” 처럼 Named Placeholder를 사용하는 방법 2가지가 있다.
■ ORM 사용 예제
Django의 querysets는 쿼리 매개변수화를 사용하여 쿼리를 구성하기 때문에 SQL 삽입 공격으로부터 보호된다. 부득이 하게 원시 SQL, 또는 사용자 정의 SQL을 사용할 경우에도 외부 입력값을 매개변수화된 쿼리의 바인딩 변수로 사용하여야한다.
아래는 Django의 원시 SQL을 사용하는 예제이다. Django의 ORM 프레임워크는 원시 SQL 쿼리를 수행하기 위해 Manager.raw() 기능을 제공한다. 다음은 안전하지 않은 코드로 6라인에서 입력받은 외부 입력값을 10라인의 쿼리문 생성에 문자열 조합으로 사용하여 쿼리문의 구조를 만들고 있다.
안전하지 않은 코드의 예 |
from django.shortcuts import render from app.models import Member def member_search(request): # 외부로부터 입력 값을 가져온다. name = request.POST.get('name', '') # 외부로부터 입력 받은 값을 검증 없이 쿼리문 생성에 사용하여 # 안전하지 않다. query=“select * from member where name=‘” + name + “’” # 외부 입력 값을 검증 없이 사용한 쿼리문을 raw()함수로 실행하면 # 안전하지 않다. data = Member.objects.raw(query) return render(request, '/member_list.html', {'member_list':data}) |
다음은 안전한 코드 예제로 Django에서 원시코드를 실행할 경우에도 매개변수화된 쿼리를 사용하고 params 인수를 사용하여 raw() 함수의 바인딩 변수로 사용하여야 한다.
9라인에서 쿼리문 생성을 매개변수화된 쿼리로 생성하고 6라인에서 입력받은 외부 입력값을 10라인의 raw()메소드에서 두 번째 인자의 바인딩변수로 사용하였다.
안전한 코드의 예 |
from django.shortcuts import render from app.models import Member def member_search(request): # 외부로부터 입력 값을 가져온다. name = request.POST.get('name', '') # 외부 입력 값을 raw()함수 실행 시 바인딩 변수로 사용하여 쿼리 구조가 # 변경되지 않도록 한다.(list 형은 %s, dictionary 형은 %(key)s를 사용) query='select * from member where name=%s' # 인자화된 쿼리문을 사용하여 raw()함수를 사용하여 안전하다. data = Member.objects.raw(query, [name]) return render(request, '/member_list.html', {'member_list':data}) |