Hi all,

I revamped attachments.py in order to catch Javascript Trojans inside a zip, which were driving me crazy. While I added that, I removed the configurable archive. The attached flavor of the filter rejects just the extensions hardcoded in the source.

Enjoy
Ale
#!/usr/bin/python
# attachments -- Courier filter which blocks specified attachment types
# Copyright (C) 2005-2008  Robert Penz <rob...@penz.name>
# hacked (H) 2017 ale
#
# This file is part of pythonfilter.
#
# pythonfilter is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pythonfilter is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pythonfilter.  If not, see <http://www.gnu.org/licenses/>.

import sys
from email.message import Message, _unquotevalue
import email.utils
from zipfile import ZipFile
from StringIO import StringIO

# https://support.google.com/mail/answer/6590?hl=en
# http://www.theverge.com/2017/1/25/14391462/gmail-javascript-block-file-attachments-malware-security
blocked_extensions = ('.js',
	'.ade', '.adp', '.bat', '.chm', '.cmd', '.com', '.cpl', '.exe', '.hta',
	'.ins', '.isp', '.jar', '.jse', '.lib', '.lnk', '.mde', '.msc', '.msp',
	'.mst', '.pif', '.scr', '.sct', '.shb', '.sys', '.vb', '.vbe', '.vbs',
	'.vxd', '.wsc', '.wsf', '.wsh')


def de_comment(field):
	"""Parse a header field fragment and remove comments.

	copied from AddrlistClass.getdelimited() in email/_parseaddr.py
	"""

	slist = ['']
	quote = False
	pos = 0
	depth = 0
	while pos < len(field):
		if quote:
			quote = False
		elif field[pos] == '(':
			depth += 1
		elif field[pos] == ')':
			depth = max(depth - 1, 0)
			pos += 1
			continue
		elif field[pos] == '\\':
			quote = True
		if depth == 0:
			slist.append(field[pos])
		pos += 1

	return ''.join(slist)

def is_quoted(value):
	""" Check whether a value (string or tuple) is quoted
	"""
	if isinstance(value, tuple):
		return value[2].startswith('"')
	else:
		return value.startswith('"')

class MyMessage(Message):
	"""Email message with comments stripped
	"""

	def __init__(self, *args, **kwargs):
		Message.__init__(self, *args, **kwargs)

	def get_filename(self, failobj=None):
		"""Return the filename associated with the payload if present.

		The filename is extracted from the Content-Disposition header's
		`filename' parameter.  If that header is missing the `filename'
		parameter, this method falls back to looking for the `name' parameter.
		"""
		# changed from original: get the unquoted string
		missing = object()
		filename = self.get_param('filename', missing, 'content-disposition',
			unquote=False)
		if filename is missing:
			filename = self.get_param('name', missing, 'content-type', unquote=False)
		if filename is missing:
			return failobj

		# added to original: non quoted comments are removed
		bare = is_quoted(filename)
		if not bare:
			filename = _unquotevalue(filename)
		filename = email.utils.collapse_rfc2231_value(filename)
		if bare and '(' in filename:
			filename = de_comment(filename)
		return filename.strip().lower()

def test_fname(filename):
	""" filename defined and lower().strip()
	"""
	if filename.endswith(".gz"):
		filename = filename[0:len(filename)-3]

	return filename.endswith(blocked_extensions)

def doFilter(bodyFile, controlFileList):
	try:
		msg = email.message_from_file(open(bodyFile), _class=MyMessage)
		block = False

		for part in msg.walk():
			try:

				# multipart/* are just containers
				if part.get_content_maintype() == 'multipart':
					continue

				# get_filename() is subclassed
				filename = part.get_filename()

				if filename:
					if filename.endswith(".zip"):
						mzip = ZipFile(StringIO(part.get_payload(decode=True)))
						for fn in mzip.namelist():
							block = test_fname(fn)
							if block:
								break
					else:
						block = test_fname(filename)

			except:
				continue

			if block:
				return "550 Attachment rejected for policy reasons"

	except Exception as e:
		sys.stderr.write('attachments ' + type(e).__name__ + ': ' + str(e) + '\n')

	# nothing found --> to the next filter
	return ''


if __name__ == '__main__':
	# For debugging, you can create a file that contains a message
	# body, possibly including attachments.
	# Run this script with the name of that file as an argument,
	# and it'll print either a permanent failure code to indicate
	# that the message would be rejected, or print nothing to
	# indicate that the remaining filters would be run.
	if len(sys.argv) != 2:
		print "Usage: attachments.py <message_body_file>"
		sys.exit(0)
	print doFilter(sys.argv[1], [])
------------------------------------------------------------------------------
Check out the vibrant tech community on one of the world's most
engaging tech sites, SlashDot.org! http://sdm.link/slashdot
_______________________________________________
courier-users mailing list
courier-users@lists.sourceforge.net
Unsubscribe: https://lists.sourceforge.net/lists/listinfo/courier-users

Reply via email to