Skip to content

Commit 46fc7b7

Browse files
committed
Added more validation of extension, mime type, etc. for uploaded meeting agendas and minutes. Added '.md' (markdown) as an accepted file type. Html with frames is now rejected. Factored out validation code into separate functions.
- Legacy-Id: 13849
1 parent fee74d3 commit 46fc7b7

4 files changed

Lines changed: 94 additions & 20 deletions

File tree

ietf/meeting/tests_views.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# -*- coding: utf-8 -*-
2+
13
import json
24
import os
35
import shutil
@@ -1520,7 +1522,7 @@ def test_upload_bluesheets(self):
15201522
q = PyQuery(r.content)
15211523
self.assertTrue('Upload' in unicode(q("title")))
15221524
self.assertFalse(session.sessionpresentation_set.exists())
1523-
test_file = StringIO('this is some text for a test')
1525+
test_file = StringIO(b'%PDF-1.4\n%âãÏÓ\nthis is some text for a test')
15241526
test_file.name = "not_really.pdf"
15251527
r = self.client.post(url,dict(file=test_file))
15261528
self.assertEqual(r.status_code, 302)
@@ -1530,7 +1532,7 @@ def test_upload_bluesheets(self):
15301532
self.assertEqual(r.status_code, 200)
15311533
q = PyQuery(r.content)
15321534
self.assertTrue('Revise' in unicode(q("title")))
1533-
test_file = StringIO('this is some different text for a test')
1535+
test_file = StringIO('%PDF-1.4\n%âãÏÓ\nthis is some different text for a test')
15341536
test_file.name = "also_not_really.pdf"
15351537
r = self.client.post(url,dict(file=test_file))
15361538
self.assertEqual(r.status_code, 302)
@@ -1555,7 +1557,7 @@ def test_upload_bluesheets_interim(self):
15551557
q = PyQuery(r.content)
15561558
self.assertTrue('Upload' in unicode(q("title")))
15571559
self.assertFalse(session.sessionpresentation_set.exists())
1558-
test_file = StringIO('this is some text for a test')
1560+
test_file = StringIO(b'%PDF-1.4\n%âãÏÓ\nthis is some text for a test')
15591561
test_file.name = "not_really.pdf"
15601562
r = self.client.post(url,dict(file=test_file))
15611563
self.assertEqual(r.status_code, 302)
@@ -1610,6 +1612,13 @@ def test_upload_minutes_agenda(self):
16101612
q = PyQuery(r.content)
16111613
self.assertTrue(q('form .has-error'))
16121614

1615+
test_file = StringIO('<html><frameset><frame src="foo.html"></frame><frame src="bar.html"></frame></frameset></html>')
1616+
test_file.name = "not_really.html"
1617+
r = self.client.post(url,dict(file=test_file))
1618+
self.assertEqual(r.status_code, 200)
1619+
q = PyQuery(r.content)
1620+
self.assertTrue(q('form .has-error'))
1621+
16131622
test_file = StringIO('this is some text for a test')
16141623
test_file.name = "not_really.txt"
16151624
r = self.client.post(url,dict(file=test_file,apply_to_all=False))

ietf/meeting/views.py

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@
1919
from django import forms
2020
from django.shortcuts import render, redirect, get_object_or_404
2121
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden, Http404
22+
from django.conf import settings
2223
from django.contrib import messages
2324
from django.contrib.auth.decorators import login_required
2425
from django.urls import reverse,reverse_lazy
2526
from django.db.models import Min, Max, Q
26-
from django.conf import settings
2727
from django.forms.models import modelform_factory, inlineformset_factory
2828
from django.forms import ModelForm
2929
from django.template import TemplateDoesNotExist
@@ -33,7 +33,7 @@
3333
from django.utils.text import slugify
3434
from django.views.decorators.csrf import ensure_csrf_cookie, csrf_exempt
3535
from django.views.generic import RedirectView
36-
from django.template.defaultfilters import filesizeformat
36+
3737

3838
from ietf.doc.fields import SearchableDocumentsField
3939
from ietf.doc.models import Document, State, DocEvent, NewRevisionDocEvent
@@ -64,6 +64,8 @@
6464
from ietf.utils.pipe import pipe
6565
from ietf.utils.pdf import pdf_pages
6666
from ietf.utils.text import xslugify
67+
from ietf.utils.textupload import ( validate_file_size, validate_mime_type,
68+
validate_file_extension, validate_no_html_frame, )
6769

6870
from .forms import (InterimMeetingModelForm, InterimAnnounceForm, InterimSessionModelForm,
6971
InterimCancelForm, InterimSessionInlineFormSet)
@@ -1132,6 +1134,12 @@ def add_session_drafts(request, session_id, num):
11321134
class UploadBlueSheetForm(forms.Form):
11331135
file = forms.FileField(label='Bluesheet scan to upload')
11341136

