FileUpload

Parsing multipart/form-data and storing what arrives, on the assumption that whoever sent it is not on your side.

Feed it a hostile filename → — that page runs the real sanitiser on whatever you type, and stores nothing.

Two halves

Receiving is a streaming parser. File parts are written to temp files as they arrive and never held whole in memory, so a large upload does not become a large process. Ordinary form fields are captured up to a cap. It guards against malformed input, oversize parts, too many parts or files, RFC 5987 extended filenames, obsolete header folding, and base64 / quoted-printable transfer encodings.

Storing is validate-then-persist. Size, extension and MIME policy are all checked before anything lands, and the write itself is atomic.

The edge cases

../../etc/passwd becomes passwd.

filesystem-hostile characters removed, leading dots and trailing dots/spaces trimmed, Windows reserved names (CON, NUL, COM1…) neutralised, and the length capped at 255 bytes while preserving the extension.

dotted component matching the dangerous set fails, not just the last one.

type drives the allow/deny decision rather than the client's claim. Set mime_must_match to require the two agree.

overwrite.

destruction if the file was never saved.

Usage

use FileUpload;

my $up = FileUpload->new(
    dir        => '/var/www/uploads',
    max_size   => 8 * 1024 * 1024,
    allow      => [qw(png jpg jpeg gif pdf)],
    allow_mime => [ 'image/*', 'application/pdf' ],
    rename     => 'random',        # sanitize | random | timestamp | sha256 | CODE
);

my $req = $up->parse;              # reads STDIN + $ENV{CONTENT_TYPE}

for my $file ( $req->files ) {
    my $path = eval { $file->save };
    if ($@) { warn 'rejected ' . $file->client_name . ": $@"; next }
    print "stored as $path\n";
}

my $comment = $req->field('comment');

Where it already runs

Loom's media manager is this parser. Loom::Upload is a vendored copy — same code, renamed package, wired to Loom's error handling — so uploading an image in the admin exercises every guard above. Sign in as demo / demodemo and try to smuggle something past it.