1

I have a website with a contact form on PHP and a mail server. Email are sent with the help of PHP mail function like so

$name = cleanInput($_POST["name"]);
$e = $_POST["email"];
$email = filter_var(cleanInput($e), FILTER_SANITIZE_EMAIL);
if (strpos($name, "@") !== false) {
    $msg = 'Invalid name!';
    exit();
}
else if (strcmp($e, $email) !== 0 ||
    filter_var($email, FILTER_VALIDATE_EMAIL) === false ||
    !checkdnsrr(substr(strrchr($email, "@"), 1))) {
    $msg = 'Invalid email address!';
    exit();
}
else
{
    $message = nl2br(cleanInput($_POST['message']));

    $subject = 'Support request';
    $to = 'support@domain.com';
    $sender = 'webmaster@domain.com';
    $headers = array(
        'From' => $name . '<' . $email . '>',
        'MIME-Version' => '1.0',
        'Content-type' => 'text/html; charset=iso-8859-1'
    );
    $result = mail($to, $subject, $message, $headers, '-r' . $sender);
}

function cleanInput($data) {
    $data = trim($data);
    $data = stripslashes($data);
    $data = htmlspecialchars($data);
    return $data;
}

Recently I've been attacked by a spammer who is posting emails with From field value like this

check@mydomain.com, this@mydomain.com, link@mydomain.com, 
"US:http"@mydomain.com://www.somedomain.com/page <somename@mail.com>

So I prohibited the @ character in the name field like so

if (strpos($_POST["name"], "@") !== false)
    exit()

I've tried sending a POST request with a name like name@ from Postman and it was rejected successfully but am still getting the same spam emails.

Any ideas please how the spammer is bypassing the validation check?

Update

Here is an example of a spam email with error_log of the raw input fields https://pastebin.com/29ppLGuX

Update2

Thanks to @mti2935 it was figured out that there is no injection peformed. The spammer is just putting a simple line of text with a link into the name field and the MTA (Postfix) to some reason is appending the domain name to all words in that text. I've tried playing with the data and found out that it is the colon which causes all this trouble.

If $name="one two three" then the header of the received email looks good
From: one two three<name@mail.com>

If $name="one: two three" then
From: one@mydomain.com:two three <name@mail.com>

If $name="one: two: three" then
From: "one:two"@mydomain.com:three <name@mail.com>

I've tried converting the header array into a string prior to feeding it into the mail and got the same results.

yaugenka
  • 113
  • 4
  • Side note: For a contact form, it's safer to not populate `From` with a user-controlled value at all. – Arminius Dec 22 '19 at 23:11
  • Not sure if this is related, but something looks squirelly with the fifth argument to your call to the mail() function (additional_parameters). Are you trying to set the envelope sender here? If so, this should be -f, not -r. Also, you need a space after -f. Finally, $sender is not defined anywhere in your script. See https://stackoverflow.com/questions/17890134/php-mail-how-to-set-sender-mail for an example of how to set both the envelope sender and the sender in the message headers when calling the php mail() function. – mti2935 Dec 23 '19 at 01:22
  • @mti2935, yes i'm setting the envelope sender. `$sender` value is hardcoded. There is no difference between the `-r` and `-f` flags. see this docs http://www.postfix.org/mailq.1.html and the space after the flag does not seem to matter because the emails are delivered as expected. – yaugenka Dec 23 '19 at 10:09
  • In that case, it might be helpful if we could see the full source (including the headers) of one of the spam messages. Can you please post the full source of one of the messages on pastebin, and then post a link to that here? – mti2935 Dec 23 '19 at 11:30
  • Here it goes https://pastebin.com/qbUEsPzb. I replaced my actual domain name by `domain` – yaugenka Dec 23 '19 at 21:12
  • Have just added another example with error_log of unsanitized values to the question update. – yaugenka Dec 24 '19 at 09:43

3 Answers3

1

The pastebin posts help a lot. First of all, we see from the unsanitized user inputs that you posted, that the spammer actually isn't entering inputs containing the @ character, after all, in 'name' field of your form. So that explains how it's getting past that check in your script.

