Contact form with spam-prevention

Posted on

Problem

Idea

After a handfull of small javascript-projects, I also wanted to work a bit with php now. I decided to create a simple contact-form. Of course, I wanted it to be as spam-save as possible, so I used some of the ideas I found online (honeypott and some sort of CAPTCHA).

I realized the CAPTCHA-idea in the following way: Every time the user loads the contact-page a simple math-problem is created randomly and the user has to enter the correct solution to submit the form.

The Code

Minimal working example

<!DOCTYPE html>
<html lang='en'>

    <!-- Head -->
    <head>
        <meta charset='utf-8'>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Contact</title>
        <link rel='stylesheet' type='text/css' href='style.css'>
    </head>

    <body>
        <main>
            <div style='margin:30px; margin-top: 50px'>
                <h2>Contact</h2>
                <div>
                    <form method="POST" action="send.php" class='input'>
                        <label>Your Name:</label><br>
                        <input type="text" name="myName" placeholder="Name" required/><br><br>

                        <label>Your Email:</label><br>
                        <input type="text" name="myEmail" placeholder="E-Mail" required/><br><br>
                        
                        <!-- Honeypott -->
                        <input type="text" id="website" name="website"/>

                        <label>Message:</label><br>
                        <textarea rows="8" name="myMessage" style='width: 100%; resize: none; border: 1px solid Gray; border-radius: 4px; box-sizing: border-box; padding: 10px 10px;' placeholder="Message" required></textarea><br><br>

                        <!-- For sending the simple math-exercise to the server -->
                        <input id='read' name='read' value='read' style='display: none;'/>

                        <label id='exercise'></label><br>
                        <input type='number' id='solution' name='solution' placeholder="Solution" required/>

                        <div style='display: inline-block; text-align: left;'>
                            <input type="checkbox" id="consent" name="consent" value="consent" required="">
                            <label>I agree with saving and sending this message according to the privacy policy.
                            </label>
                        </div>
                        <input style='' type="submit" value="Send"/>    
                    </form>
                </div>                 
            </div> <!-- End main-div -->    
            <script>
                //Two random numbers for simple math-problem (spam-prevention)
                let array = ["one", "two", "three",
                            "four", "five", "six",
                            "seven", "eight", "nine",
                            "ten"];
                let item1 = array[Math.floor(Math.random() * array.length)];
                let item2 = array[Math.floor(Math.random() * array.length)];
                document.getElementById('exercise').innerHTML = item1 + " + " + item2 + " = ?";
                document.getElementById("read").value = item1 + " + " + item2;
            </script>
        </main>
    </body>
</html>
@media (max-width: 1000px) {
    .input {
        width: 100%;
        padding: 10px 20px;
        margin: 10px 0;
        display: inline-block;
        border: 1px solid black;
        border-radius: 5px;
        box-sizing: border-box;
        background: LightGray;
    }
}

@media (min-width: 1001px) {
    .input {
        width: 30%;
        padding: 10px 20px;
        margin: 10px 0;
        display: inline-block;
        border: 1px solid black;
        border-radius: 5px;
        box-sizing: border-box;
        background: LightGray;
    }
}

input[type=text] {
  width: 100%;
  padding: 10px 10px;
  margin: 10px 0;
  display: inline;
  border: 1px solid Gray;
  border-radius: 5px;
  box-sizing: border-box;
}

input[type=submit] {
  width: 100%;
  padding: 10px 10px;
  margin: 10px 0;
  display: inline;
  border: 1px solid Gray;
  border-radius: 5px;
  box-sizing: border-box;
}

input[type=number] {
  width: 100%;
  padding: 10px 10px;
  margin: 10px 0;
  display: inline;
  border: 1px solid Gray;
  border-radius: 5px;
  box-sizing: border-box;
}

#website {
  display: none;
}

PHP-Code

