diff --git a/NEWS b/NEWS index 13f961fdb75886ac168932d9f8fedbe5c7a1b6cb..409e3a73a5fb41488e42a4c25f49a1f2f1db01bf 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 3a2421f4967fd6844a36d72b1ead905b5ad48d9e..197325b120be39c1cdcaeedfce5796148c8e6d37 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 6204f57e5e7598c5bd8b8e19d7567411ee1ea609..55a552a1191e4695f20824f08bea74965bdedc4f 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 354c588eba96824a75c7552bddf11352249b9305..a32e7838b40d89455cacbd849b4c8e9425028a84 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 edc3d61c17f4b3da75b7955543c4e4e9f662dce7..df9537323dc77fad50719e96fb33e6b07a964587 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 0000000000000000000000000000000000000000..6dfcb7585f97d58049a34a7fcc515f65c703f718 --- /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 019ec8907a2ca0e8a79b25aed1e584e480435f20..004f43e2743088c146a21241d278cf802b0cd35a 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)