1137+
def clean_file(self):
1138+
file = self.cleaned_data['file']
1139+
validate_mime_type(file.read(), settings.MEETING_VALID_BLUESHEET_MIME_TYPES)
1140+
validate_file_extension(file.name, settings.MEETING_VALID_BLUESHEET_EXTENSIONS)
1141+
return file
1142+
11351143
@role_required('Area Director', 'Secretariat', 'IRTF Chair', 'WG Chair')
11361144
def upload_session_bluesheets(request, session_id, num):
11371145
# num is redundant, but we're dragging it along an artifact of where we are in the current URL structure
@@ -1196,7 +1204,7 @@ def upload_session_bluesheets(request, session_id, num):
11961204
'form': form,
11971205
})
11981206

1199-
VALID_MINUTES_EXTENSIONS = ('.txt','.html','.htm','.pdf')
1207+
12001208
# FIXME: This form validation code (based on the secretariat upload code) only looks at filename extensions
12011209
# It should look at the contents of the files instead.
12021210
class UploadMinutesForm(forms.Form):
@@ -1210,10 +1218,12 @@ def __init__(self, show_apply_to_all_checkbox, *args, **kwargs):
12101218

12111219
def clean_file(self):
12121220
file = self.cleaned_data['file']
1213-
if file._size > settings.SECR_MAX_UPLOAD_SIZE:
1214-
raise forms.ValidationError('Please keep filesize under %s. Requested upload size is %s' % (filesizeformat(settings.SECR_MAX_UPLOAD_SIZE),filesizeformat(file._size)))
1215-
if os.path.splitext(file.name)[1].lower() not in VALID_MINUTES_EXTENSIONS:
1216-
raise forms.ValidationError('Only these file types supported for minutes: %s' % ','.join(VALID_MINUTES_EXTENSIONS))
1221+
validate_file_size(file._size)
1222+
ext = validate_file_extension(file.name, settings.MEETING_VALID_MINUTES_EXTENSIONS)
1223+
content = file.read()
1224+
mime_type, encoding = validate_mime_type(content, settings.MEETING_VALID_MINUTES_MIME_TYPES)
1225+
if ext in ['.html', '.htm'] or mime_type in ['text/html', ]:
1226+
validate_no_html_frame(content)
12171227
return file
12181228

12191229
def upload_session_minutes(request, session_id, num):
@@ -1292,7 +1302,7 @@ def upload_session_minutes(request, session_id, num):
12921302
'form': form,
12931303
})
12941304

1295-
VALID_AGENDA_EXTENSIONS = ('.txt','.html','.htm',)
1305+
12961306
# FIXME: This form validation code (based on the secretariat upload code) only looks at filename extensions
12971307
# It should look at the contents of the files instead.
12981308
class UploadAgendaForm(forms.Form):
@@ -1306,10 +1316,12 @@ def __init__(self, show_apply_to_all_checkbox, *args, **kwargs):
13061316

13071317
def clean_file(self):
13081318
file = self.cleaned_data['file']
1309-
if file._size > settings.SECR_MAX_UPLOAD_SIZE:
1310-
raise forms.ValidationError('Please keep filesize under %s. Requested upload size is %s' % (filesizeformat(settings.SECR_MAX_UPLOAD_SIZE),filesizeformat(file._size)))
1311-
if os.path.splitext(file.name)[1].lower() not in VALID_AGENDA_EXTENSIONS:
1312-
raise forms.ValidationError('Only these file types supported for agendas: %s' % ','.join(VALID_AGENDA_EXTENSIONS))
1319+
validate_file_size(file._size)
1320+
ext = validate_file_extension(file.name, settings.MEETING_VALID_AGENDA_EXTENSIONS)
1321+
content = file.read()
1322+
mime_type, encoding = validate_mime_type(content, settings.MEETING_VALID_AGENDA_MIME_TYPES)
1323+
if ext in ['.html', '.htm'] or mime_type in ['text/html', ]:
1324+
validate_no_html_frame(content)
13131325
return file
13141326

13151327
def upload_session_agenda(request, session_id, num):
@@ -1400,7 +1412,7 @@ def upload_session_agenda(request, session_id, num):
14001412
'form': form,
14011413
})
14021414

1403-
VALID_SLIDE_EXTENSIONS = ('.doc','.docx','.pdf','.ppt','.pptx','.txt') # Note the removal of .zip
1415+
14041416
# FIXME: This form validation code (based on the secretariat upload code) only looks at filename extensions
14051417
# It should look at the contents of the files instead.
14061418
class UploadSlidesForm(forms.Form):
@@ -1415,10 +1427,8 @@ def __init__(self, show_apply_to_all_checkbox, *args, **kwargs):
14151427