<?php
    
    //Get simple math-problem (e.g. four + six)
    $str = $_REQUEST['read'];
    $first = strpos($str, " ");

    //Get first number (e.g. four)
    $substr1 = substr($str, 0, $first);

    //Get second number (e.g. six)
    $substr2 = substr($str, $first + 3, strlen($str) - $first - 3);
    $arr = array("zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten");

    /*
     * Convertring strings to numbers, e.g.
     * four -> 4
     * six  -> 6
     */
    $x = 0;
    $y = 0;

    for($i = 0; $i <= 10; $i++) {
        if(strcmp($substr1, $arr[$i]) == 0) {
            $x = $i;
            break;
        }
    }

    for($i = 0; $i <= 10; $i++) {
        if(strcmp($substr2, $arr[$i]) == 0) {
            $y = $i;
            break;
        }
    }

    $z = intval($_POST['solution']);

    //Did user enter right solution?
    if($z == ($x + $y)) {
        
        //Bot filled the honeypott-tree
        if(!empty($_POST['website'])) {
            echo "Something went wrong";
            die();
        }

        $userName = $_POST['myName'];
        $userEmail = $_POST['myEmail'];
        $userMessage = $_POST['myMessage'];

        //Did user enter a valid email-adress?
        if(!filter_var($userEmail, FILTER_VALIDATE_EMAIL)) {
            echo "Something went wrong";
            die();
        }

        //Creating message
        $to = "mail@domain.com";
        $subject = "New Contact-form message";
        $body = "Content:";

        $body .= "nn Name: " . $userName;
        $body .= "nn Email: " . $userEmail;
        $body .= "nn Message: " . $userMessage;

        //Trying to send message
        if(mail($to, $subject, $body)){
            echo "Thank you for your message";
            die();
        } else{
            echo "Something went wrong";
            die();
        }
    }

    echo "Something went wrong";
?>

Question(s)

I am especially interested in suggestions to improve the php-code.

  • Did I follow best-practices?
  • Is anything in the code a really bad idea?

Another thing I am interested in is the safety of this approach. How can it improved further?

Of course all other suggestions are appreciated as well.

Solution

JS and front-end style

  • If you aren’t going to reassign a variable, always use const instead of let.
  • Or, since this sounds like it’s on a public facing website, if you wish to support ancient obsolete browsers that some users unfortunately still use (such as IE11), either write in ES5, or (recommended for anything more than a handful of lines) use Babel to transpile your ES6+ source code to ES5 automatically for production.
  • Better to only use .innerHTML when deliberately inserting or retrieving HTML markup. If you aren’t dealing with HTML markup, using textContent is faster and safer. (setting .innerHTML with untrusted input is a security risk. The input happens to be trusted here, but it’s better to get into the habit of only using .innerHTML when necessary)
  • This is somewhat opinion-based, but I’d prefer to avoid creating elements with IDs, because the ID of the element will implicitly become a global variable, and variable names that happen to be unexpectedly globally defined can be an easy source of bugs, especially in larger projects (unless you’re using a linter or Typescript to protect yourself). I’d use class="foo" instead of id="foo".

Security

The random numbers are being generated on the front-end, and as a result are very simple for any interested abuser with a bit of understanding of JS to bypass. (Admittedly, this is unlikely for a small-time website, but it’s still something that would be good to fix.) All they’d need to do would be to send a request with a pre-set read property of one + one and a solution of 2, along with whatever spam messages they’d want to send, and they could send you 1000 of them.

I would recommend generating the random numbers on the server, and sending them to the client to verify in such a format that the random numbers could not be programatically extracted from payload easily.

If I were creating a home-spun solution rather than something tried-and-trusted like Recaptcha, I would have the client request an image from the server, have the server create the background image with imagecreatefrompng, draw random text onto it with imagettftext, save the text in a session variable, and send the image to the client for verification. It wouldn’t be perfect, but it would dramatically raise the bar for successful abuse.

Leave a Reply

Your email address will not be published. Required fields are marked *