Skip to content

Commit

Permalink
Added MAT to source.py as a metadata purge option.
Browse files Browse the repository at this point in the history
- MAT consist of a python library that makes
use of other metadata tools dedicated to specific
file-formats.

Always nice tidy and neat.

Added checkbox for metadata purge.

- Also added more validation on the
file selection for the cleanup.

Better validation on what file to write to.

Added MAT to requirements.txt

- Note: MAT is not available in
  PyPi, therefore, we clone it
  from the Tor repository.

Added MAT to source-requirements.txt

Added exiftool and poppler dependency to Debian/Ubuntu script.

Added tests for binary file upload.

Added test images for MAT
  • Loading branch information
gnusosa committed Mar 3, 2014
1 parent c59609d commit 8d7c5e4
Show file tree
Hide file tree
Showing 9 changed files with 108 additions and 6 deletions.
2 changes: 2 additions & 0 deletions install_files/source-requirements.txt
Expand Up @@ -7,3 +7,5 @@ apache2-mpm-worker
libapache2-mod-wsgi libapache2-mod-wsgi
python-pip python-pip
python-dev python-dev
libimage-exiftool-perl
python-poppler
5 changes: 5 additions & 0 deletions securedrop/source-requirements.txt
Expand Up @@ -12,3 +12,8 @@ pycrypto==2.6.1
gnupg-securedrop==1.2.5-9-g6f9d63a-dirty gnupg-securedrop==1.2.5-9-g6f9d63a-dirty
scrypt==0.6.1 scrypt==0.6.1
wsgiref==0.1.2 wsgiref==0.1.2
hachoir-core==1.3.3
hachoir-parser==1.3.4
mutagen==1.22
pdfrw==0.1
-e git+https://git.torproject.org/user/jvoisin/mat.git#egg=MAT
3 changes: 2 additions & 1 deletion securedrop/source.py
Expand Up @@ -172,12 +172,13 @@ def async_genkey(sid, codename):
def submit(): def submit():
msg = request.form['msg'] msg = request.form['msg']
fh = request.files['fh'] fh = request.files['fh']
not_clean = True if 'notclean' in request.form else False


if msg: if msg:
store.save_message_submission(g.sid, msg) store.save_message_submission(g.sid, msg)
flash("Thanks! We received your message.", "notification") flash("Thanks! We received your message.", "notification")
if fh: if fh:
store.save_file_submission(g.sid, fh.filename, fh.stream) store.save_file_submission(g.sid, fh.filename, fh.stream, fh.content_type, not_clean)
flash("Thanks! We received your document '%s'." flash("Thanks! We received your document '%s'."
% fh.filename or '[unnamed]', "notification") % fh.filename or '[unnamed]', "notification")


Expand Down
4 changes: 3 additions & 1 deletion securedrop/source_templates/lookup.html
Expand Up @@ -29,7 +29,9 @@ <h2><span class="headline">Submit a document, message, or both</span></h2>
<input name="csrf_token" type="hidden" value="{{ csrf_token() }}"/> <input name="csrf_token" type="hidden" value="{{ csrf_token() }}"/>
<p style="padding-bottom: 0"><b>Upload a file:</b></p> <p style="padding-bottom: 0"><b>Upload a file:</b></p>
<div id="browse-select"> <div id="browse-select">
<input type="file" name="fh" autocomplete="off"/> <input type="file" name="fh" autocomplete="off"/><br />
<input type="checkbox" id="cleanup" name="notclean" value="True" />
<label for="cleanup">Remove all the metadata of the file.</label>
</div> </div>


<p><b>Or just enter a message:</b></p> <p><b>Or just enter a message:</b></p>
Expand Down
10 changes: 10 additions & 0 deletions securedrop/static/css/securedrop.css
Expand Up @@ -145,6 +145,16 @@ form input#filename{
padding:0 10px; padding:0 10px;
} }


form input#cleanup{
color:#666;
border:none;
font-family: Helvetica, Arial, Verdana, sans-serif;
font-weight:400;
font-size:12px;
height:30px;
margin:15px 5px 0 5px;
padding:0 10px;
}


