Added image content type white list with python-magic

This commit is contained in:
Mikko Ohtamaa 2013-07-19 01:34:57 +03:00
parent b8844d8cd9
commit 217baaa317
5 changed files with 72 additions and 34 deletions

View file

@ -8,46 +8,70 @@ from django.template.defaultfilters import filesizeformat
from avatar.models import Avatar
from avatar.settings import (AVATAR_MAX_AVATARS_PER_USER, AVATAR_MAX_SIZE,
AVATAR_ALLOWED_FILE_EXTS, AVATAR_DEFAULT_SIZE)
AVATAR_ALLOWED_FILE_EXTS, AVATAR_DEFAULT_SIZE,
AVATAR_ALLOWED_MIMETYPES)
def avatar_img(avatar, size):
if not avatar.thumbnail_exists(size):
avatar.create_thumbnail(size)
return mark_safe("""<img src="%s" alt="%s" width="%s" height="%s" />""" %
return mark_safe("""<img src="%s" alt="%s" width="%s" height="%s" />""" %
(avatar.avatar_url(size), unicode(avatar), size, size))
class UploadAvatarForm(forms.Form):
avatar = forms.ImageField(label=_(u"avatar"))
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
super(UploadAvatarForm, self).__init__(*args, **kwargs)
def clean_avatar(self):
data = self.cleaned_data['avatar']
if AVATAR_ALLOWED_MIMETYPES:
try:
import magic
except ImportError:
raise ImportError("python-magic library must be installed in order to use uploaded file content limitation")
# Construct 256 bytes needed for mime validation
magic_buffer = ""
for chunk in data.chunks():
magic_buffer += chunk
if len(magic_buffer) >= 256:
break
# https://github.com/ahupp/python-magic#usage
mime = magic.from_buffer(magic_buffer, mime=True)
if mime not in AVATAR_ALLOWED_MIMETYPES:
raise forms.ValidationError(
_(u"File content is invalid. Detected: %(mimetype)s Allowed content types are: %(valid_mime_list)s") %
{'valid_mime_list': ", ".join(AVATAR_ALLOWED_MIMETYPES), "mimetype": mime})
if AVATAR_ALLOWED_FILE_EXTS:
(root, ext) = os.path.splitext(data.name.lower())
if ext not in AVATAR_ALLOWED_FILE_EXTS:
raise forms.ValidationError(
_(u"%(ext)s is an invalid file extension. Authorized extensions are : %(valid_exts_list)s") %
{ 'ext' : ext, 'valid_exts_list' : ", ".join(AVATAR_ALLOWED_FILE_EXTS) })
_(u"%(ext)s is an invalid file extension. Authorized extensions are : %(valid_exts_list)s") %
{ 'ext' : ext, 'valid_exts_list' : ", ".join(AVATAR_ALLOWED_FILE_EXTS) })
if data.size > AVATAR_MAX_SIZE:
raise forms.ValidationError(
_(u"Your file is too big (%(size)s), the maximum allowed size is %(max_valid_size)s") %
{ 'size' : filesizeformat(data.size), 'max_valid_size' : filesizeformat(AVATAR_MAX_SIZE)} )
count = Avatar.objects.filter(user=self.user).count()
if AVATAR_MAX_AVATARS_PER_USER > 1 and \
count >= AVATAR_MAX_AVATARS_PER_USER:
count >= AVATAR_MAX_AVATARS_PER_USER:
raise forms.ValidationError(
_(u"You already have %(nb_avatars)d avatars, and the maximum allowed is %(nb_max_avatars)d.") %
{ 'nb_avatars' : count, 'nb_max_avatars' : AVATAR_MAX_AVATARS_PER_USER})
return
return
class PrimaryAvatarForm(forms.Form):
def __init__(self, *args, **kwargs):
user = kwargs.pop('user')
size = kwargs.pop('size', AVATAR_DEFAULT_SIZE)

View file

