From 0283c20eef92cf9aa1c09bf43132e62a8ede8cce Mon Sep 17 00:00:00 2001
From: Stuart Gathman <stuart@gathman.org>
Date: Thu, 2 Jun 2005 15:00:17 +0000
Subject: [PATCH] Configure banned extensions.  Scan zipfile option with test
 case.

---
 NEWS        |  2 ++
 bms.py      | 26 +++++++++++++++++++++++---
 milter.cfg  |  7 +++++++
 milter.spec |  2 ++
 mime.py     | 33 +++++++++++++++++++++++++--------
 test/zip1   | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++
 testmime.py | 14 ++++++++++++--
 7 files changed, 122 insertions(+), 13 deletions(-)
 create mode 100644 test/zip1

diff --git a/NEWS b/NEWS
index 13f961f..409e3a7 100644
--- a/NEWS
+++ b/NEWS
@@ -4,6 +4,8 @@ Here is a history of user visible changes to Python milter.
 	DSN support for Three strikes rule and SPF SOFTFAIL
 	Move /*mime*/ and dynip to Milter subpackage
 	Fix SPF unknown mechanism list not cleared
+	Make banned extensions configurable.
+	Option to scan zipfiles for bad extensions.
 0.7.3	Experimental release with python2.4 support
 0.7.2	Return unknown for invalid ip address in mechanism
 	Recognize dynamic PTR names, and don't count them as authentication.
diff --git a/bms.py b/bms.py
index 3a2421f..197325b 100644
--- a/bms.py
+++ b/bms.py
@@ -1,6 +1,9 @@
 #!/usr/bin/env python
 # A simple milter that has grown quite a bit.
 # $Log$
+# Revision 1.4  2005/06/02 04:18:55  customdesigned
+# Update copyright notices after reading article on /.
+#
 # Revision 1.3  2005/06/02 02:09:00  customdesigned
 # Record timestamp in send_dsn.log
 #
@@ -236,6 +239,8 @@ log_headers = False
 block_chinese = False
 spam_words = ()
 porn_words = ()
+banned_exts = mime.extlist.split(',')
+scan_zip = False
 scan_html = True
 scan_rfc822 = True
 internal_connect = ()
@@ -340,10 +345,11 @@ def read_config(list):
   })
   cp.read(list)
   tempfile.tempdir = cp.get('milter','tempdir')
-  global socketname, scan_rfc822, scan_html, block_chinese, timeout
+  global socketname, scan_rfc822, scan_html, block_chinese, timeout, scan_zip
   socketname = cp.get('milter','socket')
   timeout = cp.getint('milter','timeout')
   scan_rfc822 = cp.getboolean('milter','scan_rfc822')
+  scan_zip = cp.getboolean('milter','scan_zip')
   scan_html = cp.getboolean('milter','scan_html')
   block_chinese = cp.getboolean('milter','block_chinese')
 
@@ -366,9 +372,11 @@ def read_config(list):
   internal_domains = cp.getlist('milter','internal_domains')
 
   global porn_words, spam_words, smart_alias, trusted_relay, hello_blacklist
+  global banned_exts
   trusted_relay = cp.getlist('milter','trusted_relay')
   porn_words = cp.getlist('milter','porn_words')
   spam_words = cp.getlist('milter','spam_words')
+  banned_exts = cp.getlist('milter','banned_exts')
   hello_blacklist = cp.getlist('milter','hello_blacklist')
   for sa in cp.getlist('wiretap','smart_alias'):
     sm = cp.getlist('wiretap',sa)
@@ -897,11 +905,22 @@ class bmsMilter(Milter.Milter):
 	for i in range(len(h),0,-1):
 	  self.chgheader(name,i-1,'')
 
+  def _chk_ext(self,name):
+    "Check a name for dangerous Winblows extensions."
+    if not name: return name
+    lname = name.lower()
+    for ext in self.bad_extensions:
+      if lname.endswith(ext): return name
+    return None
+
+    
   def _chk_attach(self,msg):
     "Filter attachments by content."
-    mime.check_name(msg,self.tempname)	# check for bad extensions
+    # check for bad extensions
+    mime.check_name(msg,self.tempname,ckname=self._chk_ext,scan_zip=scan_zip)
+    # remove scripts from HTML
     if scan_html:
-      mime.check_html(msg,self.tempname)	# remove scripts from HTML
+      mime.check_html(msg,self.tempname)	
     # don't let a tricky virus slip one past us
     if scan_rfc822:
       msg = msg.get_submsg()
@@ -1014,6 +1033,7 @@ class bmsMilter(Milter.Milter):
 
       # filter leaf attachments through _chk_attach
       assert not msg.ismodified()
+      self.bad_extensions = ['.' + x for x in banned_exts]
       rc = mime.check_attachments(msg,self._chk_attach)
     except:	# milter crashed trying to analyze mail
       exc_type,exc_value = sys.exc_info()[0:2]
diff --git a/milter.cfg b/milter.cfg
index 6204f57..55a552a 100644
--- a/milter.cfg
+++ b/milter.cfg
@@ -28,6 +28,8 @@ log_headers = 0
 ;[defang]
 # do virus scanning on attached messages also
 scan_rfc822 = 1
