Configure banned extensions. Scan zipfile option with test case.

This commit is contained in:
Stuart Gathman
2005-06-02 15:00:17 +00:00
parent bdc6b71845
commit 0283c20eef
7 changed files with 122 additions and 13 deletions
+2
View File
@@ -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.
+23 -3
View File
@@ -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]
+7
View File
@@ -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]
+2
View File
@@ -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
+25 -8
View File
@@ -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
+51
View File
@@ -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----
+12 -2
View File
@@ -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)