mirror of
https://github.com/jazzband/django-avatar.git
synced 2026-05-12 17:43:11 +00:00
Added image content type white list with python-magic
This commit is contained in:
parent
b8844d8cd9
commit
217baaa317
5 changed files with 72 additions and 34 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
BIN
avatar/testdata/test.tiff
vendored
Normal file
Binary file not shown.
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
-------------------
|
||||
|
|
|
|||
Loading…
Reference in a new issue