+# do virus scanning on attached zipfiles also
+scan_zip = 0
 # Comment out scripts in HTML attachments.  Can be CPU intensive.
 scan_html = 0
 # reject messages with asian fonts because we can't read them
@@ -45,6 +47,11 @@ porn_words = penis, breast, pussy, horse cock, porn, xenical, diet pill, d1ck,
 	valium, rolex, sexual
 # reject mail with these case sensitive strings in the subject
 spam_words = $$$, !!!, XXX, FREE, HGH
+# attachments with these extensions will be replaced with a warning
+# message.  A copy of the original will be saved. 
+banned_exts = ade,adp,asd,asx,asp,bas,bat,chm,cmd,com,cpl,crt,dll,exe,hlp,hta,
+	inf,ins,isp,js,jse,lnk,mdb,mde,msc,msi,msp,mst,ocx,pcd,pif,reg,scr,sct,
+	shs,url,vb,vbe,vbs,wsc,wsf,wsh 
 
 # See http://bmsi.com/python/pysrs.html for details
 [srs]
diff --git a/milter.spec b/milter.spec
index 354c588..a32e783 100644
--- a/milter.spec
+++ b/milter.spec
@@ -169,6 +169,8 @@ rm -rf $RPM_BUILD_ROOT
 - DSN support for Three strikes rule and SPF SOFTFAIL
 - Move /*mime*/ and dynip to Milter subpackage
 - Fix SPF unknown mechanism list not cleared
+- Make banned extensions configurable.
+- Option to scan zipfiles for bad extensions.
 * Tue Feb 08 2005 Stuart Gathman <stuart@bmsi.com> 0.7.3-1.EL3
 - Support EL3 and Python2.4 (some scanning/defang support broken)
 * Mon Aug 30 2004 Stuart Gathman <stuart@bmsi.com> 0.7.2-1
diff --git a/mime.py b/mime.py
index edc3d61..df95373 100644
--- a/mime.py
+++ b/mime.py
@@ -1,4 +1,7 @@
 # $Log$
+# Revision 1.2  2005/06/02 04:18:55  customdesigned
+# Update copyright notices after reading article on /.
+#
 # Revision 1.1.1.4  2005/05/31 18:23:49  customdesigned
 # Development changes since 0.7.2
 #
@@ -71,6 +74,7 @@
 import StringIO
 import socket
 import Milter
+import zipfile
 
 import email
 import email.Message
@@ -156,7 +160,7 @@ class MimeMessage(Message):
   def getname(self):
     return self.get_param('name')
 
-  def getnames(self):
+  def getnames(self,scan_zip=False):
     """Return a list of (attr,name) pairs of attributes that IE might
        interpret as a name - and hence decide to execute this message."""
     names = []
@@ -171,7 +175,16 @@ class MimeMessage(Message):
       else:
 	  val = _unquotevalue(val.strip())
       names.append((attr,val))
-    return names + [("filename",self.get_filename())]
+    names += [("filename",self.get_filename())]
+    if scan_zip:
+      for key,name in names:
+	if name and name.lower().endswith('.zip'):
+	  txt = self.get_payload(decode=True)
+	  fp =  StringIO.StringIO(txt)
+	  zipf = zipfile.ZipFile(fp,'r')
+	  for nm in zipf.namelist():
+	    names.append(('zipname',nm))
+    return names
 
   def ismodified(self):
     "True if this message or a subpart has been modified."
