django-ninja icon indicating copy to clipboard operation
django-ninja copied to clipboard

ImageField with UploadedFile doesn't validate that uploaded file is an image

Open saberworks opened this issue 2 years ago • 5 comments

I created a POST endpoint that allows one to create a new "Project". The endpoint optionally accepts an image along with the normal json payload:

image: Optional[UploadedFile] = NinjaFile(None)

The problem I'm having is that when I choose a non-image, I don't get any errors or exceptions and the non-image file is placed in the database and on the file system despite the fact that it's not an image.

If I do the same operation with the normal django ORM and HTML form, it gives me an error like this:

Upload a valid image. The file you uploaded was either not an image or a corrupted image.

Is there a way to get django ninja to validate that the upload file passes the image constraints?

Schema:

class ProjectIn(ModelSchema):
    games: List[int] = [] # list of game IDs
    tags: List[int] = []  # list of tag IDs
    image: Optional[UploadedFile] = None

    class Config:
        model = Project
        model_fields = ['games', 'tags', 'name', 'description', 'accent_color', 'image']

api route:

@router.post("/projects", response=NewProjectOut, auth=django_auth)
def add_project(request, payload: ProjectIn, image: Optional[UploadedFile] = NinjaFile(None)):
    project = payload.dict()

    ...

    new_project = Project.objects.create(**project)

    ...

    if image is not None:
        new_project.image = image
        new_project.save()

    return { "project": new_project, "messages": messages }

Model portion:

    image = models.ImageField(upload_to=get_image_upload_to, null=True, blank=True)
    thumbnail = ImageSpecField(
        source='image',
        processors=[Thumbnail(400, 300)],
        format='JPEG',
        options={'quality': 85},
    )

After creating new project using the api (and attaching a .txt file), the response body looks like:

{
  "project": {
    "id": 38,
    "user": 1,
    "games": [],
    "tags": [],
    "name": "Test Project",
    "slug": "38-test-project",
    "description": "fake description",
    "accent_color": "F0F0F0",
    "image": "/media/saberworks/project/38-test-project/dh_checklist.txt"
  },
  "messages": []
}

Thank you!

saberworks avatar Feb 21 '22 08:02 saberworks

@saberworks

class ProjectIn(ModelSchema):
    games: List[int] = [] # list of game IDs
    tags: List[int] = []  # list of tag IDs
    image: Optional[UploadedFile] = None # <-------- !!!!!!!!!!!

this is the part that is not correct - you cannot define an image on a JSON body of the payload

HTTP protocol by default allows you to transfer files in multipart form body

keep File param only on view arguments:

def add_project(request, payload: ProjectIn, image: Optional[UploadedFile] = NinjaFile(None)):
                                             ------------------------------------------------

vitalik avatar Feb 21 '22 09:02 vitalik

Thank you,

Your message makes sense. However, even after I deleted that part of the schema, and updated the code to pass in the image from the argument when calling create on the model class, it still does not validate that an image file was uploaded. It accepts any types of files, including plain text.

class ProjectIn(ModelSchema):
    games: List[int] = [] # list of game IDs
    tags: List[int] = []  # list of tag IDs

    class Config:
        model = Project
        model_fields = ['games', 'tags', 'name', 'description', 'accent_color']
@router.post("/projects", response=NewProjectOut, auth=django_auth)
def add_project(request, payload: ProjectIn, image: Optional[UploadedFile] = NinjaFile(None)):
    project = payload.dict()

    games = project.pop('games', None)
    tags = project.pop('tags', None)

    project['user'] = request.user

    new_project = Project.objects.create(**project, image = image)
    ...

saberworks avatar Feb 22 '22 18:02 saberworks

The only way I could get validation on the image field was to create and instantiate a ModelForm:

class ProjectForm(ModelForm):
    class Meta:
        model = Project
        fields = ['games', 'tags', 'name', 'description', 'accent_color', 'image', 'user']

@router.post("/projects", response=NewProjectOut, auth=django_auth)
def add_project(request, payload: ProjectIn, image: Optional[UploadedFile] = NinjaFile(None)):
    project = payload.dict()

    project['user'] = request.user

    form = ProjectForm(project, { "image": image })

    if not form.is_valid():
        messages = []

        for key, error_list in form.errors.items():
            messages.append("{}: {}".format(key, "\n".join(error_list)))

        return { "success": False, "messages": messages }

    new_project = form.save()

    return { "success": True, "project": new_project }

saberworks avatar Feb 22 '22 19:02 saberworks

@saberworks

ah.. well the image saving is a bit different mechanism in django (it's not passed to __init__)

try this:

...
def add_project(request, payload: ProjectIn, image: Optional[UploadedFile] = NinjaFile(None)):
     data = payload.dict()
     obj = Project(**data, image = image)
     obj.image.save("filename.jpg", image) # this will save project obj
``

vitalik avatar Feb 23 '22 06:02 vitalik

Thanks again for your replies. I think we might be having a misunderstanding. I am not having a problem getting the model to save with the attached image. The problem I'm having is that if I upload a plain text file instead of an actual image, the plain text file gets stuffed into the image field. The example you provided above works for "jpg" images, but if I upload a plain text file, it gets saved as "filename.jpg" but in fact only contains plain text. I would expect the model save() method to throw an error if invalid data was provides, but this is not happening. It does throw an error if other types of data is wrong (like foreign key error), but no errors when a non-image is uploaded to an ImageField.

I don't think the django models are doing any validation on the contents put into an ImageField, I guess they expect it to be handled on the form level? Or I've completely missed something and I'm still doing it wrong (very possible).

saberworks avatar Feb 23 '22 07:02 saberworks

i cannot find it named NinjaFile. image

sunboy123 avatar Sep 26 '22 09:09 sunboy123

Hi @saberworks ,

I dont think django-ninja checks the file extension.

You can do this

@app.post("/upload")
def read_file(input_file: UploadFile = File(...)):
    if input_file.content_type not in ["application/pdf", "text/plain", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"]:
        raise HTTPException(400, detail="Invalid document type")
    return {"filename": "input_file.filename"}

Which is based on https://github.com/tiangolo/fastapi/discussions/6680#discussioncomment-5131396

baseplate-admin avatar Apr 02 '23 12:04 baseplate-admin