📑

DRFを使ったファイルアップロード方法について

2021/10/04に公開

概要

DRF を利用したファイルのアップロード方法は、HTMLフォームコンテンツを利用した場合と、ネイティブクライアントを利用した場合の2通りあります。

基本的にはHTMLフォームコンテンツを利用した実装を覚えておけば問題ないはずですが、ネイティブクライアントを利用する場合についても覚えておくことをおすすめします。

覚えておくことで、誤ったパーサを指定することを避けることができ、意図しない脆弱性を入れてしまうことを回避できます。

今回は両方の実装方法と、 curl を使った実行例を紹介します。

検証環境

  • curl 7.64.1
  • python 3.9.4
  • Django 3.2.5
  • djangorestframework 3.12.4

HTMLフォームコンテンツを利用したアップロードの場合

サーバ側の実装例

class UploadImageAPI(GenericAPIView):
    queryset = ImageModel.objects.all()
    parser_classes = [FormParser, MultiPartParser]

    def post(self, request, *args, **kwargs):
        try:
            file = request.data['file']
            logger.info(file)
        except KeyError:
            raise ParseError('Request has no resource file attached')
        return JsonResponse({}, status=status.HTTP_201_CREATED)

このとき curl を使ってファイルをアップロードする場合は以下のコマンドを実行します。

curl -XPOST \
    -H "csrfmiddlewaretoken: {csrf_token}" \
    -H "sessionid: {session_id}" \
    -F file=@/path/to/sample.png  \
    http://localhost:8000/api/images

parser_classes にparser_classes = [FormParser, MultiPartParser]を指定している場合、HTMLフォームコンテンツを解析します。

ブラウザ等で実装したファイルアップロードに対応する場合に利用するパーサーです。

フロント側の実装例

<form id="uploadForm">
    <label for="uploadFile">Choose a profile picture:</label>
    <input type="file" id="uploadFile" name="uploadFile" 
        accept="image/png, image/jpeg" multiple>
    <input type="submit" value="upload file">
</form>
const imageUploadForm = () => {
    const form = $('#uploadForm')
    form.on('submit', function(e) {
        e.preventDefault()
        console.log(this, e);
        const uploadFiles = $('#uploadFile').prop('files')
        console.log(uploadFiles)
        const url = '/api/images'
        const data = new FormData();
        data.append('file', uploadFiles[0])
        const headers = { 'content-type': 'multipart/form-data' };
        axios.post(url, data, {headers}).then((r) => {
            console.log(r)
        }).catch((e) => {
            console.log(e)
        })
    })
}

ネイティブクライアントを利用したアップロードの場合

実装例

class UploadImageAPI(GenericAPIView):
    queryset = ImageModel.objects.all()
    parser_classes = [FileUploadParser]

    def post(self, request, *args, **kwargs):
        try:
            file = request.data['file']
            logger.info(file)
        except KeyError:
            raise ParseError('Request has no resource file attached')
        return JsonResponse({}, status=status.HTTP_201_CREATED)

このときに curl を使ってアップロードする場合は以下のコマンドを実行します。

curl -XPOST \
    -H "csrfmiddlewaretoken: {csrf_token}" \
    -H "sessionid: {session_id}" \
    -F file=@/path/to/sample.png \
    -H "Content-Disposition: attachment; filename=upload.jpg"  \
    http://localhost:8000/api/images

parser_classes にparser_classes = [FileUploadParser]を指定している場合、生ファイルのアップロードが可能です。

このとき重要なのが、このパーサーはネイティブクライアントで利用するためのものということです。

ヘッダーに"Content-Disposition: attachment; filename=upload.jpg"を指定しない場合は URL キーワード引数でfilenmaeを指定する必要があります。

URL 引数に filename を指定する場合は以下のように実装します。

urls.py

urlpatterns += [
    path('api/images/<str:filename>/upload', UploadImageAPI.as_view()),
]

views.py

class UploadImageAPI(GenericAPIView):
    lookup_url_kwarg = 'filename'
    queryset = ImageModel.objects.all()
    parser_classes = [FileUploadParser]

    def post(self, request, *args, **kwargs):
        try:
            file = request.data['file']
            logger.info(file)
        except KeyError:
            raise ParseError('Request has no resource file attached')
        return JsonResponse({}, status=status.HTTP_201_CREATED)

ヘッダーにファイル名を含める場合、URLに含める場合のどちらの場合でも脆弱性を入れないように注意する必要があります。

例えば、XSSを入れてしまうことが考えられます。

XSSの場合は指定されたファイル名にスクリプトタグを埋め込んでおき、画面上に表示する際に不十分なパースを行っていた場合に起こりえます。

フロント側の実装については、HTMLフォームコンテンツを利用した場合の例と curl に指定していたヘッダーを入れることで実装できます。

Discussion