@@ -279,12 +292,14 @@ A copy of your original message was saved as '%s:%s'.
 See your administrator.
 """
 
-def check_name(msg,savname=None,ckname=check_ext):
+def check_name(msg,savname=None,ckname=check_ext,scan_zip=False):
   "Replace attachment with a warning if its name is suspicious."
-  for key,name in msg.getnames():
+  for key,name in msg.getnames(scan_zip):
     badname = ckname(name)
     if badname:
       hostname = socket.gethostname()
+      if key == 'zipname':
+        badname = msg.get_filename()
       msg.set_payload(virus_msg % (badname,hostname,savname))
       del msg["content-type"]
       del msg["content-disposition"]
@@ -312,11 +327,11 @@ check	function(MimeMessage): int
 # save call context for Python without nested_scopes
 class _defang:
 
-  def __init__(self):
-    self.scan_html = True
+  def __init__(self,scan_html=True):
+    self.scan_html = scan_html
 
   def _chk_name(self,msg):
-    rc = check_name(msg,self._savname,self._check)
+    rc = check_name(msg,self._savname,self._check,self.scan_zip)
     if self.scan_html:
       check_html(msg,self._savname)	# remove scripts from HTML
     if self.scan_rfc822:
@@ -325,12 +340,14 @@ class _defang:
         return check_attachments(msg,self._chk_name)
     return rc
 
-  def __call__(self,msg,savname=None,check=check_ext,scan_rfc822=True):
+  def __call__(self,msg,savname=None,check=check_ext,scan_rfc822=True,
+  		scan_zip=False):
     """Compatible entry point.
     Replace all attachments with dangerous names."""
     self._savname = savname
     self._check = check
     self.scan_rfc822 = scan_rfc822
+    self.scan_zip = scan_zip
     check_attachments(msg,self._chk_name)
     if msg.ismodified():
       return True
diff --git a/test/zip1 b/test/zip1
new file mode 100644
index 0000000..6dfcb75
--- /dev/null
+++ b/test/zip1
@@ -0,0 +1,51 @@
+From paulp@go2net.com  Wed Jun  1 22:35:12 2005
+Return-Path: <paulp@go2net.com>
+Received: from mail.bmsi.com (spidey.bmsi.com [192.168.9.81])
+	by bmsred.bmsi.com (8.13.1/8.12.10) with ESMTP id j522ZCQg014058
+	for <stuart@bmsred.bmsi.com>; Wed, 1 Jun 2005 22:35:12 -0400
+Received: from 127.0.0.1 ([220.117.92.241])
+	by mail.bmsi.com (8.13.1/8.13.1) with ESMTP id j522Ynjm028604
+	for stuart@bmsi.com; Wed, 1 Jun 2005 22:34:51 -0400
+Message-Id: <200506020234.j522Ynjm028604@mail.bmsi.com>
+SUBJECT: urgent
+FROM: paulp@go2net.com
+TO: stuart@bmsi.com
+DATE: [[ ��, 02 6 2005 ���� 11:34:47 ]]
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="--------bound--"
+X-DSpam-Score: 0.081200
+Received-SPF: neutral (mail.bmsi.com: guessing: 220.117.92.241 is neither permitted nor denied by domain of go2net.com)
+Status: RO
+X-Status: 
+X-Keywords: NonJunk         
+
+----------bound--
+Content-Type: text/plain; charset=us-ascii
+Content-Transfer-Encoding: 7bit
+
+Hi 
+
+Sorry, I forgot to send an important
+document to you in that last email. I had an important phone call.
+Please checkout attached doc file when you have a moment.
+
+Best Regards
+
+<!DSPAM:1043AE6B6492860536935410>
+
+
+----------bound--
+Content-Type: application/x-msdownload; name="zip.zip"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename="zip.zip"
+
+UEsDBAoAAAAAADVVwjLaV2nEGgAAABoAAAAzABUAemlwLmRvYyAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAuZXhlVVQJAAOmGp9CphqfQlV4BACGA2UAVGhpcyBw
+cm9ncmFtIHdhcyBhIHZpcnVzLgpQSwECFwMKAAAAAAA1VcIy2ldpxBoAAAAaAAAAMwANAAAA
+AAABAAAAtIEAAAAAemlwLmRvYyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAuZXhlVVQFAAOmGp9CVXgAAFBLBQYAAAAAAQABAG4AAACAAAAAAAA=
+----------bound--
+
+
+----------bound----
+
diff --git a/testmime.py b/testmime.py
index 019ec89..004f43e 100644
--- a/testmime.py
+++ b/testmime.py
@@ -1,4 +1,7 @@
 # $Log$
+# Revision 1.1.1.2  2005/05/31 18:23:49  customdesigned
+# Development changes since 0.7.2
+#
 # Revision 1.23  2005/02/11 18:34:14  stuart
 # Handle garbage after quote in boundary.
 #
@@ -63,7 +66,7 @@ class MimeTestCase(unittest.TestCase):
   def testDefang(self,vname='virus1',part=1,
   	fname='LOVE-LETTER-FOR-YOU.TXT.vbs'):
     msg = mime.message_from_file(open('test/'+vname,"r"))
-    mime.defang(msg)
+    mime.defang(msg,scan_zip=True)
     self.failUnless(msg.ismodified(),"virus not removed")
     oname = vname + '.out'
     msg.dump(open('test/'+oname,"w"))
@@ -71,7 +74,8 @@ class MimeTestCase(unittest.TestCase):
     txt2 = msg.get_payload()
     if type(txt2) == list:
       txt2 = txt2[part].get_payload()
-    self.failUnless(txt2.rstrip()+'\n' == mime.virus_msg % (fname,hostname,None),txt2)
+    self.failUnless(
+      txt2.rstrip()+'\n' == mime.virus_msg % (fname,hostname,None),txt2)
 
   def testDefang3(self):
     self.testDefang('virus3',0,'READER_DIGEST_LETTER.TXT.pif')
@@ -121,6 +125,12 @@ class MimeTestCase(unittest.TestCase):
     name = parts[1].getname()
     self.failUnless(name == "Jim&amp;amp;Girlz.jpg","name=%s"%name)
 
+  def testZip(self,vname="zip1",fname='zip.zip'):
+    self.testDefang('zip1',1,'zip.zip')
+    msg = mime.message_from_file(open('test/'+vname,"r"))
+    mime.defang(msg,scan_zip=False)
+    self.failIf(msg.ismodified())
+
   def testHTML(self,fname=""):
     result = StringIO.StringIO()
     filter = mime.HTMLScriptFilter(result)
-- 
GitLab