django-ninja
django-ninja copied to clipboard
ImageField with UploadedFile doesn't validate that uploaded file is an image
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
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)):
------------------------------------------------
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)
...
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
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
``
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).
i cannot find it named NinjaFile.

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