# -*- Perl -*- #*********************************************************************** # # mimedefang-filter # # Suggested minimum-protection filter for Microsoft Windows clients, plus # SpamAssassin checks if SpamAssassin is installed. # # Copyright (C) 2002 Roaring Penguin Software Inc. # # This program may be distributed under the terms of the GNU General # Public License, Version 2, or (at your option) any later version. # # $Id: suggested-minimum-filter-for-windows-clients,v 1.43 2002/08/23 19:16:29 dfs Exp $ #*********************************************************************** #*********************************************************************** # Set administrator's e-mail address here. The administrator receives # quarantine messages and is listed as the contact for site-wide # MIMEDefang policy. A good example would be 'defang-admin@mydomain.com' #*********************************************************************** #$AdminAddress = 'postmaster@localhost'; $AdminAddress = 'postmaster@XXX'; #$AdminName = "MIMEDefang Administrator's Full Name"; $AdminName = "MIMEDefang Administrator"; #*********************************************************************** # Set the e-mail address from which MIMEDefang quarantine warnings and # user notifications appear to come. A good example would be # 'mimedefang@mydomain.com'. Make sure to have an alias for this # address if you want replies to it to work. #*********************************************************************** #$DaemonAddress = 'mimedefang@localhost'; $DaemonAddress = 'postmaster@XXX'; #*********************************************************************** # If you set $AddWarningsInline to 1, then MIMEDefang tries *very* hard # to add warnings directly in the message body (text or html) rather # than adding a separate "WARNING.TXT" MIME part. If the message # has no text or html part, then a separate MIME part is still used. #*********************************************************************** $AddWarningsInline = 0; #*********************************************************************** # Set various stupid things your mail client does below. #*********************************************************************** # Set the next one if your mail client cannot handle nested multipart # messages. DO NOT set this lightly; it will cause action_add_part to # work rather strangely. Leave it at zero, even for MS Outlook, unless # you have serious problems. $Stupidity{"flatten"} = 0; # Set the next one if your mail client cannot handle multiple "inline" # parts. $Stupidity{"NoMultipleInlines"} = 0; # This is a list of permitted ext when the message is larger than # $MaxOutSize $MaxOutSize = 100*1024; sub filter_good_filename ($) { my($entity) = @_; my($good_exts, $re); # Good extensions $good_exts = '\.(gz|zip|rar|bz2|tgz|tbz)$'; $re = $good_exts; return re_match($entity, $re); } # This procedure returns true for entities with bad filenames. sub filter_bad_filename ($) { my($entity) = @_; my($bad_exts, $re); # Bad extensions #$bad_exts = '(ade|adp|bas|bat|chm|cmd|com|cpl|crt|dll|exe|hlp|hta|inf|ini|ins|isp|jse?|lib|lnk|mdb|mde|msc|msi|msp|mst|ocx|pcd|pif|reg|scr|sct|shb|shs|sys|url|vb|vbe|vbs|vxd|wsc|wsf|wsh)'; $bad_exts = '(avi|asf|mp3|mpg|mpe|mpeg|mov|mng|ade|adp|bas|bat|chm|cmd|com|cpl|crt|dll|exe|hlp|hta|inf|ini|ins|isp|jse?|lib|lnk|mdb|mde|msc|msi|msp|mst|ocx|pcd|pif|reg|scr|sct|shb|shs|sys|url|vb|vbe|vbs|vxd|wsc|wsf|wsh)'; # Do not allow: # - curlies # - bad extensions (possibly with trailing dots) at end or # followed by non-alphanum $re = '(\{)|(\})|(\.' . $bad_exts . ')\.*([^-A-Za-z0-9_.]|$)'; return re_match($entity, $re); } # Scan for a virus using the first supported virus scanner we find. sub message_contains_virus () { #return message_contains_virus_avp() if ($Features{'Virus:AVP'}); #return message_contains_virus_fprot() if ($Features{'Virus:FPROT'}); #return message_contains_virus_fsav() if ($Features{'Virus:FSAV'}); #return message_contains_virus_hbedv() if ($Features{'Virus:HBEDV'}); #return message_contains_virus_nai() if ($Features{'Virus:NAI'}); #return message_contains_virus_nvcc() if ($Features{'Virus:NVCC'}); #return message_contains_virus_rav() if ($Features{'Virus:RAV'}); #return message_contains_virus_sophie() if ($Features{'Virus:SOPHIE'}); #return message_contains_virus_sophos() if ($Features{'Virus:SOPHOS'}); #return message_contains_virus_trend() if ($Features{'Virus:TREND'}); return message_contains_virus_filescan() if ($Features{'Virus:FileScan'}); #return message_contains_virus_clamav() if ($Features{'Virus:CLAMAV'}); return (wantarray ? (0, 'ok', 'ok') : 0); } # Scan for a virus using the first supported virus scanner we find. sub entity_contains_virus ($) { my($e) = @_; #return entity_contains_virus_avp($e) if ($Features{'Virus:AVP'}); #return entity_contains_virus_fprot($e) if ($Features{'Virus:FPROT'}); #return entity_contains_virus_fsav($e) if ($Features{'Virus:FSAV'}); #return entity_contains_virus_hbedv($e) if ($Features{'Virus:HBEDV'}); #return entity_contains_virus_nai($e) if ($Features{'Virus:NAI'}); #return entity_contains_virus_nvcc($e) if ($Features{'Virus:NVCC'}); #return entity_contains_virus_rav($e) if ($Features{'Virus:RAV'}); #return entity_contains_virus_sophie($e) if ($Features{'Virus:SOPHIE'}); #return entity_contains_virus_sophos($e) if ($Features{'Virus:SOPHOS'}); #return entity_contains_virus_trend($e) if ($Features{'Virus:TREND'}); return entity_contains_virus_filescan($e) if ($Features{'Virus:FileScan'}); #return entity_contains_virus_clamav($e) if ($Features{'Virus:CLAMAV'}); return (wantarray ? (0, 'ok', 'ok') : 0); } #*********************************************************************** # %PROCEDURE: filter_begin # %ARGUMENTS: # None # %RETURNS: # Nothing # %DESCRIPTION: # Called just before e-mail parts are processed #*********************************************************************** sub filter_begin () { # ALWAYS drop messages with suspicious chars in headers if ($SuspiciousCharsInHeaders) { action_quarantine_entire_message(); action_notify_administrator("Message quarantined because of suspicious characters in headers"); # Do NOT allow message to reach recipient(s) return action_discard(); } if ($RelayAddr eq "172.16.1.227" or $RelayAddr eq "172.18.1.13" or $RelayAddr eq "172.17.1.13" or $RelayAddr eq "172.22.1.14" or $RelayAddr eq "172.21.1.14") { $LocalRelay = 1; } else { $LocalRelay = 0; } # Scan for viruses if any virus-scanners are installed my($code, $category, $action) = message_contains_virus(); $FoundVirus = ($category eq "virus"); } #*********************************************************************** # %PROCEDURE: filter # %ARGUMENTS: # entity -- a Mime::Entity object (see MIME-tools documentation for details) # fname -- the suggested filename, taken from the MIME Content-Disposition: # header. If no filename was suggested, then fname is "" # ext -- the file extension (everything from the last period in the name # to the end of the name, including the period.) # type -- the MIME type, taken from the Content-Type: header. # # NOTE: There are two likely and one unlikely place for a filename to # appear in a MIME message: In Content-Disposition: filename, in # Content-Type: name, and in Content-Description. If you are paranoid, # you will use the re_match and re_match_ext functions, which return true # if ANY of these possibilities match. re_match checks the whole name; # re_match_ext checks the extension. See the sample filter below for usage. # %RETURNS: # Nothing # %DESCRIPTION: # This function is called once for each part of a MIME message. # There are many action_*() routines which can decide the fate # of each part; see the mimedefang-filter man page. #*********************************************************************** sub filter ($$$$) { my($entity, $fname, $ext, $type) = @_; return if message_rejected(); # Avoid unnecessary work # Reject Korean Mail $head = $entity->head; $charset = $head->mime_attr("content-type.charset"); if (defined($charset)) { $charset =~ tr/A-Z/a-z/; if ($charset eq "ks_c_5601-1987" or $charset eq "euc-kr") { return action_bounce("Spam from Korea not accepted."); } } # Virus scan if ($FoundVirus) { my($code, $category, $action); $VirusScannerMessages = ""; ($code, $category, $action) = entity_contains_virus($entity); if ($category eq "virus") { #return action_quarantine($entity, "A known virus was discovered and deleted. Virus-scanner messages follow:\n$VirusScannerMessages\n\n"); if (!$LocalRelay) { return action_drop_with_warning("Un virus e' stato scoperto e cancellato. Segue il messaggio dell'antivirus:\n$VirusScannerMessages\n\n"); } else { action_notify_administrator("Message bounced with virus $VirusScannerMessages"); return action_bounce("Il messaggio e' stato rifiutato perche' e` presente un Virus"); } } } if (filter_bad_filename($entity)) { if (!LocalRelay) { #return action_quarantine($entity, "An attachment named $fname was removed from this document as it\nconstituted a security hazard. If you require this document, please contact\nthe sender and arrange an alternate means of receiving it.\n"); return action_drop_with_warning("Un allegato chiamato $fname e' stato rimosso da questo documento\ndato che si tratta di un allegato pericoloso. Se hai bisogno di questo\ndocumento contatta il mittente.\n"); } else { return action_bounce("Il messaggio e' stato rifiutato perche' e` presente un allegato pericoloso chiamato $fname"); } } # eml is bad if it's not multipart if (re_match($entity, '\.eml')) { if (!LocalRelay) { #return action_quarantine($entity, "A non-multipart attachment named $fname was removed from this document as it\nconstituted a security hazard. If you require this document, please contact\nthe sender and arrange an alternate means of receiving it.\n"); return action_drop_with_warning("Un allegato non-multipart chiamato $fname e' stato rimosso da questo documento\ndato che si tratta di un allegato pericoloso. Se hai bisogno di questo\ndocumento contatta il mittente.\n"); } else { return action_bounce("Il messaggio e' stato rifiutato perche' e` presente un allegato non-multipart chiamato $fname"); } } # Clean up HTML if Anomy::HTMLCleaner is installed. if (!$LocalRelay) { if ($Features{"HTMLCleaner"}) { if ($type eq "text/html") { return anomy_clean_html($entity); } } } # Usually people break my balls because I restrict the # message size. OK: I have changed the limit. # Italian people are lazy. I have never seen a compressed # attach in an e-mail, so this time ball-breakers *MUST* compress the # attach if the message is bigger than $MaxOutSize if ($LocalRelay) { if (-s "./INPUTMSG" > $MaxOutSize) { if (filter_good_filename($entity) or ($type =~ m+^text/+) or ($type eq "text") ) { return action_accept(); } else { return action_bounce("You are lazy! Compress the attach"); } } } return action_accept(); } #*********************************************************************** # %PROCEDURE: filter_multipart # %ARGUMENTS: # entity -- a Mime::Entity object (see MIME-tools documentation for details) # fname -- the suggested filename, taken from the MIME Content-Disposition: # header. If no filename was suggested, then fname is "" # ext -- the file extension (everything from the last period in the name # to the end of the name, including the period.) # type -- the MIME type, taken from the Content-Type: header. # %RETURNS: # Nothing # %DESCRIPTION: # This is called for multipart "container" parts such as message/rfc822. # You cannot replace the body (because multipart parts have no body), # but you should check for bad filenames. #*********************************************************************** sub filter_multipart ($$$$) { my($entity, $fname, $ext, $type) = @_; if (!$LocalRelay) { if (filter_bad_filename($entity)) { action_notify_administrator("A MULTIPART attachment of type $type, named $fname was dropped.\n"); #return action_drop_with_warning("An attachment of type $type, named $fname was removed from this document as it\nconstituted a security hazard. If you require this document, please contact\nthe sender and arrange an alternate means of receiving it.\n"); return action_drop_with_warning("Un allegato di tipo $type, chiamato $fname e' stato rimosso da questo documento\ndato che si tratta di un allegato pericoloso. Se hai bisogno di questo documento\ncontatta il mittente.\n"); } # eml is bad if it's not message/rfc822 if (re_match($entity, '\.eml') and ($type ne "message/rfc822")) { #return action_drop_with_warning("A non-message/rfc822 attachment named $fname was removed from this document as it\nconstituted a security hazard. If you require this document, please contact\nthe sender and arrange an alternate means of receiving it.\n"); return action_drop_with_warning("Un allegato di tipo non-message/rfc822, chiamato $fname e' stato rimosso da questo documento\ndato che si tratta di un allegato pericoloso. Se hai bisogno di questo documento\ncontatta il mittente.\n"); } return action_accept(); } if ($LocalRelay) { if (filter_bad_filename($entity)) { action_notify_administrator("Message bounced because of A MULTIPART attachment of type $type, named $fname"); #return action_drop_with_warning("An attachment of type $type, named $fname was removed from this document as it\nconstituted a security hazard. If you require this document, please contact\nthe sender and arrange an alternate means of receiving it.\n"); return action_bounce("Il messaggio e' stato rifiutato perche' contiene un allegato di tipo $type, chiamato $fname"); } # eml is bad if it's not message/rfc822 if (re_match($entity, '\.eml') and ($type ne "message/rfc822")) { #return action_drop_with_warning("A non-message/rfc822 attachment named $fname was removed from this document as it\nconstituted a security hazard. If you require this document, please contact\nthe sender and arrange an alternate means of receiving it.\n"); return action_bounce("Il messaggio e' stato rifiutato perche' contiene un allegato di tipo non-message/rfc822, chiamato $fname"); } return action_accept(); } } #*********************************************************************** # %PROCEDURE: defang_warning # %ARGUMENTS: # oldfname -- the old file name of an attachment # fname -- the new "defanged" name # %RETURNS: # A warning message # %DESCRIPTION: # This function customizes the warning message when an attachment # is defanged. #*********************************************************************** sub defang_warning ($$) { my($oldfname, $fname) = @_; return "An attachment named '$oldfname' was converted to '$fname'.\n" . "To recover the file, right-click on the attachment and Save As\n" . "'$oldfname'\n"; } # If SpamAssassin found SPAM, append report. We do it as a separate # attachment of type text/plain sub filter_end ($) { my($entity) = @_; # No sense doing any extra work return if message_rejected(); # Spam checks if SpamAssassin is installed if (!$LocalRelay) { if ($Features{"SpamAssassin"}) { if (-s "./INPUTMSG" < 100*1024) { # Only scan messages smaller than 100kB. Larger messages # are extremely unlikely to be spam, and SpamAssassin is # dreadfully slow on very large messages. my($hits, $req, $names, $report) = spam_assassin_check(); if ($hits >= $req) { my($score); if ($hits < 20) { $score = "*" x int($hits); } else { $score = "*" x 20; } # We add a header which looks like this: # X-Spam-Score: 6.8 (******) NAME_OF_TEST,NAME_OF_TEST # The number of asterisks in parens is the integer part # of the spam score clamped to a maximum of 40. # MUA filters can easily be written to trigger on a # minimum number of asterisks... if ($hits >= 25) { action_bounce("We don't accept SPAM"); } action_change_header("Subject", "***SPAM*** $Subject ($score)"); action_change_header("X-Spam-Score", "$hits ($score) $names"); # If you find the SA report useful, add it, I guess... action_add_part($entity, "text/plain", "-suggest", "$report\n", "SpamAssassinReport.txt", "inline"); } } } } } # DO NOT delete the next line, or Perl will complain. 1;