Matt Wright's FormMail.pl was the most downloaded CGI script in web history — over 2 million downloads since 1997. It parses any HTML form and sends the results to specified email addresses via sendmail.
FormMail v1.0–1.6 has known vulnerabilities: CVE-2001-0357 (email header injection) and CVE-2002-0564 (open relay). SecurityFocus listed it as the #3 attack vector in Q1 2002. All versions before 1.93 (2009) are exploitable.
Use NMS FormMail or Tectite FormMail instead for production.
FormMail is a Perl CGI script that takes the input of any HTML form and sends it as an email to specified recipients. Configuration is done entirely through hidden form fields — no programming required to add forms to a site.
Written by Matt Wright in 1995, FormMail became the standard way to handle contact forms before server-side languages like PHP had built-in mail functions. By 1997 it had been downloaded over 2 million times from Matt's Script Archive.
| File | Description |
|---|---|
FormMail.pl |
The Perl script that processes form data and sends emails via sendmail |
README |
Installation instructions and configuration guide |
FormMail's popularity made it a primary target for spammers. The script lacked input sanitization, allowing attackers to inject arbitrary email headers and use servers as open relays. The transition away from FormMail followed a clear path: CVE disclosures (2001–2002) led to secure Perl rewrites (NMS, 2002), then PHP alternatives (Tectite, PHPMailer), and eventually hosted form services that require no server at all.
Drop-in replacement by London Perl Mongers (2002). Uses strict mode, taint checking, and CGI.pm. Recommended for existing Perl/CGI setups.
Free PerlActive since 2001. Available in both PHP and Perl. 23-year track record with only 4 minor security advisories.
Free PHP/PerlForm backend for static sites. Point your form action to their endpoint. Free tier: 50 submissions/month.
Free tier From $8/moBuilt into Netlify hosting. Add a netlify attribute to any HTML form. 100 free submissions/month.
| Feature | Original FormMail | NMS / Tectite | Third-Party Services |
|---|---|---|---|
| Header injection protection | No | Yes | Yes |
| CSRF protection | No | Optional | Yes |
| Rate limiting | No | Optional | Yes |
| Spam filtering | No | Basic | Built-in |
| CAPTCHA support | No | Yes | Yes |
| Requires server | Yes | Yes | No |
| Field Name | Required | Description |
|---|---|---|
recipient |
Yes | Email address(es) to receive form data |
subject |
No | Subject line for the email |
redirect |
No | URL to redirect to after submission |
required |
No | Comma-separated list of required fields |
env_report |
No | Environment variables to include in email |
sort |
No | Field order in email: order:field1,field2 or alphabetic |
print_config |
No | Configuration fields to include in email body |
print_blank_fields |
No | Include empty fields in output |
title |
No | Title for the built-in success page |
return_link_url |
No | URL for "return" link on the success page |
return_link_title |
No | Text for the return link |
Download the FormMail package and extract it to your server.
Edit FormMail.pl and set these variables:
# Restrict to your domain (prevents cross-site use)
@referers = ('yourdomain.com', 'www.yourdomain.com');
# Whitelist allowed recipients (prevents open relay abuse)
@recipients = ('[email protected]', '[email protected]');
# Path to sendmail binary
$mailprog = '/usr/sbin/sendmail';
# Upload to your cgi-bin directory, then:
chmod 755 FormMail.pl
<form action="/cgi-bin/FormMail.pl" method="POST">
<input type="hidden" name="recipient" value="[email protected]">
<input type="hidden" name="subject" value="Contact Form">
<!-- Your visible form fields here -->
</form>
| Variable | Description | Example |
|---|---|---|
@referers |
Domains allowed to submit to this script. Prevents cross-site abuse. | ('yourdomain.com') |
@recipients |
Whitelisted email recipients. Without this, the script is an open relay. | ('[email protected]') |
$mailprog |
Path to sendmail or compatible MTA. | '/usr/sbin/sendmail' |
Modern secure implementations of the same form-to-email functionality:
#!/usr/bin/perl
use strict;
use warnings;
use CGI;
use Email::Valid;
use HTML::Entities;
my $cgi = CGI->new;
# Configuration - whitelist recipients and referers
my @allowed_recipients = ('[email protected]');
my @allowed_referers = ('yourdomain.com');
# Validate referer
my $referer = $ENV{'HTTP_REFERER'} || '';
my $valid_referer = 0;
for my $allowed (@allowed_referers) {
$valid_referer = 1 if $referer =~ /$allowed/i;
}
die "Invalid referer" unless $valid_referer;
# Get and validate form data
my $recipient = $cgi->param('recipient') || '';
my $subject = $cgi->param('subject') || 'Form Submission';
my $redirect = $cgi->param('redirect') || '';
my $valid_recipient = 0;
for my $allowed (@allowed_recipients) {
$valid_recipient = 1 if lc($recipient) eq lc($allowed);
}
die "Invalid recipient" unless $valid_recipient;
die "Invalid email" unless Email::Valid->address($recipient);
# Build email body
my $body = "Form submission received:\n\n";
for my $param ($cgi->param) {
next if $param =~ /^(recipient|subject|redirect)$/;
my $value = encode_entities($cgi->param($param));
$body .= "$param: $value\n";
}
$body .= "\n--- Environment ---\n";
$body .= "Remote IP: $ENV{'REMOTE_ADDR'}\n";
$body .= "User Agent: $ENV{'HTTP_USER_AGENT'}\n";
# Send via sendmail
open(my $mail, '|-', '/usr/sbin/sendmail', '-t') or die "Cannot send: $!";
print $mail "To: $recipient\n";
print $mail "Subject: $subject\n";
print $mail "Content-Type: text/plain; charset=UTF-8\n\n";
print $mail $body;
close($mail);
if ($redirect) {
print $cgi->redirect($redirect);
} else {
print $cgi->header();
print "<h1>Thank You</h1><p>Your message has been sent.</p>";
}
<?php
// PHP Form Handler with security measures
$allowed_recipients = ['[email protected]', '[email protected]'];
$allowed_domains = ['yourdomain.com', 'www.yourdomain.com'];
// CSRF check
session_start();
if ($_POST['csrf_token'] !== $_SESSION['csrf_token']) {
die('Invalid CSRF token');
}
// Validate referer
$referer = parse_url($_SERVER['HTTP_REFERER'] ?? '', PHP_URL_HOST);
if (!in_array($referer, $allowed_domains)) {
die('Invalid referer');
}
// Sanitize inputs
$recipient = filter_var($_POST['recipient'] ?? '', FILTER_SANITIZE_EMAIL);
$subject = htmlspecialchars($_POST['subject'] ?? 'Form Submission', ENT_QUOTES, 'UTF-8');
$redirect = filter_var($_POST['redirect'] ?? '', FILTER_SANITIZE_URL);
// Validate recipient against whitelist
if (!in_array(strtolower($recipient), array_map('strtolower', $allowed_recipients))) {
die('Invalid recipient');
}
if (!filter_var($recipient, FILTER_VALIDATE_EMAIL)) {
die('Invalid email format');
}
// Build message body
$message = "Form submission received:\n\n";
$excluded = ['recipient', 'subject', 'redirect', 'csrf_token'];
foreach ($_POST as $key => $value) {
if (in_array($key, $excluded)) continue;
$clean_key = htmlspecialchars($key, ENT_QUOTES, 'UTF-8');
$clean_value = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
$message .= "$clean_key: $clean_value\n";
}
$message .= "\n--- Environment ---\n";
$message .= "IP: " . $_SERVER['REMOTE_ADDR'] . "\n";
$message .= "User Agent: " . $_SERVER['HTTP_USER_AGENT'] . "\n";
// Send
$headers = [
'From: [email protected]',
'Reply-To: ' . ($_POST['email'] ?? '[email protected]'),
'Content-Type: text/plain; charset=UTF-8'
];
if (mail($recipient, $subject, $message, implode("\r\n", $headers))) {
if ($redirect) {
header("Location: $redirect");
exit;
}
echo "<h1>Thank You</h1><p>Your message has been sent.</p>";
} else {
echo "<h1>Error</h1><p>Failed to send message.</p>";
}
// Node.js/Express form handler
const express = require('express');
const nodemailer = require('nodemailer');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const app = express();
app.use(helmet());
app.use(express.urlencoded({ extended: true }));
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10
});
const ALLOWED_RECIPIENTS = ['[email protected]'];
const ALLOWED_DOMAINS = ['yourdomain.com'];
const transporter = nodemailer.createTransport({
host: 'smtp.yourdomain.com',
port: 587,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
}
});
app.post('/contact', limiter, async (req, res) => {
try {
const { recipient, subject, redirect, ...formData } = req.body;
// Validate referer
const referer = new URL(req.headers.referer || '').hostname;
if (!ALLOWED_DOMAINS.includes(referer)) {
return res.status(403).send('Invalid referer');
}
// Validate recipient against whitelist
if (!ALLOWED_RECIPIENTS.includes(recipient?.toLowerCase())) {
return res.status(400).send('Invalid recipient');
}
// Build message
let message = 'Form submission:\n\n';
for (const [key, value] of Object.entries(formData)) {
message += `${key}: ${value}\n`;
}
message += `\nIP: ${req.ip}\nUser Agent: ${req.headers['user-agent']}\n`;
await transporter.sendMail({
from: '[email protected]',
to: recipient,
subject: subject || 'Form Submission',
text: message
});
redirect ? res.redirect(redirect) : res.send('Message sent.');
} catch (error) {
console.error(error);
res.status(500).send('Error sending message');
}
});
app.listen(3000);
<form action="/cgi-bin/FormMail.pl" method="POST">
<input type="hidden" name="recipient" value="[email protected]">
<input type="hidden" name="subject" value="Website Contact Form">
<input type="hidden" name="redirect" value="https://yourdomain.com/thank-you.html">
<input type="hidden" name="required" value="name,email,message">
<input type="hidden" name="sort" value="order:name,email,phone,message">
<div class="mb-3">
<label for="name">Name *</label>
<input type="text" id="name" name="name" required>
</div>
<div class="mb-3">
<label for="email">Email *</label>
<input type="email" id="email" name="email" required>
</div>
<div class="mb-3">
<label for="phone">Phone</label>
<input type="tel" id="phone" name="phone">
</div>
<div class="mb-3">
<label for="message">Message *</label>
<textarea id="message" name="message" rows="5" required></textarea>
</div>
<button type="submit">Send Message</button>
</form>
$mailprog path — run which sendmail on your server to confirm@recipients whitelist — the recipient address must be listed/var/log/httpd/error_log or /var/log/apache2/error.logYes. Separate addresses with commas:
<input type="hidden" name="recipient" value="[email protected],[email protected]">
All addresses must appear in the @recipients whitelist in FormMail.pl.
The redirect field requires a full URL with protocol:
<!-- Correct -->
<input type="hidden" name="redirect" value="https://yourdomain.com/thank-you.html">
<!-- Will not work -->
<input type="hidden" name="redirect" value="thank-you.html">
Also verify that no Perl errors occur before the redirect header is sent (check the CGI error log).
The original FormMail (pre-1.93) is trivially exploitable. Minimum steps:
@recipients — only allow specific email addresses@referers — restrict to your domainThe NMS version is a drop-in replacement — same form fields, same configuration approach, but with taint checking and input sanitization.
Two options:
redirect hidden field to your own thank-you page URLtitle, return_link_url, and return_link_title to customize the default output<input type="hidden" name="title" value="Thank You!">
<input type="hidden" name="return_link_url" value="https://yourdomain.com">
<input type="hidden" name="return_link_title" value="Back to Homepage">
Third-party add-ons and modifications built on top of FormMail:
Adds courtesy reply email, flatfile database logging, and fax-to-email support.
Adds optional courtesy reply and database logging of all submissions.
Automated personalized email responses to form submissions.
Stripped-down FormMail variant for emailing files to users.
FormMail derivative for emailing files to users on request.
Visitor comment and feedback system with public display.
Email subscription management and newsletter distribution.