diff --git a/avatar/forms.py b/avatar/forms.py
index 012ab08..902ec39 100644
--- a/avatar/forms.py
+++ b/avatar/forms.py
@@ -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("""
""" %
+ return mark_safe("""
""" %
(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)
diff --git a/avatar/settings.py b/avatar/settings.py
index 12d5db2..8f696dd 100644
--- a/avatar/settings.py
+++ b/avatar/settings.py
@@ -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', [])
diff --git a/avatar/testdata/test.tiff b/avatar/testdata/test.tiff
new file mode 100644
index 0000000..c3d899b
Binary files /dev/null and b/avatar/testdata/test.tiff differ
diff --git a/avatar/tests.py b/avatar/tests.py
index 6d233d8..c3d29e2 100644
--- a/avatar/tests.py
+++ b/avatar/tests.py
@@ -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
diff --git a/docs/usage.txt b/docs/usage.txt
index 327e8ba..6c38a95 100644
--- a/docs/usage.txt
+++ b/docs/usage.txt
@@ -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::
-
+
Change your avatar
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 `_.
+ 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
-------------------