It seems that what's happening is that your MTA is appending @yourdomain.com to these inputs, which is what MTA's default to doing when they encounter an email address without a domain.

So, that brings us to the question of why your MTA is treating these values, in the From: header, where you would normally have the sender's name, as email addresses instead of a name. I think it's related to the way that you are constructing the $headers array that you are passing as the fourth input to the mail() function. I think you need "\r\n" after each line in the headers, and the array needs to be converted to a string. There may be other things to look out for as well. See https://stackoverflow.com/questions/27478307/having-issue-on-setting-array-of-headers-in-php-mail-function for more info on how to get this right.

Update: It turns out that in addition to the issues described above, problems were being caused if the user included a colon in the sender's name field when filling out the form, as this character later manifested itself in the From: header of the message. See comments below by yaugenka for more info and for the solution.

mti2935
  • 19,868
  • 2
  • 45
  • 64
  • You are partially right. According to the docs `mail` can accept headers as array https://www.php.net/manual/en/function.mail. Please take a look at the Update2 in the question. – yaugenka Dec 24 '19 at 15:07
  • I think you're on the right track with your last attempt in Update2, where you created the headers as a string instead of using an array. But, you need to use double quotes to enclose `\r\n`, not single quotes, so that the backslash is interpreted as an escape character, instead of taken literally. See https://stackoverflow.com/questions/3446216/what-is-the-difference-between-single-quoted-and-double-quoted-strings-in-php. Also, you need a `\r\n` after the last line of the headers. – mti2935 Dec 24 '19 at 15:48
  • Yes, I have figured that out already and removed it from the question to illiminate confusion. Thanks anyway. Both array and string versions perform identically. But the `colon` issue remains. – yaugenka Dec 24 '19 at 15:51
  • Looks like progress. I think the colon is causing problems, because the colon is used as the delimiter in the headers to separate the field name and field body. See rfc2822, section 2.2. As a test, I inserted a colon in my name in my mail client settings (Thunderbird) to see what it would do. Looking at the headers of the message that it sent, I see that it enclosed my name in the double quotes. So, the `From` header was like so: `From: "my:name" `. I think if you modify your code so that it builds the `From:` header the same way, it should solve the problem. – mti2935 Dec 24 '19 at 18:35
  • So the solution is either to prohibit `colon` at all or quote the value like so `if (strpos($name, ":") !== false) $name = '"'.$name.'"';` with htmlspecialchars() applied beforehand. – yaugenka Dec 24 '19 at 20:01
  • If you wish, you can update your answer respectively to be accepted. – yaugenka Dec 24 '19 at 20:11
  • Done. It's been a fun exercise. – mti2935 Dec 24 '19 at 21:32
0

You're combining both $name and $email in the from header, but according to your post you only filter $name. The attacker (automated spam bot) is probably injecting into the $email variable.

wireghoul
  • 5,745
  • 2
  • 17
  • 26
  • I do filter the $email field as well. I did not show that code just for brevity. The `From` field of all those spam emails always ends with `<$email>` with a well formed email format which means it is clean. – yaugenka Dec 23 '19 at 10:17
  • 1
    How do you expect informed answers if you don't post all the details? – wireghoul Dec 23 '19 at 10:59
  • Sorry, I assumed that the provided example of the spam `From` field shows clearly that the $email part is well-formed. – yaugenka Dec 23 '19 at 11:12
0

Out of the box, PHP provides direct access to various interfaces without a framework of checks. Sometimes its good to restrict people to operating within a strictly controlled sandbox with strict rules about what goes in and comes out. PHP is not such an environment.

Based on the code you have shown us, it would be trivial to exploit this. If you allow any user contributed text within email headers then an attacker can control the recipient, and several of the sender addresses. Restricting field length to 70 characters and disallowing '<', '>', ',', \r and \n from any field which might end up in an SMTP header would help, but still fall a long way short of a robust solution.

That you don't know how they did this also highlights that your code has insufficient logging.

symcbean
  • 18,278
  • 39
  • 73