### Copyright (C) 1997-2006 John D. Hardin ### This program is free software; you can redistribute it and/or modify ### it under the terms of the GNU General Public License as published by ### the Free Software Foundation; either version 2 of the License, or ### (at your option) any later version. ### ### This program is distributed in the hope that it will be useful, ### but WITHOUT ANY WARRANTY; without even the implied warranty of ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ### GNU General Public License for more details. ### ### Contact the copyright holder for commercial licensing terms ### if you wish to incorporate this code or portions of it into ### non-GPL software. ### # # # $Id: html-trap.procmail,v 1.152pre8 2014-07-10 07:29:24-08 jhardin Exp jhardin $ # # Baroquely complex Procmail and Perl recipe to defang active-content HTML tags # to protect those people foolish enough to read their mail from a web browser # or HTML-enabled mail client. Also mangles the attachment name on executable # attachments to prevent attacks, at the cost of not being able to run # programs from within your mail client - which you shouldn't do anyway. # Also protects against excessively long filenames in attachments, which # can cause nasty things to happen in some clients, and excessively long # MIME headers, which may crash or allow exploits of some clients. # # All of the configuration options discussed below must be set in the # procmail script that calls this filter, before this filter is called. # Please see http://www.impsec.org/email-tools/sanitizer-configuration.html # for complete documentation of all configuration options. # # If you wish to override the default list of executable extensions, # set MANGLE_EXTENSIONS to a regexp (see where it is defined below for # the proper syntax). DO NOT start or end it with a vertical bar or have # two adjacent vertical bars! # # If you wish to block specific executable or document filenames from # attachments, define $POISONED_EXECUTABLES to point at a filename # containing one filename per line, with no leading spaces. Trailing spaces # and comments (beginning with "#") are permitted. # Case is ignored. Wildcards are allowed, using a mishmash of filename # globbing and regular expression syntax. # # Site policy for trapped messages can be specified within limited bounds. # To redirect poisoned messages to a file or directory, set # $SECURITY_QUARANTINE to the name of the file or directory. To notify # administrator(s), set $SECURITY_NOTIFY and/or $SECURITY_NOTIFY_VERBOSE to a # comma-delimited address list. The _VERBOSE recipients will get the whole # message. # # If the trapped message cannot be saved in $SECURITY_QUARANTINE for any # reason, the message will be bounced unless $SECURITY_QUARANTINE_OPTIONAL is # set to any value. # # This performs limited scanning of attachments for M$ macros which contain # possibly dangerous code (as opposed to specific strings from specific # variants of specific exploits). To disable this scanning, set # $DISABLE_MACRO_CHECK to anything. To adjust the score an attachment is # considered "poisoned" at, set $POISONED_SCORE. The default is 25, which # should be okay unless you see valid attachments with complex macros. Set # $SCORE_HISTORY to a filename to track the scores on scanned documents. # If you wish to do profiling before implementing poisoning, set # $SCORE_ONLY to anything to scan and possibly record scores but not # poison. This could also be accomplished by setting $POISONED_SCORE to a # very high value (200?), which would trap attacks while still allowing # profiling. # # This performs scanning of .ZIP archive attachments for filenames. .ZIP # attachments will be handled by the POISON and STRIP lists, and # $ZIPPED_EXECUTABLES lists filespecs that will cause the message to be # quarantined if found in the .ZIP attachment # # If you wish to send a message to the author of a trapped message, set # $SECURITY_NOTIFY_SENDER to any value. To replace the canned message that # is sent to the author of a trapped message, set $SECURITY_NOTIFY_SENDER # to point at a text file for the message body. No variable expansion is # performed on the body of this message. If you want to notify the # sender's postmaster as well, set $SECURITY_NOTIFY_SENDER_POSTMASTER # to any value. If you are not running this on a mail relay, you can use # $SECURITY_NOTIFY_RECIPIENT in the same manner as $SECURITY_NOTIFY_SENDER. # # This could also be extended fairly easily to allow virus-checking of # attachments, assuming you have a virus-checker that will run under *nix. # # NOTES # Requires perl. # Attachment scanning requires the "mktemp" and a base-64 decoder program OR # the installation of the CPAN Perl modules MIME::Base64 and File::MkTemp. # Set $USE_CPAN if you want to use CPAN modules. If you can't put those # modules in their system directories (e.g. you're running this on your # ISP's computer), put them somewhere accessible and set $PVT_CPAN to that # directory. # Scanning of ZIP attachments requires the "unzip" program. # # This is a non-delivering filter recipe unless $SECURITY_QUARANTINE is # set. # # INVOCATION # Insert # CONFIG_VARIABLE=some_value # {repeat as needed} # INCLUDERC=html-trap.procmail # into your .procmailrc at the beginning or end. # # For further details, particularly how to set up site-wide and mail hub # or relay filtering, visit: # http://www.impsec.org/email-tools/procmail-security.html # # possible bug workaround? LINEBUF=8192 # Size LINEBUF dynamically to deal with excessively large headers :0 H * 32000^0 * 1^1 . { LINEBUF="$=" } # override csh and cousins :0 * SHELL ?? csh$ { SHELL="/bin/sh" } # Make sure $LOGFILE exists so the shells don't barf LOGFILE=${LOGFILE:-"/dev/null"} #--------------------------------------------------------------------------- # Grab some info for logging # NL=" " SUBJ="" FROM="unknown" FROMDOM="" REPLY_SUPPRESSED="" :0 * ^Subject[ ]*:[ ]+\/[^ ].+ { SUBJ=" in \"$MATCH\"" } OVERRIDEFORMAIL="Xx:" # AOL mail servers look up who is dialled in and # add that user's ID as the X-Apparently-From: header # This is pretty spoof-proof :0 * ^Received: from .*\.aol\.com .* by .*\.aol\.com * ^X-Apparently-From:.*\/[^ ]+@aol\.com { FROM="$MATCH" OVERRIDEFORMAIL="To: $FROM" SUBJ="$SUBJ from $FROM" FROMDOM="aol.com" } # otherwise, try Return-Path: (the envelope sender) # which is still subject to spoofing but may be # overlooked :0 E * ^Return-Path:.*\/<[^>]+@[^>]+> { FROM="$MATCH" # Did a mailing list rewrite the Return-Path header? :0 * -1^0 * 1^0 ^Precedence: (bulk|junk|list) * 1^0 ^(List-Id|X-Mailing-List): * 9876543210^0 FROM ?? \]+-l-admin@ { :0 * ^From:.*\/<[^>]+> { FROM="$MATCH" } } SUBJ="$SUBJ from $FROM" :0 * ^Return-Path:.*<[^@]+@\/[^>]+ { FROMDOM="$MATCH" } } # The following runs if Return-Path: header not found # MAKE SURE your MTA puts in a Return-Path: header! :0 E * ^From:.*\/<[^>]+> { FROM="$MATCH" SUBJ="$SUBJ from $FROM" LOG=" WARN: No usable Return-Path: header found in message.${NL}" :0 * SECURITY_NOTIFY_SENDER ?? [^ ] * ! SECURITY_DISABLE_SMART_REPLY ?? [^ ] { REPLY_SUPPRESSED="NOTICE: No Return-Path: header. Suppressing sender notification.${NL}" LOG=" $REPLY_SUPPRESSED" SECURITY_NOTIFY_SENDER= } } TO=<$LOGNAME> :0 * LOGNAME ?? ^root$ { # If $LOGNAME is root, we're probably running as a gateway filter: # get the "real" to name(s) out of the message headers. :0 * ^To: +\/.* { TO="$MATCH" } } # try to get the full recipient address from a Received: header # this overrides LOGNAME and the To: header :0 * ^Received: .*for \/ ]+@[^> ]+\.[^> ;]+>? { TO="$MATCH" } :0 * ^Received: .*for \/<[^> ]+@[^> ]+\.[a-z][a-z][a-z]?[a-z]?[a-z]?[a-z]?> { TO="$MATCH" } SUBJ="$SUBJ to $TO" :0 * ^Message-ID:.*\/<[^>]+> { MSGID="$MATCH" SUBJ="$SUBJ msgid=$MSGID" } SUBJ="$SUBJ " #--------------------------------------------------------------------------- # Override these in /etc/procmailrc as needed # STRIPPED_WARNING=${STRIPPED_WARNING:-" SECURITY NOTICE: The mail system has removed a file attachment or other body part from this message. The attachment or body part has been discarded. Please contact your system administrator for details. "} POISONED_WARNING=${POISONED_WARNING:-" SECURITY WARNING! The mail system has detected that the following attachment may contain hazardous program code, is a suspicious file type, or has a suspicious file name. Do not trust it. Contact your system administrator immediately. "} MACRO_WARNING=${MACRO_WARNING:-" SECURITY WARNING! The mail delivery system has detected that the preceding document attachment appears to contain hazardous macro code. Macro Scanner score: "} MACRO_DETAILS=${MACRO_DETAILS:-"Macro Scanner score details: " ZIP_MAGIC_WARNING=${ZIP_MAGIC_WARNING:-" SECURITY WARNING! The mail system has detected that the preceding attachment claims to be a ZIP archive, but it does not have a valid ZIP archive signature. Do not trust it. Contact your system administrator immediately. "} ZIPPED_WARNING=${ZIPPED_WARNING:-" SECURITY WARNING! The mail system has detected that the preceding ZIP archive attachment contains suspicious files. Do not trust it. Contact your system administrator immediately. The suspicious files in the archive are: "} :0 * ! DISABLE_RAR_SCAN ?? [^ ] * ! ? type unrar >/dev/null 2>&1 { # unrar not on $PATH LOG=" unrar not found, disabling RAR scan - either install unrar or define DISABLE_RAR_SCAN${NL}" DISABLE_RAR_SCAN=Y } RAR_MAGIC_WARNING=${RAR_MAGIC_WARNING:-" SECURITY WARNING! The mail system has detected that the preceding attachment claims to be a RAR archive, but it does not have a valid RAR archive signature. Do not trust it. Contact your system administrator immediately. "} RARRED_WARNING=${RARRED_WARNING:-" SECURITY WARNING! The mail system has detected that the preceding RAR archive attachment contains suspicious files. Do not trust it. Contact your system administrator immediately. The suspicious files in the archive are: "} BAD_BASE64_WARNING=${BAD_BASE64_WARNING:-" SECURITY WARNING! The mail system has detected that the preceding attachment was encoded in a manner intended to bypass security filtering. Do not trust it. Contact your system administrator immediately. "} BAD_JPEG_WARNING=${BAD_JPEG_WARNING:-" SECURITY WARNING! The mail system has detected that the preceding attachment appears to be a JPEG image containing a Microsoft buffer overflow attack. Do not trust it. Contact your system administrator immediately. "} WMF_WARNING=${WMF_WARNING:-" SECURITY WARNING! The mail system has detected that the preceding attachment appears to be a WMF image. It has been blocked for security reasons. Contact your system administrator immediately. "} TNEF_WARNING=${TNEF_WARNING:-" SECURITY NOTICE: The mail system has removed a Microsoft attachment for security reasons. The sender should disable sending Rich Text format in Outlook and disable sending TNEF to the Internet from their Microsoft Exchange gateway. See http://support.microsoft.com/support/kb/articles/Q241/5/38.ASP and http://www.microsoft.com/TechNet/exchange/2505ch10.asp for more information. "} # FROM address for notifications SECURITY_LOCAL_POSTMASTER=${SECURITY_LOCAL_POSTMASTER:-"postmaster"} # MTA command line options when generating messages # get recipient(s) from command line MTA_FLAGS_CMDLN=${MTA_FLAGS_CMDLN:-"-U"} # get recipient(s) from message headers MTA_FLAGS_HDRS=${MTA_FLAGS_HDRS:-"-oi -t -f$SECURITY_LOCAL_POSTMASTER"} # How paranoid to be about stuff embedded in documents # default: very SC_MBD=${SECURITY_OFFICE_EMBED_SCORE:-99} #--------------------------------------------------------------------------- # trap some excessively long RFC-822 headers # :0 * ! SECURITY_IGNORE_LONGSUBJECT ?? [^ ] * ^\/Subject: ......................................................................................................................................................................................................................................................................................................................................................................................* { LOG=" Trapped excessively long header$SUBJ" STATUS="STATUS: Header truncated." HDR="$MATCH" # truncate the header :0 fw h * ^\/Subject: ................................................................................................................................................................................................................................................................................................................................... | formail -i "$MATCH" SECURITY_NOTIFY=${SECURITY_NOTIFY:-"postmaster"} :0 * ! SECURITY_NONOTIFY_LONGSUBJECT ?? [^ ] * SECURITY_NOTIFY ?? [^ ] * !$ ^X-Loop: EMAIL SECURITY WARNING $HOST $SECRET { LOG="${NL} NOTIFY $SECURITY_NOTIFY${NL}" :0 h ci | ( \ echo "To: $SECURITY_NOTIFY";\ echo 'From: "Procmail Security daemon"' "<${SECURITY_LOCAL_POSTMASTER}>";\ echo 'Subject: SECURITY WARNING - possible email attack';\ echo "X-Loop: EMAIL SECURITY WARNING $HOST $SECRET"; \ echo ;\ echo 'Trapped excessively long header:' ;\ echo "$HDR";\ echo ;\ echo "$STATUS";\ echo ;\ echo 'Headers from message:';\ echo ;\ sed -e 's/^/> /' ;\ ) | $SENDMAIL $MTA_FLAGS_CMDLN $SECURITY_NOTIFY } } :0 * \/^((resent-)?(sender|from|(reply-)?to|cc|bcc)|(errors|disposition-notification|apparently)-to|Return-Path): .*<>.*<>.*<>.*<>.*<>.*<>.* { HDR=$MATCH LOG=" Trapped multiple null addresses (possible sendmail attack)" STATUS="STATUS: Header cleaned." # truncate the header :0 fw h | sed -e 's/<>.*<>.*<>.*<>.*<>.*<>/<>/g' SECURITY_NOTIFY=${SECURITY_NOTIFY:-"postmaster"} :0 * SECURITY_NOTIFY ?? [^ ] * !$ ^X-Loop: EMAIL SECURITY WARNING $HOST $SECRET { LOG="${NL} NOTIFY $SECURITY_NOTIFY${NL}" :0 h ci | ( \ echo "To: $SECURITY_NOTIFY";\ echo 'From: "Procmail Security daemon"' "<${SECURITY_LOCAL_POSTMASTER}>";\ echo 'Subject: SECURITY WARNING - possible email attack';\ echo "X-Loop: EMAIL SECURITY WARNING $HOST $SECRET"; \ echo ;\ echo 'Trapped multiple null addresses (possible sendmail exploit):' ;\ echo "$HDR";\ echo ;\ echo "$STATUS";\ echo ;\ echo 'Headers from message:';\ echo ;\ sed -e 's/^/> /' ;\ ) | $SENDMAIL $MTA_FLAGS_CMDLN $SECURITY_NOTIFY } } :0 * ^\/(Mime-Version|(Resent-)?(Date|Sender|From|Reply-To)|(errors|disposition-notification|apparently)-to|Message-ID|Return-Path|Status|X-Status|X-Keywords|Content-(Class|Type|Disposition|Transfer-Encoding)): ......................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................* { LOG=" Trapped excessively long header$SUBJ" STATUS="STATUS: Message bounced." HDR="$MATCH" :0 * SECURITY_QUARANTINE ?? [^ ] { STATUS="STATUS: Message quarantined in $SECURITY_QUARANTINE, not delivered to recipient." } SECURITY_NOTIFY=${SECURITY_NOTIFY:-"postmaster"} :0 * SECURITY_NOTIFY ?? [^ ] * !$ ^X-Loop: EMAIL SECURITY WARNING $HOST $SECRET { LOG="${NL} NOTIFY $SECURITY_NOTIFY${NL}" :0 h ci | ( \ echo "To: $SECURITY_NOTIFY";\ echo 'From: "Procmail Security daemon"' "<${SECURITY_LOCAL_POSTMASTER}>";\ echo 'Subject: SECURITY WARNING - possible email attack';\ echo "X-Loop: EMAIL SECURITY WARNING $HOST $SECRET"; \ echo ;\ echo 'Trapped excessively long header:' ;\ echo "$HDR";\ echo ;\ echo "$STATUS";\ echo ;\ echo 'Headers from message:';\ echo ;\ sed -e 's/^/> /' ;\ ) | $SENDMAIL $MTA_FLAGS_CMDLN $SECURITY_NOTIFY } :0 * SECURITY_QUARANTINE ?? [^ ] { :0 :${SECURITY_QUARANTINE_LOCKFILE} $SECURITY_QUARANTINE :0 e { # Argh! Quarantine failed! # notify administrator LOG="${NL} ERR: QUARANTINE FAILED!${NL}" # bounce it. EXITCODE=65 :0 h * SECURITY_NOTIFY ?? [^ ] * !$ ^X-Loop: EMAIL SECURITY WARNING $HOST $SECRET | ( \ echo "To: $SECURITY_NOTIFY";\ echo 'From: "Procmail Security daemon"' "<${SECURITY_LOCAL_POSTMASTER}>";\ echo 'Subject: SECURITY WARNING - quarantine failed!';\ echo "X-Loop: EMAIL SECURITY WARNING $HOST $SECRET"; \ echo ;\ echo 'Attempt to quarantine the following message in $SECURITY_QUARANTINE failed.';\ echo 'Message has been bounced.';\ echo 'Verify file access permissions:';\ ls -l $SECURITY_QUARANTINE 2>&1 ;\ echo ;\ echo 'Headers from message:';\ echo ;\ sed -e 's/^/> /' ;\ ) | $SENDMAIL $MTA_FLAGS_CMDLN $SECURITY_NOTIFY } } # bounce it. EXITCODE=65 # zap it. :0 /dev/null } #--------------------------------------------------------------------------- # Not all MIME can be sanitized... # :0 * ^Content-Type[ ]*:.*multipart/((signed)|(encrypted)); * ! SECURITY_DEFANG_SIGNED ?? [^ ] { LOG=" WARN: Cannot sanitize message due to signing or encryption$SUBJ" } #--------------------------------------------------------------------------- # Defang HTML active-content tags # # NB: In case you think the regexes should be /<[ ]*TAG/, I suggest # you *try* such tags in your browser first... # # Unfortunately the "on*" (e.g. "onload=") syntax is such that we can't # reliably look for /onload="/ - there may be whitespace around the =. # This isn't intended to be a full HTML parser, so we'll err on the side of # safety by defanging everything, even though it may be outside of an HTML # context. # # This keeps getting uglier as more and more holes are discovered. # # This will be folded into a better sanitizer Real Soon Now... # :0 * 9876543210^1 ! ^Content-Type[ ]*:.*multipart/((signed)|(encrypted)); * 9876543210^1 SECURITY_DEFANG_SIGNED ?? [^ ] { #----------- ALL OF THIS IS SKIPPED FOR SIGNED/ENCRYPTED MESSAGES # "perl -e" has problems when run as root... DROPPRIVS=YES :0 * DEBUG_VERBOSE ?? [^ ] { VERBOSE=YES } :0 H * [\015][^\012] { # this apparently happens a lot, so stop logging it #LOG="Defanging bare CR in headers$SUBJ" :0 fw h | perl -p -e 's/\015/\012/g' } :0 B * ! SECURITY_TRUST_HTML ?? [^ ] * 9876543210^1 \<(html|title|body|meta|app|script|object|embed|i?frame|style|img|bgsound|i?layer|link|form|input|table|th|td|xml) * 9876543210^1 =(3d)?[ ]*["'](&{|([a-z\/*]+script|mocha):) { LOG="Defanging active HTML content$SUBJ" HAVE_UUE= :0 B * ^begin[ ]+([0-9]+)?[ ]+[^ ]+ { HAVE_UUE=YES LOG=" UUE content, HTML defang suppression enabled.$NL" } :0 fw b | perl -p -e ' #\ unless ($ENV{"HAVE_UUE"} && /^M.{60}$/ ) { #\ if (/ / && /["\047][^"\047\s]*&#x?[1-9][0-9a-f]/i) { #\ while (/["\047][^"\047\s]*&#((4[6-9]|5[0-8]|6[4-9]|[78][0-9]|9[07-9]|1[0-1][0-9]|12[0-2])(?![0-9]))/) { #\ $char = chr($1); #\ s/&#$1;?/$char/g; #\ } #\ while (/["\047][^"\047\s]*&#(x(2[ef]|3[0-9a]|4[0-9a-f]|5[0-9a]|6[1-9a-f]|7[0-9a]))/i) { #\ $char = chr(hex("0$1")); #\ s/&#$1;?/$char/gi; #\ } #\ } #\ if (/ / && /["\047][^"\047\s]*%[2-7][0-9a-f]/i) { #\ while (/["\047][^"\047\s]*%((2[ef]|3[0-9a]|4[0-9a-f]|5[0-9a]|6[1-9a-f]|7[0-9a]))/i) { #\ $char = chr(hex("0x$1")); #\ s/%$1/$char/gi; #\ } #\ } #\ if (/<|%3c/) { #\ s/(<|%3c)(META|APP|SCRIPT|OBJECT|EMBED|FRAME|IFRAME|LAYER|ILAYER|LINK|FORM|INPUT|XML)/$1DEFANGED_$2/gi; #\ unless ($ENV{"SECURITY_TRUST_STYLE_TAGS"}) { #\ s/