Hosting Autopsy / The contact form that blacklisted the server

The contact form that blacklisted the server

HOSTING AUTOPSY

5 min read · 1,185 words

This is a composite case written by the editors. It is built from patterns that come up often in support work and is not the account of a particular named person or company.

A neighbourhood association had a contact form on its website. It sent each message to the secretary using PHP's built-in mail() function, and it had no protection of any kind: no CAPTCHA, no rate limit, no check on what went into the fields. For three years that did not matter. Then a bot found it.

The bot began submitting thousands of messages, with the spammer's content inserted into the fields so that the server dutifully forwarded it to addresses chosen by the attacker. Within two days, the shared server's IP address was listed on several public blocklists, and the association's real emails, including replies to members and invoices from the treasurer, began to bounce or land in spam.

How a contact form becomes a spam gun

A form like this builds an email from the visitor's input. The simplest version takes a name, an email address and a message, and passes them to mail(), with the visitor's address placed in a From: or Reply-To: header so that the secretary can reply.

mail($to, $subject, $message, "From: " . $_POST['email']);

The flaw is that headers in an email are separated by line breaks. If the visitor's "email address" field contains a line break followed by more headers, the script treats them as real headers. An attacker who submits something like this can add recipients:

[email protected]%0ABcc: [email protected]%0A%0ABuy cheap pills...

That is header injection. The server obligingly sends the attacker's message to the attacker's list, with the association's server as the apparent source. Even without injection, a bot that fills the form fields with spam and submits them thousands of times does damage, because each submission is a real email delivered to the secretary and the sender reputation of the server takes the beating from the volume.

Botposts the form contact.phpno checks mail()local sendmail Mail queuethousands Strangers' inboxesserver IP gets the blame The visitor's input decides who receives the mail.Nothing in the path asks who is allowed to send.
The form accepts anything, the script trusts it, and the mail system sends whatever it is handed.

The first two days

On Saturday morning the treasurer sent an invoice to a supplier. Nothing seemed wrong. By Saturday evening the secretary noticed an unusually large number of form messages, hundreds of them, all nonsense, and assumed it was a one-off. On Sunday, three members said they had received no replies to their questions, and one forwarded a bounce message to the secretary.

The bounce was the clue. It said, in the long-winded way of these messages, that the sending IP address was listed on a blocklist and that delivery was refused. The same message arrived again, from another provider, on Monday morning.

The association's site sat on a shared server, so the blocklist entry was for an address that many other customers used too. That is part of what made the host's abuse team act quickly: they had a server-wide reputation to protect, and they suspended outgoing mail for this one account on the Monday afternoon. From the association's point of view, the damage now included legitimate messages as well.

Wrong turns

The first assumption was that the association's mailbox had been hacked, so the secretary changed her password, which made no difference. The second was that the email address on the domain was being impersonated, which does happen, and someone looked at SPF records. They were fine. The messages in question were really sent from the server, and the SPF check passed, which is the unfortunate point: authentication says who sent a message, not whether it was wanted.

The turning point was the host's abuse team forwarding a sample of the outgoing mail and the name of the script that had made the calls. The server's mail log tied each message to contact.php and to the account, and the PHP mail logging option showed the originating script and its folder.

exim -bpc          # count messages in the queue
exim -bp | head    # list a few
grep "cwd=" /var/log/exim_mainlog | grep public_html | head

The commands differ between mail systems and on shared hosting you may not have access to them at all, in which case the host's support team runs them. The principle is the same: find the script that is injecting the mail.

Cleaning up

The work was done in this order, and the order matters.

  1. Disable the form. Without this, anything else is undone by the next bot request.
  2. Fix the form (validation, a CAPTCHA or honeypot, a rate limit), or replace it with a maintained form plugin.
  3. Ask the host to clear the mail queue, so that the thousands of waiting messages are not delivered after the fix.
  4. Check the site for other changes. A form that is this open often means the rest of the site is not carefully maintained either.
  5. Request delisting from each blocklist that named the address. Each has its own process, and most will relist the address if the problem returns.
  6. Wait for reputation to recover, and tell members what happened.

Delisting took a day for some lists and several for others. Large mailbox providers keep their own internal reputation, and they recover more slowly still. The whole recovery took the best part of a week before mail flowed normally.

Sending through authenticated SMTP

The better design is to stop using the server's local mail function and send through an authenticated SMTP account instead. The script logs in with a mailbox's credentials, and the mailbox has limits: a maximum number of messages per hour, restrictions on which From address is allowed, and a log that can be examined. A bot that finds the form can then only do as much damage as that mailbox's limits allow, and the shared server's address is not involved.

Way of sendingWho is accountableWhat limits abuse
PHP mail() to local sendmailThe server's IP addressOnly the host's overall limits
Authenticated SMTP, own mailboxThe mailboxHourly cap, one From address
Transactional email serviceThe service accountIts own sending limits and monitoring

Pair that with authentication records for the domain. The SPF builder and the DMARC builder help set them up, and reports from DMARC will show unexpected senders using your domain.

Honeypotor CAPTCHA Rate limitper visitor Validate fieldsno line breaks AuthenticatedSMTP with a cap Each layer stops a different part of the problem; none needs to be perfect.
Several small barriers in sequence do more than any single clever one.

Talking to the people who were affected

The association wrote to members on the Wednesday, in two short paragraphs: what had happened, that nothing in the membership list had been taken, and that anything sent in the previous week might have gone astray, so a repeat was welcome. Several members replied that they had assumed the committee was ignoring them. The apology cost nothing and settled most of the irritation.

What would have caught it

PreviousThe backup that could not be restoredNextThe redirect loop nobody could see

More from Hosting Autopsy

Autopsy

The firewall rule that blocked the payment provider

After a burst of suspicious traffic, a developer added a rule that blocked all requests from addresses...

Autopsy

The CDN that served one customer's basket to another

A boutique put its site behind a CDN and turned on the option to cache everything, including HTML. Speed...

Autopsy

The launch-day database that ran out of connections

A local theatre put tickets for its autumn season on sale at noon and announced it on social media a day...