[Mimedefang] survey: dropping password protected file

Clayton, Nik [IT] nik.clayton at citigroup.com
Fri Mar 5 09:42:27 EST 2004


In re using Archive::Zip for zip file scanning.

> Just started doing this here (well, I'll be testing it next 
> week).  I might be able to send you proof of concept code if 
> I can get the necessary sign off.  

I got the necessary sign off today, so here are the changes I've
got planned.  Comments welcomed.

--- snippets of mimedefang-filter

# Near the top of the filter
use File::Path;
use File::Temp qw(tempdir);
use Archive::Zip qw(:ERROR_CODES);

# Archive::Zip defaults to being verbose about error reporting, which
# we don't need
Archive::Zip::setErrorHandler( sub { return; } );

[...]

# Pull the list of nasty filenames and extensions out in to global
# variables, because now we need to access them from multiple subs.
#
# While I'm here, use /x to make the regexes more readable.

my $BAD_FILENAMES = qr/(?:              # Non capturing grouping
                          application|  # These are all nasty .zip files
                          document|
                          message|
                          movie|
                          myphoto|
                          photos|
                          pp-app|
                          readnow|
                          screensaver|
                          wendy|
                          your_details)\.zip
                        /x;

my $BAD_EXTS      = qr/(?:              # Non capturing grouping
                         \{[^\}]+\}|    # CLSID attacks
                         ade|adp|
                         bas|bat|
                         chm|cmd|com|
                         cpl|crt|
                         dll|
                         exe|
                         hlp|hta|
                         inf|ini|ins|isp|
                         jse?|
                         lib|lnk|
                         mdb|mde|mht|
                         mid|mp3|msc|
                         msi|msp|mst|
                         nws|
                         ocx|
                         pcd|pif?|
                         reg|rm|
                         scr|sct|shb|shs|
                         swf|sys|
                         url|
                         vb|vbe|vbs|vi|
                         vxd|
                         wma|ws|wsc|wsf|
                         wsh|
                         zi
                        )/x;

my $BAD_RE = qr/(?:$BAD_FILENAMES)|     # Any of the bad filenames, or
                (?:\.$BAD_EXTS)         # any of the bad extensions
                \.*                     # followed by zero or more dots
                (?:                     # and either
                  [^-A-Za-z0-9_.,]|     # a non-alphanumeric, or
                  $                     # the end of the string
                )
                /x;

# filter_bad_filename() seems like a reasonable place to hook in the .zip
# file scanning.
#
# In my ideal world, filter_bad_filename (and filter_zip) would call 
# action_bounce() (or the other action_* routines) themselves, instead of
# letting their call do it.
sub filter_bad_filename ($) {
    my($entity) = @_;

    # If it matches a banned extension, fail
    return 1 if re_match($entity, $BAD_RE);

    # If it looks like it contains a .zip file, check inside it for
    # banned extensions and other nastiness
    if(re_match($entity, qe/\.zip/i)) {
        return 1 if filter_zip($entity);
    }

    # Play it safe -- continue blocking .zip files for the moment
    return 1 if re_match($entitity, qr/\.zip\.*(?:[^-A-Za-z0-9_.,]|$)/);

    # All looks good, return 0
    return 0;
}

# Filter zip files.  Look through them, seeing if any of the files they
# contain (or any files in any zip files they contain) should be blocked.
#
# Returns true if we should block the file

my $ZIP_SCRATCH = 10 * 1024 * 1024;     # 10MB scratch space when recursing
my $MEMBER_LIMIT = 25 * 1024 * 1024;    # Compressed files should be no
bigger
                                        # than this when uncompressed
sub filter_zip {
    my $entity = shift;
    my $csize  = shift || 0;            # Cumulative size seen so far
    my $chain  = shift || 'MSG';        # Chain of filenames for logging
    my $r      = undef;                 # Return value from recursion

    my $zip    = Archive::Zip->new();

    if($zip->readFromFileHandle($entity->open('r')) == AZ_OK) {
        # Look for EOCD offset problems, generally indicates a virus
        if($zip->eocdOffset()) {
            md_graphdefang_log("filter_zip: '$chain' non-zero EOCD offset");
            return 1;
        }

        my @members = $zip->members();

        foreach my $member (@members) {
            my $file = $member->fileName();
            my $size = $member->uncompressedSize();

            if($member->isEncrypted()) {
                md_graphdefang_log("filter_zip: '$chain:$file' encrypted zip
contents not allowed");
                return 1;
            }

            if(lc($file) =~ /\.zip/) {
                if($csize + $size >= $ZIP_SCRATCH) {
                    # Recursing in to this .zip file would exceed our
scratch
                    # space limit
                    md_graphdefang_log("filter_zip: '$chain:$file' space
limit exceeded");
                    return 1;
                }

                # Create a temp directory, extract the .zip file in to it,
                # recurse, and clean up.
                my $tmpdir = tempdir(CLEANUP => 1);
                if($zip->extractMember($member, "$tmpdir/tmp.zip") == AZ_OK)
{
                    $r = filter_zip("$tmpdir/tmp.zip", $csize + $size,
                                    "$chain:$file");
                } else {
                    md_graphdefang_log("filter_zip: '$chain:$file'
decompression failed");
                    $r = 1;
                }
                rmtree($tmpdir, 0, 0);
            } else {
                # Fail if the file is over the per-member limit
                if($size >= $MEMBER_LIMIT) {
                    md_graphdefang_log("filter_zip: '$chain:$file'
per-member limit exceeded, file is $size");
                    return 1;
                }

                # Fail if the file fails the regex checks
                if($file =~ /$BAD_RE/i) {
                    md_graphdefang_log("filter_zip: '$chain:$file' fails
regex checks");
                    return 1;
                }
            }

            return $r if $r;
        }
    }  else {
        # Couldn't open the .zip file, fail
        md_graphdefang_log("filter_zip: '$chain' readFromFileHandle()
failed");
        return 1;
    }

    return 0;
}



More information about the MIMEDefang mailing list