@ -24,3 +24,4 @@ AVATAR_ALLOWED_FILE_EXTS = getattr(settings, 'AVATAR_ALLOWED_FILE_EXTS', None)
AVATAR_CACHE_TIMEOUT = getattr(settings, 'AVATAR_CACHE_TIMEOUT', 60 * 60)
AVATAR_STORAGE = getattr(settings, 'AVATAR_STORAGE', settings.DEFAULT_FILE_STORAGE)
AVATAR_CLEANUP_DELETED = getattr(settings, 'AVATAR_CLEANUP_DELETED', False)
AVATAR_ALLOWED_MIMETYPES = getattr(settings, 'AVATAR_ALLOWED_MIMETYPES', [])

BIN
avatar/testdata/test.tiff vendored Normal file

Binary file not shown.

View file

@ -13,7 +13,7 @@ try:
dir(Image) # Placate PyFlakes
except ImportError:
import Image
def upload_helper(o, filename):
f = open(os.path.join(o.testdatapath, filename), "rb")
@ -24,7 +24,7 @@ def upload_helper(o, filename):
return response
class AvatarUploadTests(TestCase):
def setUp(self):
self.testdatapath = os.path.join(os.path.dirname(__file__), "testdata")
self.user = User.objects.create_user('test', 'lennon@thebeatles.com', 'testpassword')
@ -36,7 +36,7 @@ class AvatarUploadTests(TestCase):
response = upload_helper(self, "nonimagefile")
self.failUnlessEqual(response.status_code, 200)
self.failIfEqual(response.context['upload_avatar_form'].errors, {})
def testNormalImageUpload(self):
response = upload_helper(self, "test.png")
self.failUnlessEqual(response.status_code, 200)
@ -44,28 +44,35 @@ class AvatarUploadTests(TestCase):
self.failUnlessEqual(response.context['upload_avatar_form'].errors, {})
avatar = get_primary_avatar(self.user)
self.failIfEqual(avatar, None)
def testUnsupportedImageFormatUpload(self):
""" Check with python-magic that we detect corrupted / unapprovd image files correctly """
response = upload_helper(self, "test.tiff")
self.failUnlessEqual(response.status_code, 200)
self.failUnlessEqual(len(response.redirect_chain), 0) # Redirect only if it worked
self.failIfEqual(response.context['upload_avatar_form'].errors, {})
def testImageWithoutExtension(self):
# use with AVATAR_ALLOWED_FILE_EXTS = ('.jpg', '.png')
response = upload_helper(self, "imagefilewithoutext")
self.failUnlessEqual(response.status_code, 200)
self.failUnlessEqual(len(response.redirect_chain), 0) # Redirect only if it worked
self.failUnlessEqual(len(response.redirect_chain), 0) # Redirect only if it worked
self.failIfEqual(response.context['upload_avatar_form'].errors, {})
def testImageWithWrongExtension(self):
# use with AVATAR_ALLOWED_FILE_EXTS = ('.jpg', '.png')
response = upload_helper(self, "imagefilewithwrongext.ogg")
self.failUnlessEqual(response.status_code, 200)
self.failUnlessEqual(len(response.redirect_chain), 0) # Redirect only if it worked
self.failUnlessEqual(len(response.redirect_chain), 0) # Redirect only if it worked
self.failIfEqual(response.context['upload_avatar_form'].errors, {})
def testImageTooBig(self):
# use with AVATAR_MAX_SIZE = 1024 * 1024
response = upload_helper(self, "testbig.png")
self.failUnlessEqual(response.status_code, 200)
self.failUnlessEqual(len(response.redirect_chain), 0) # Redirect only if it worked
self.failUnlessEqual(len(response.redirect_chain), 0) # Redirect only if it worked
self.failIfEqual(response.context['upload_avatar_form'].errors, {})
def testDefaultUrl(self):
response = self.client.get(reverse('avatar_render_primary', kwargs={
'user': self.user.username,
@ -81,13 +88,13 @@ class AvatarUploadTests(TestCase):
def testNonExistingUser(self):
a = get_primary_avatar("nonexistinguser")
self.failUnlessEqual(a, None)
def testThereCanBeOnlyOnePrimaryAvatar(self):
for i in range(1, 10):
self.testNormalImageUpload()
count = Avatar.objects.filter(user=self.user, primary=True).count()
self.failUnlessEqual(count, 1)
def testDeleteAvatar(self):
self.testNormalImageUpload()
avatar = Avatar.objects.filter(user=self.user)
@ -99,7 +106,7 @@ class AvatarUploadTests(TestCase):
self.failUnlessEqual(len(response.redirect_chain), 1)
count = Avatar.objects.filter(user=self.user).count()
self.failUnlessEqual(count, 0)
def testDeletePrimaryAvatarAndNewPrimary(self):
self.testThereCanBeOnlyOnePrimaryAvatar()
primary = get_primary_avatar(self.user)
@ -116,7 +123,7 @@ class AvatarUploadTests(TestCase):
def testTooManyAvatars(self):
for i in range(0, AVATAR_MAX_AVATARS_PER_USER):
self.testNormalImageUpload()
count_before = Avatar.objects.filter(user=self.user).count()
count_before = Avatar.objects.filter(user=self.user).count()
response = upload_helper(self, "test.png")
count_after = Avatar.objects.filter(user=self.user).count()
self.failUnlessEqual(response.status_code, 200)
@ -129,5 +136,5 @@ class AvatarUploadTests(TestCase):
# def testHashFileName
# def testHashUserName
# def testChangePrimaryAvatar
# def testDeleteThumbnailAndRecreation
# def testDeleteThumbnailAndRecreation
# def testAutomaticThumbnailCreation

View file

@ -10,7 +10,7 @@ that are required. A minimal integration can work like this:
1. List this application in the ``INSTALLED_APPS`` portion of your settings
file. Your settings file will look something like::
INSTALLED_APPS = (
# ...
'avatar',
@ -18,7 +18,7 @@ that are required. A minimal integration can work like this:
2. Add the pagination urls to the end of your root urlconf. Your urlconf
will look something like::
urlpatterns = patterns('',
# ...
(r'^admin/(.*)', admin.site.root),
@ -27,20 +27,20 @@ that are required. A minimal integration can work like this:
3. Somewhere in your template navigation scheme, link to the change avatar
page::
<a href="{% url 'avatar_change' %}">Change your avatar</a>
4. Wherever you want to display an avatar for a user, first load the avatar
template tags::
{% load avatar_tags %}
Then, use the ``avatar`` tag to display an avatar of a default size::
{% avatar user %}
Or specify a size (in pixels) explicitly::
{% avatar user 65 %}
5. Optionally customize ``avatar/change.html`` and
@ -59,7 +59,7 @@ Changing an avatar
The actual view function is located at ``avatar.views.change``, and this can
be referenced by the url name ``avatar_change``. It takes two keyword
arguments: ``extra_context`` and ``next_override``. If ``extra_context`` is
provided, that context will be placed into the template's context.
provided, that context will be placed into the template's context.
If ``next_override`` is provided, the user will be redirected to the specified
URL after form submission. Otherwise the user will be redirected to the URL
@ -128,6 +128,12 @@ AVATAR_DEFAULT_URL
The default URL to default to if ``AVATAR_GRAVATAR_BACKUP`` is set to False
and there is no ``Avatar`` instance found in the system for the given user.
AVATAR_ALLOWED_MIMETYPES
Limit allowed avatar image uploads by their actual content payload and what image codecs we wish to support.
This limits website user content site attack vectors against image codec buffer overflow and similar bugs.
`You must have python-imaging library installed <https://github.com/ahupp/python-magic>`_.
Suggested safe setting: ``("image/png", "image/gif", "image/jpeg")``.
When enabled you'll get the following error on the form upload *File content is invalid. Detected: image/tiff Allowed content types are: image/png, image/gif, image/jpg*.
Management Commands
-------------------