Skip to content

Commit 3ec4dff

Browse files
committed
Check and sanitize text file upload (code is factored out in a new
helper so it can be reused elsewhere in the future). - Legacy-Id: 4380
1 parent eaf09d9 commit 3ec4dff

3 files changed

Lines changed: 68 additions & 7 deletions

File tree

ietf/utils/textupload.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import re
2+
3+
import django.forms
4+
5+
def get_cleaned_text_file_content(uploaded_file):
6+
"""Read uploaded file, try to fix up encoding to UTF-8 and
7+
transform line endings into Unix style, then return the content as
8+
a UTF-8 string. Errors are reported as
9+
django.forms.ValidationError exceptions."""
10+
11+
if not uploaded_file:
12+
return u""
13+
14+
if uploaded_file.size and uploaded_file.size > 10 * 1000 * 1000:
15+
raise django.forms.ValidationError("Text file too large (size %s)." % uploaded_file.size)
16+
17+
content = "".join(uploaded_file.chunks())
18+
19+
# try to fixup encoding
20+
import magic
21+
m = magic.open(magic.MAGIC_MIME)
22+
m.load()
23+
24+
filetype = m.buffer(content) # should look like "text/plain; charset=us-ascii"
25+
26+
if not filetype.startswith("text"):
27+
raise django.forms.ValidationError("Uploaded file does not appear to be a text file.")
28+
29+
match = re.search("charset=([\w-]+)", filetype)
30+
if not match:
31+
raise django.forms.ValidationError("File has unknown encoding.")
32+
33+
encoding = match.group(1)
34+
if "ascii" not in encoding:
35+
try:
36+
content = content.decode(encoding)
37+
except Exception as e:
38+
raise django.forms.ValidationError("Error decoding file (%s). Try submitting with UTF-8 encoding or remove non-ASCII characters." % str(e))
39+
40+
# turn line-endings into Unix style
41+
content = content.replace("\r\n", "\n").replace("\r", "\n")
42+
43+
return content.encode("utf-8")

ietf/wgcharter/tests.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,20 @@ def test_submit_charter(self):
131131
q = PyQuery(r.content)
132132
self.assertEquals(len(q('form input[name=txt]')), 1)
133133

134+
# faulty post
135+
test_file = StringIO("\x10\x11\x12") # post binary file
136+
test_file.name = "unnamed"
137+
138+
r = self.client.post(url, dict(txt=test_file))
139+
self.assertEquals(r.status_code, 200)
140+
self.assertTrue("does not appear to be a text file" in r.content)
141+
142+
# post
134143
prev_rev = charter.rev
135144

136-
test_file = StringIO("hello world")
145+
latin_1_snippet = '\xe5' * 10
146+
utf_8_snippet = '\xc3\xa5' * 10
147+
test_file = StringIO("Windows line\r\nMac line\rUnix line\n" + latin_1_snippet)
137148
test_file.name = "unnamed"
138149

139150
r = self.client.post(url, dict(txt=test_file))
@@ -143,6 +154,10 @@ def test_submit_charter(self):
143154
self.assertEquals(charter.rev, next_revision(prev_rev))
144155
self.assertTrue("new_revision" in charter.latest_event().type)
145156

157+
with open(os.path.join(self.charter_dir, charter.canonical_name() + "-" + charter.rev + ".txt")) as f:
158+
self.assertEquals(f.read(),
159+
"Windows line\nMac line\nUnix line\n" + utf_8_snippet)
160+
146161
class CharterApproveBallotTestCase(django.test.TestCase):
147162
fixtures = ['names']
148163

ietf/wgcharter/views.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from django.conf import settings
1515

1616
from ietf.utils.mail import send_mail_text, send_mail_preformatted
17+
from ietf.utils.textupload import get_cleaned_text_file_content
1718
from ietf.ietfauth.decorators import has_role, role_required
1819
from ietf.iesg.models import TelechatDate
1920
from ietf.doc.models import *
@@ -229,13 +230,14 @@ class UploadForm(forms.Form):
229230
def clean_content(self):
230231
return self.cleaned_data["content"].replace("\r", "")
231232

233+
def clean_txt(self):
234+
return get_cleaned_text_file_content(self.cleaned_data["txt"])
235+
232236
def save(self, wg, rev):
233-
fd = self.cleaned_data['txt']
234237
filename = os.path.join(settings.CHARTER_PATH, '%s-%s.txt' % (wg.charter.canonical_name(), rev))
235-
with open(filename, 'wb+') as destination:
236-
if fd:
237-
for chunk in fd.chunks():
238-
destination.write(chunk)
238+
with open(filename, 'wb') as destination:
239+
if self.cleaned_data['txt']:
240+
destination.write(self.cleaned_data['txt'])
239241
else:
240242
destination.write(self.cleaned_data['content'])
241243

@@ -246,7 +248,8 @@ def submit(request, name):
246248

247249
login = request.user.get_profile()
248250

249-
not_uploaded_yet = charter.rev.endswith("-00") and not os.path.exists(os.path.join(settings.CHARTER_PATH, '%s-%s.txt' % (charter.canonical_name(), charter.rev)))
251+
path = os.path.join(settings.CHARTER_PATH, '%s-%s.txt' % (charter.canonical_name(), charter.rev))
252+
not_uploaded_yet = charter.rev.endswith("-00") and not os.path.exists(path)
250253

251254
if not_uploaded_yet:
252255
# this case is special - we recently chartered or rechartered and have no file yet

0 commit comments

Comments
 (0)