14161428
def clean_file(self):
14171429
file = self.cleaned_data['file']
1418-
if file._size > settings.SECR_MAX_UPLOAD_SIZE:
1419-
raise forms.ValidationError('Please keep filesize under %s. Requested upload size is %s' % (filesizeformat(settings.SECR_MAX_UPLOAD_SIZE),filesizeformat(file._size)))
1420-
if os.path.splitext(file.name)[1].lower() not in VALID_SLIDE_EXTENSIONS:
1421-
raise forms.ValidationError('Only these file types supported for slides: %s' % ','.join(VALID_SLIDE_EXTENSIONS))
1430+
validate_file_size(file._size)
1431+
validate_file_extension(file.name, settings.MEETING_VALID_SLIDES_EXTENSIONS)
14221432
return file
14231433

14241434
def upload_session_slides(request, session_id, num, name):

ietf/settings.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,17 @@ def skip_unreadable_post(record):
722722
MEETING_MATERIALS_DEFAULT_SUBMISSION_CUTOFF_DAYS = 26
723723
MEETING_MATERIALS_DEFAULT_SUBMISSION_CORRECTION_DAYS = 50
724724

725+
MEETING_VALID_AGENDA_EXTENSIONS = ['.txt','.html','.htm', '.md', ]
726+
MEETING_VALID_AGENDA_MIME_TYPES = ['text/plain', 'text/html', ]
727+
#
728+
MEETING_VALID_MINUTES_EXTENSIONS = ['.txt','.html','.htm', '.md', '.pdf', ]
729+
MEETING_VALID_MINUTES_MIME_TYPES = ['text/plain', 'text/html', 'application/pdf', ]
730+
#
731+
MEETING_VALID_SLIDES_EXTENSIONS = ('.doc','.docx','.pdf','.ppt','.pptx','.txt') # Note the removal of .zip
732+
#
733+
MEETING_VALID_BLUESHEET_EXTENSIONS = ['.pdf', ]
734+
MEETING_VALID_BLUESHEET_MIME_TYPES = ['application/pdf', ]
735+
725736
INTERNET_DRAFT_DAYS_TO_EXPIRE = 185
726737

727738
FLOORPLAN_MEDIA_DIR = 'floor'

ietf/utils/textupload.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import re
2+
import os
3+
import magic
4+
from pyquery import PyQuery
25

6+
from django import forms
7+
from django.conf import settings
38
from django.core.exceptions import ValidationError
9+
from django.template.defaultfilters import filesizeformat
10+
11+
import debug # pyflakes:ignore
412

513
def get_cleaned_text_file_content(uploaded_file):
614
"""Read uploaded file, try to fix up encoding to UTF-8 and
@@ -46,3 +54,39 @@ def get_cleaned_text_file_content(uploaded_file):
4654
content = content.replace("\r\n", "\n").replace("\r", "\n")
4755

4856
return content.encode("utf-8")
57+
58+
def get_mime_type(content):
59+
# try to fixup encoding
60+
if hasattr(magic, "open"):
61+
m = magic.open(magic.MAGIC_MIME)
62+
m.load()
63+
filetype = m.buffer(content)
64+
else:
65+
m = magic.Magic()
66+
m.cookie = magic.magic_open(magic.MAGIC_NONE | magic.MAGIC_MIME | magic.MAGIC_MIME_ENCODING)
67+
magic.magic_load(m.cookie, None)
68+
filetype = m.from_buffer(content)
69+
70+
return filetype.split('; ', 1)
71+
72+
def validate_file_size(size):
73+
if size > settings.SECR_MAX_UPLOAD_SIZE:
74+
raise forms.ValidationError('Please keep filesize under %s. Requested upload size was %s' % (filesizeformat(settings.SECR_MAX_UPLOAD_SIZE), filesizeformat(size)))
75+
76+
def validate_mime_type(content, valid):
77+
mime_type, encoding = get_mime_type(content)
78+
if not mime_type in valid:
79+
raise forms.ValidationError('Found content with unexpected mime type: %s. Expected one of %s.' %
80+
(mime_type, ', '.join(valid) ))
81+
return mime_type, encoding
82+
83+
def validate_file_extension(name, valid):
84+
name, ext = os.path.splitext(name)
85+
if ext.lower() not in valid:
86+
raise forms.ValidationError('Found an unexpected extension: %s. Expected one of %s' % (ext, ','.join(valid)))
87+
return ext
88+
89+
def validate_no_html_frame(content):
90+
q = PyQuery(content)
91+
if q("frameset") or q("frame") or q("iframe"):
92+
raise forms.ValidationError('Found content with html frames. Please upload a file that does not use frames')

0 commit comments

Comments
 (0)