Configure banned extensions. Scan zipfile option with test case.
This commit is contained in:
@@ -4,6 +4,8 @@ Here is a history of user visible changes to Python milter.
|
|||||||
DSN support for Three strikes rule and SPF SOFTFAIL
|
DSN support for Three strikes rule and SPF SOFTFAIL
|
||||||
Move /*mime*/ and dynip to Milter subpackage
|
Move /*mime*/ and dynip to Milter subpackage
|
||||||
Fix SPF unknown mechanism list not cleared
|
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.3 Experimental release with python2.4 support
|
||||||
0.7.2 Return unknown for invalid ip address in mechanism
|
0.7.2 Return unknown for invalid ip address in mechanism
|
||||||
Recognize dynamic PTR names, and don't count them as authentication.
|
Recognize dynamic PTR names, and don't count them as authentication.
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# A simple milter that has grown quite a bit.
|
# A simple milter that has grown quite a bit.
|
||||||
# $Log$
|
# $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
|
# Revision 1.3 2005/06/02 02:09:00 customdesigned
|
||||||
# Record timestamp in send_dsn.log
|
# Record timestamp in send_dsn.log
|
||||||
#
|
#
|
||||||
@@ -236,6 +239,8 @@ log_headers = False
|
|||||||
block_chinese = False
|
block_chinese = False
|
||||||
spam_words = ()
|
spam_words = ()
|
||||||
porn_words = ()
|
porn_words = ()
|
||||||
|
banned_exts = mime.extlist.split(',')
|
||||||
|
scan_zip = False
|
||||||
scan_html = True
|
scan_html = True
|
||||||
scan_rfc822 = True
|
scan_rfc822 = True
|
||||||
internal_connect = ()
|
internal_connect = ()
|
||||||
@@ -340,10 +345,11 @@ def read_config(list):
|
|||||||
})
|
})
|
||||||
cp.read(list)
|
cp.read(list)
|
||||||
tempfile.tempdir = cp.get('milter','tempdir')
|
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')
|
socketname = cp.get('milter','socket')
|
||||||
timeout = cp.getint('milter','timeout')
|
timeout = cp.getint('milter','timeout')
|
||||||
scan_rfc822 = cp.getboolean('milter','scan_rfc822')
|
scan_rfc822 = cp.getboolean('milter','scan_rfc822')
|
||||||
|
scan_zip = cp.getboolean('milter','scan_zip')
|
||||||
scan_html = cp.getboolean('milter','scan_html')
|
scan_html = cp.getboolean('milter','scan_html')
|
||||||
block_chinese = cp.getboolean('milter','block_chinese')
|
block_chinese = cp.getboolean('milter','block_chinese')
|
||||||
|
|
||||||
@@ -366,9 +372,11 @@ def read_config(list):
|
|||||||
internal_domains = cp.getlist('milter','internal_domains')
|
internal_domains = cp.getlist('milter','internal_domains')
|
||||||
|
|
||||||
global porn_words, spam_words, smart_alias, trusted_relay, hello_blacklist
|
global porn_words, spam_words, smart_alias, trusted_relay, hello_blacklist
|
||||||
|
global banned_exts
|
||||||
trusted_relay = cp.getlist('milter','trusted_relay')
|
trusted_relay = cp.getlist('milter','trusted_relay')
|
||||||
porn_words = cp.getlist('milter','porn_words')
|
porn_words = cp.getlist('milter','porn_words')
|
||||||
spam_words = cp.getlist('milter','spam_words')
|
spam_words = cp.getlist('milter','spam_words')
|
||||||
|
banned_exts = cp.getlist('milter','banned_exts')
|
||||||
hello_blacklist = cp.getlist('milter','hello_blacklist')
|
hello_blacklist = cp.getlist('milter','hello_blacklist')
|
||||||
for sa in cp.getlist('wiretap','smart_alias'):
|
for sa in cp.getlist('wiretap','smart_alias'):
|
||||||
sm = cp.getlist('wiretap',sa)
|
sm = cp.getlist('wiretap',sa)
|
||||||
@@ -897,11 +905,22 @@ class bmsMilter(Milter.Milter):
|
|||||||
for i in range(len(h),0,-1):
|
for i in range(len(h),0,-1):
|
||||||
self.chgheader(name,i-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):
|
def _chk_attach(self,msg):
|
||||||
"Filter attachments by content."
|
"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:
|
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
|
# don't let a tricky virus slip one past us
|
||||||
if scan_rfc822:
|
if scan_rfc822:
|
||||||
msg = msg.get_submsg()
|
msg = msg.get_submsg()
|
||||||
@@ -1014,6 +1033,7 @@ class bmsMilter(Milter.Milter):
|
|||||||
|
|
||||||
# filter leaf attachments through _chk_attach
|
# filter leaf attachments through _chk_attach
|
||||||
assert not msg.ismodified()
|
assert not msg.ismodified()
|
||||||
|
self.bad_extensions = ['.' + x for x in banned_exts]
|
||||||
rc = mime.check_attachments(msg,self._chk_attach)
|
rc = mime.check_attachments(msg,self._chk_attach)
|
||||||
except: # milter crashed trying to analyze mail
|
except: # milter crashed trying to analyze mail
|
||||||
exc_type,exc_value = sys.exc_info()[0:2]
|
exc_type,exc_value = sys.exc_info()[0:2]
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ log_headers = 0
|
|||||||
;[defang]
|
;[defang]
|
||||||
# do virus scanning on attached messages also
|
# do virus scanning on attached messages also
|
||||||
scan_rfc822 = 1
|
scan_rfc822 = 1
|
||||||
|
# do virus scanning on attached zipfiles also
|
||||||
|
scan_zip = 0
|
||||||
# Comment out scripts in HTML attachments. Can be CPU intensive.
|
# Comment out scripts in HTML attachments. Can be CPU intensive.
|
||||||
scan_html = 0
|
scan_html = 0
|
||||||
# reject messages with asian fonts because we can't read them
|
# 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
|
valium, rolex, sexual
|
||||||
# reject mail with these case sensitive strings in the subject
|
# reject mail with these case sensitive strings in the subject
|
||||||
spam_words = $$$, !!!, XXX, FREE, HGH
|
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
|
# See http://bmsi.com/python/pysrs.html for details
|
||||||
[srs]
|
[srs]
|
||||||
|
|||||||
@@ -169,6 +169,8 @@ rm -rf $RPM_BUILD_ROOT
|
|||||||
- DSN support for Three strikes rule and SPF SOFTFAIL
|
- DSN support for Three strikes rule and SPF SOFTFAIL
|
||||||
- Move /*mime*/ and dynip to Milter subpackage
|
- Move /*mime*/ and dynip to Milter subpackage
|
||||||
- Fix SPF unknown mechanism list not cleared
|
- 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
|
* Tue Feb 08 2005 Stuart Gathman <stuart@bmsi.com> 0.7.3-1.EL3
|
||||||
- Support EL3 and Python2.4 (some scanning/defang support broken)
|
- Support EL3 and Python2.4 (some scanning/defang support broken)
|
||||||
* Mon Aug 30 2004 Stuart Gathman <stuart@bmsi.com> 0.7.2-1
|
* Mon Aug 30 2004 Stuart Gathman <stuart@bmsi.com> 0.7.2-1
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
# $Log$
|
# $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
|
# Revision 1.1.1.4 2005/05/31 18:23:49 customdesigned
|
||||||
# Development changes since 0.7.2
|
# Development changes since 0.7.2
|
||||||
#
|
#
|
||||||
@@ -71,6 +74,7 @@
|
|||||||
import StringIO
|
import StringIO
|
||||||
import socket
|
import socket
|
||||||
import Milter
|
import Milter
|
||||||
|
import zipfile
|
||||||
|
|
||||||
import email
|
import email
|
||||||
import email.Message
|
import email.Message
|
||||||
@@ -156,7 +160,7 @@ class MimeMessage(Message):
|
|||||||
def getname(self):
|
def getname(self):
|
||||||
return self.get_param('name')
|
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
|
"""Return a list of (attr,name) pairs of attributes that IE might
|
||||||
interpret as a name - and hence decide to execute this message."""
|
interpret as a name - and hence decide to execute this message."""
|
||||||
names = []
|
names = []
|
||||||
@@ -171,7 +175,16 @@ class MimeMessage(Message):
|
|||||||
else:
|
else:
|
||||||
val = _unquotevalue(val.strip())
|
val = _unquotevalue(val.strip())
|
||||||
names.append((attr,val))
|
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):
|
def ismodified(self):
|
||||||
"True if this message or a subpart has been modified."
|
"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.
|
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."
|
"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)
|
badname = ckname(name)
|
||||||
if badname:
|
if badname:
|
||||||
hostname = socket.gethostname()
|
hostname = socket.gethostname()
|
||||||
|
if key == 'zipname':
|
||||||
|
badname = msg.get_filename()
|
||||||
msg.set_payload(virus_msg % (badname,hostname,savname))
|
msg.set_payload(virus_msg % (badname,hostname,savname))
|
||||||
del msg["content-type"]
|
del msg["content-type"]
|
||||||
del msg["content-disposition"]
|
del msg["content-disposition"]
|
||||||
@@ -312,11 +327,11 @@ check function(MimeMessage): int
|
|||||||
# save call context for Python without nested_scopes
|
# save call context for Python without nested_scopes
|
||||||
class _defang:
|
class _defang:
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self,scan_html=True):
|
||||||
self.scan_html = True
|
self.scan_html = scan_html
|
||||||
|
|
||||||
def _chk_name(self,msg):
|
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:
|
if self.scan_html:
|
||||||
check_html(msg,self._savname) # remove scripts from HTML
|
check_html(msg,self._savname) # remove scripts from HTML
|
||||||
if self.scan_rfc822:
|
if self.scan_rfc822:
|
||||||
@@ -325,12 +340,14 @@ class _defang:
|
|||||||
return check_attachments(msg,self._chk_name)
|
return check_attachments(msg,self._chk_name)
|
||||||
return rc
|
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.
|
"""Compatible entry point.
|
||||||
Replace all attachments with dangerous names."""
|
Replace all attachments with dangerous names."""
|
||||||
self._savname = savname
|
self._savname = savname
|
||||||
self._check = check
|
self._check = check
|
||||||
self.scan_rfc822 = scan_rfc822
|
self.scan_rfc822 = scan_rfc822
|
||||||
|
self.scan_zip = scan_zip
|
||||||
check_attachments(msg,self._chk_name)
|
check_attachments(msg,self._chk_name)
|
||||||
if msg.ismodified():
|
if msg.ismodified():
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -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
@@ -1,4 +1,7 @@
|
|||||||
# $Log$
|
# $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
|
# Revision 1.23 2005/02/11 18:34:14 stuart
|
||||||
# Handle garbage after quote in boundary.
|
# Handle garbage after quote in boundary.
|
||||||
#
|
#
|
||||||
@@ -63,7 +66,7 @@ class MimeTestCase(unittest.TestCase):
|
|||||||
def testDefang(self,vname='virus1',part=1,
|
def testDefang(self,vname='virus1',part=1,
|
||||||
fname='LOVE-LETTER-FOR-YOU.TXT.vbs'):
|
fname='LOVE-LETTER-FOR-YOU.TXT.vbs'):
|
||||||
msg = mime.message_from_file(open('test/'+vname,"r"))
|
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")
|
self.failUnless(msg.ismodified(),"virus not removed")
|
||||||
oname = vname + '.out'
|
oname = vname + '.out'
|
||||||
msg.dump(open('test/'+oname,"w"))
|
msg.dump(open('test/'+oname,"w"))
|
||||||
@@ -71,7 +74,8 @@ class MimeTestCase(unittest.TestCase):
|
|||||||
txt2 = msg.get_payload()
|
txt2 = msg.get_payload()
|
||||||
if type(txt2) == list:
|
if type(txt2) == list:
|
||||||
txt2 = txt2[part].get_payload()
|
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):
|
def testDefang3(self):
|
||||||
self.testDefang('virus3',0,'READER_DIGEST_LETTER.TXT.pif')
|
self.testDefang('virus3',0,'READER_DIGEST_LETTER.TXT.pif')
|
||||||
@@ -121,6 +125,12 @@ class MimeTestCase(unittest.TestCase):
|
|||||||
name = parts[1].getname()
|
name = parts[1].getname()
|
||||||
self.failUnless(name == "Jim&amp;Girlz.jpg","name=%s"%name)
|
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=""):
|
def testHTML(self,fname=""):
|
||||||
result = StringIO.StringIO()
|
result = StringIO.StringIO()
|
||||||
filter = mime.HTMLScriptFilter(result)
|
filter = mime.HTMLScriptFilter(result)
|
||||||
|
|||||||
Reference in New Issue
Block a user