/* add back (removed by reset) indents and bullets for plain lists in text */ /* add back (removed by reset) indents and bullets for plain lists in text */
ul { ul {
Expand Down
32 changes: 29 additions & 3 deletions securedrop/store.py
Expand Up @@ -8,6 +8,10 @@
import tempfile import tempfile
import subprocess import subprocess
from cStringIO import StringIO from cStringIO import StringIO
from shutil import copyfileobj

from MAT import mat
from MAT import strippers


import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
Expand Down Expand Up @@ -74,15 +78,34 @@ def get_bulk_archive(filenames):
zip.write(filename, arcname=os.path.basename(filename)) zip.write(filename, arcname=os.path.basename(filename))
return zip_file return zip_file



def save_file_submission(sid, filename, stream, content_type, not_clean):
def save_file_submission(sid, filename, stream):
sanitized_filename = secure_filename(filename) sanitized_filename = secure_filename(filename)
text_plain = content_type == 'text/plain'

f = None
t = None
clean_file = False

if not_clean and not text_plain:
t = tempfile.NamedTemporaryFile()
copyfileobj(stream, t)
t.flush()
file_meta = metadata_handler(t.name)

if not file_meta.is_clean():
file_meta.remove_all()
f = open(t.name)
clean_file = True


s = StringIO() s = StringIO()
with zipfile.ZipFile(s, 'w') as zf: with zipfile.ZipFile(s, 'w') as zf:
zf.writestr(sanitized_filename, stream.read()) zf.writestr(sanitized_filename, f.read() if clean_file else stream.read())
s.reset() s.reset()


if clean_file:
f.close()
t.close()

file_loc = path(sid, "%s_doc.zip.gpg" % uuid.uuid4()) file_loc = path(sid, "%s_doc.zip.gpg" % uuid.uuid4())
crypto_util.encrypt(config.JOURNALIST_KEY, s, file_loc) crypto_util.encrypt(config.JOURNALIST_KEY, s, file_loc)


Expand All @@ -103,3 +126,6 @@ def secure_unlink(fn, recursive=False):


def delete_source_directory(source_id): def delete_source_directory(source_id):
secure_unlink(path(source_id), recursive=True) secure_unlink(path(source_id), recursive=True)

def metadata_handler(f):
return mat.create_class_file(f, False, add2archive=True)
Binary file added securedrop/tests/test_images/clean.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added securedrop/tests/test_images/dirty.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
58 changes: 57 additions & 1 deletion securedrop/tests/unit_tests.py
Expand Up @@ -25,7 +25,6 @@
import journalist import journalist
import test_setup import test_setup



def _block_on_reply_keypair_gen(codename): def _block_on_reply_keypair_gen(codename):
sid = crypto_util.hash_codename(codename) sid = crypto_util.hash_codename(codename)
while not crypto_util.getkey(sid): while not crypto_util.getkey(sid):
Expand Down Expand Up @@ -192,6 +191,63 @@ def test_submit_both(self):
self.assertIn(escape("Thanks! We received your document 'test.txt'."), self.assertIn(escape("Thanks! We received your document 'test.txt'."),
rv.data) rv.data)


def test_submit_dirty_file(self):
self.gpg = gnupg.GPG(homedir=config.GPG_KEY_DIR)
self._new_codename()
img = open(os.getcwd()+'/tests/test_images/dirty.jpg')
img_metadata = store.metadata_handler(img.name)
self.assertFalse(img_metadata.is_clean(), "The file is dirty.")
del(img_metadata)
codename = self._new_codename()
rv = self.client.post('/submit', data=dict(
msg="This is a test",
fh=(img, 'dirty.jpg'),
notclean='True',
), follow_redirects=True)
self.assert200(rv)
self.assertIn("Thanks! We received your message.", rv.data)
self.assertIn(escape("Thanks! We received your document 'dirty.jpg'."),
rv.data)

store_dirs = [os.path.join(config.STORE_DIR,d) for d in os.listdir(config.STORE_DIR) if os.path.isdir(os.path.join(config.STORE_DIR,d))]
latest_subdir = max(store_dirs, key=os.path.getmtime)
zip_gpg_files = [os.path.join(latest_subdir,f) for f in os.listdir(latest_subdir) if os.path.isfile(os.path.join(latest_subdir,f))]
zip_gpg = max(zip_gpg_files, key=os.path.getmtime)

zip_gpg_file = open(zip_gpg)
decrypted_data = self.gpg.decrypt_file(zip_gpg_file)
self.assertTrue(decrypted_data.ok, 'Checking the integrity of the data after decryption.')

s = StringIO(decrypted_data.data)
zip_file = zipfile.ZipFile(s, 'r')
clean_file = open(os.path.join(latest_subdir,'dirty.jpg'), 'w+b')
clean_file.write(zip_file.read('dirty.jpg'))
clean_file.seek(0)
zip_file.close()

# check for the actual file been clean
clean_file_metadata = store.metadata_handler(clean_file.name)
self.assertTrue(clean_file_metadata.is_clean(), "the file is now clean.")
del(clean_file_metadata)
zip_gpg_file.close()
clean_file.close()
img.close()

def test_submit_clean_file(self):
self._new_codename()
img = open(os.getcwd()+'/tests/test_images/clean.jpg')
codename = self._new_codename()
rv = self.client.post('/submit', data=dict(
msg="This is a test",
fh=(img, 'clean.jpg'),
notclean='True',
), follow_redirects=True)
self.assert200(rv)
self.assertIn("Thanks! We received your message.", rv.data)
self.assertIn(escape("Thanks! We received your document 'clean.jpg'."),
rv.data)
img.close()

@patch('zipfile.ZipFile.writestr') @patch('zipfile.ZipFile.writestr')
def test_submit_sanitizes_filename(self, zipfile_write): def test_submit_sanitizes_filename(self, zipfile_write):
"""Test that upload file name is sanitized""" """Test that upload file name is sanitized"""
Expand Down

0 comments on commit 8d7c5e4

Please sign in to comment.