[시큐어코딩 가이드] 위험한 형식 파일 업로드

 

 

 

■ 입력데이터 검증 및 표현

 

프로그램 입력 값에 대한 검증 누락 또는 부적절한 검증, 데이터의 잘못된 형식지정, 일관되지 않은 언어셋 사용 등으로 인해 발생되는 보안약점으로 SQL 삽입, 크로스사이트 스크립트(XSS) 등의 공격을 유발할 수 있다.

 

 

위험한 형식 파일 업로드

 

■ 개요

 

서버 측에서 실행될 수 있는 스크립트 파일(asp, jsp, php, sh 파일 등)이 업로드 가능하고, 이 파일을 공격자가 웹을 통해 직접 실행시킬 수 있는 경우, 시스템 내부명령어를 실행하거나 외부와 연결하여 시스템을 제어할 수 있는 보안약점이다.


공격자가 실행 가능한 파일을 서버에 업로드 하면 파이썬에서 String 형식으로 표현된 표현식을 인수로 받아 반환하는 eval() 함수와 인수로 받은 문자열을 실행하는 exec()를 같이 사용하여 여러 변수들을 동적으로 값을 할당받아 실행될 수 있어 웹쉘(Web Shell) 공격에 취약하다.

 

 

■ 안전한 코딩 기법

 

파일 업로드 공격을 방지하기 위해서 특정 파일 유형만 허용하도록 화이트리스트 방식으로 파일 유형을 제한하여야 한다. 이때 파일의 확장자 및 업로드 된 파일의 Content-Type도 같이 확인한다. 파일 크기 및 파일 개수를 제한하여 시스템 자원 고갈 등으로 서비스 거부 공격이 발생하지 않도록 제한하여야 한다. 

 

업로드 된 파일을 웹 루트 폴더 외부에 저장하여 공격자가 URL을 통해 파일을 실행할 수 없도록 해야 하고 가능하면 업로드 되는 파일의 이름은 공격자가 추측할 수 없는 무작위한 이름으로 변경하여 저장하는 것이 안전하다. 또한 업로드 된 파일을 저장할 경우에는 최소 권한만 부여하는 것이 안전하고 실행 여부를 확인하여 실행권한을 삭제 한다.

 

 

코드예제

 

업로드 할 파일에 대한 개수, 크기, 확장자 등의 유효성 검사를 하지 않고 파일시스템에 저장 할 경우 공격자에 의해 악성코드, 쉘 코드 등 위험한 형식의 파일을 시스템에 업로드 할 수 있다.

 

안전하지 않은 코드의 예
from django.shortcuts import render
from django.core.files.storage import FileSystemStorage

def file_upload(request):
if request.FILES['upload_file']:
# 사용자로부터 업로드 되는 파일에 대해 검증 없이 저장하고 있어
# 안전하지 않다.
upload_file = request.FILES['upload_file']
fs = FileSystemStorage(location='media/screenshot', base_url='media/screenshot')
# 업로드 하는 파일에 대한 크기, 개수, 확장자 등을 검증 하지 않음.
filename = fs.save(upload_file.name, upload_file)
return render(request, '/success.html', {'filename':filename})
return render(request, '/error.html', {'error':'파일 업로드 실패'})

 

아래 코드는 업로드 하는 파일의 파일 개수, 파일 크기, 파일 확장자 등을 검사하여 업로드를 제한하고 있다. 21라인의 파일 타입 확인은 MIME 타입을 확인하는 과정으로 파일이름에서 확장자만 검사할 경우 변조된 확장자를 통해 업로드 제한을 회피할 수 있어 파일자체의 시그니처를 확인하는 과정이다.

 

안전한 코드의 예
import os
from django.shortcuts import render
from django.core.files.storage import FileSystemStorage

# 업로드 하는 파일에 대한 개수, 크기, 확장자 제한
FILE_COUNT_LIMIT = 5
# 업로드 하는 파일의 최대 사이즈 제한 예 ) 5MB - 5*1024*1024
FILE_SIZE_LIMIT = 5242880
# 허용하는 확장자는 화이트리스트로 관리한다.
WHITE_LIST_EXT = [
'.jpg',
'.jpeg'
]

def file_upload(request):
# 파일 개수 제한
if len(request.FILES) == 0 or len(request.FILES) > FILE_COUNT_LIMIT:
return render(request, '/error.html', {'error': '파일 개수 초과'})

for filename, upload_file in request.FILES.items():
# 파일 타입 체크
if upload_file.content_type != 'image/jpeg':
return render(request, '/error.html', {'error': '파일 타입 오류'})
# 파일 크기 제한
if upload_file.size > FILE_SIZE_LIMIT:
return render(request, '/error.html', {'error': '파일사이즈 오류'})
# 파일 확장자 검사
file_name, file_ext = os.path.splitext(upload_file.name)
if file_ext.lower() not in WHITE_LIST_EXT:
return render(request, '/error.html', {'error': '파일 타입 오류'})

fs = FileSystemStorage(location='media/screenshot', base_url = 'media/screenshot')
for upload_file in request.FILES.values():
fs.save(upload_file.name, upload_file)

return render(request, '/success.html', {'filename': filename})

 

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})

 

 

댓글

Designed by JB FACTORY