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;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