phpemailspam-preventionemail-spam

Simple PHP contact form vs spam


I realize this is probably bad practice but... I've used a simple php script I wrote with the help of a tutorial book I read years ago. I've adapted it as much as I'm able for use with multiple sites but it's largely the same across sites. I have tried and tried to eliminate spam type messages, but alas I cannot figure out what else I can do/can be done. I'm sure someone will mention that e.g. Javascript would be better but I don't have the time or drive to learn it at this point, so please stick to the PHP. The specific code follows below, suggestions will be greatly appreciated as to how to future proof this for spam elimination.

The contact page:

<?php 

    session_start();
    $_SESSION['form_time'] = time();

    define ('SITE_KEY', '...');
    define ('SECRET_KEY', '...');

if (array_key_exists('send', $_POST)) {
    
    function getCaptcha($SecretKey) {
        $Response = file_get_contents("https://www.google.com/recaptcha/api/siteverify?secret=".SECRET_KEY."&response={$SecretKey}");
        $Return = json_decode($Response);
        return $Return;
    }
    $Return = getCaptcha($_POST['g-recaptcha-response']);
    //var_dump($Return);
    
    // mail processing script
    $to = 'email1';
    $me = 'email2';
    $subject = 'Feedback From Website';
    
    // list expected fields
    $expected = array('name', 'email', 'question');
    // set required fields
    $required = array('name', 'email', 'question');
    
    // set additional headers
    $headers = 'From: Megan Roth<feedback@meganroth.com>';

    // set the include
    $process = 'includes/process.inc.php';
    if (file_exists($process) && is_readable($process)) {
        include($process);
    }
    else {
        $mailSent = false;
        mail($me, 'Server Problem', "$process cannot be read", $headers);
    }
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta property="og:title" content="Megan Roth, Mezzo-Soprano - Contact">
<meta property="og:type" content="website">
<meta property="og:image" content="http://www.meganroth.com/EditedImages/index.jpg">
<meta property="og:url" content="http://www.meganroth.com/contact.php">
<meta property="og:description" content="Mezzo-soprano Megan Roth enjoys a career as a soloist in opera and oratorio as well as with prestigious chamber ensembles around the country.">
<title>Megan Roth, Mezzo-Soprano - Contact</title>
<meta name="description" content="Mezzo-soprano Megan Roth enjoys a career as a soloist in opera and oratorio as well as with prestigious chamber ensembles around the country.">
<meta name="author" content="Nathan Roth" >
<link href="css/w3_parallax_template.css" type="text/css" rel="stylesheet">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Crimson+Pro">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=EB+Garamond">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Domine">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<link href="css/parallax12.css" type="text/css" rel="stylesheet">
<style>


/************* ABOVE THIS LINE GLOBAL ***************/

form { width: 100%; }
form p { margin: 0px 0px 25px 20px; }
textarea {
    width: 380px;
    height: 150px;
}
@media screen and (max-width: 400px) { textarea {
            width: 240px;
            height: 120px;
} }
.textInput { width: 300px; }
@media screen and (max-width: 400px) { .textInput { width: 125px; } }
.sendButton { border:1px solid #000000!important; color: #000; background-color: #FAC41E; }
.sendButton:hover { border:1px; color:#000000; background-color: #08748F; }
form .website{ display:none; } /* hide because is spam protection */
.conStudio {
    font-family: "EB Garamond", Times, "Times New Roman", serif;
    font-size: 1.4em;
    line-height: 1.68em;
    color: #FAC41E;
    font-weight: bold;
    margin: 0px 0px 0px 0px;
}
</style>
<script>
    <!--
    function MM_validateForm() { //v4.0
      if (document.getElementById){
        var i,p,q,nm,test,num,min,max,errors='',args=MM_validateForm.arguments;
        for (i=0; i<(args.length-2); i+=3) { test=args[i+2]; val=document.getElementById(args[i]);
          if (val) { nm=val.name; if ((val=val.value)!="") {
            if (test.indexOf('isEmail')!=-1) { p=val.indexOf('@');
              if (p<1 || p==(val.length-1)) errors+='- '+nm+' must contain an e-mail address.\n';
            } else if (test!='R') { num = parseFloat(val);
              if (isNaN(val)) errors+='- '+nm+' must contain a number.\n';
              if (test.indexOf('inRange') != -1) { p=test.indexOf(':');
                min=test.substring(8,p); max=test.substring(p+1);
                if (num<min || max<num) errors+='- '+nm+' must contain a number between '+min+' and '+max+'.\n';
        } } } else if (test.charAt(0) == 'R') errors += '- '+nm+' is required.\n'; }
        } if (errors) alert('The following error(s) occurred:\n'+errors);
        document.MM_returnValue = (errors == '');
    } }
    //-->
    </script>
    <script src="https://www.google.com/recaptcha/api.js?render=6LcEl-8UAAAAAMlzOfIDXmnooj34lkDNfKDTxN2m"></script>
    <!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-34193066-1"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', 'UA-34193066-1');
</script>
</head>
<body>

<?php include("includes/new_navigation12.inc.php"); ?>

<!-- Container  -->
<div class="w3-content w3-container w3-padding-64">
  <div class="w3-row">
            <div class="hdquote">
                &quot;…(her) soaring mezzo-soprano is clean and clear and her vocal glissandos precise and near perfect.&quot;<br>
                - <em>Asheville Citizen-Times</em><br>
                <strong>The Barber of Seville</strong>, Brevard Music Center.
                    <div class="decLine"></div>
            </div>
     </div>
            
        <div class="w3-row">
                    
         <div class="w3-col m9 w3-padding-large">  
            <?php 
            if ($_POST && isset($missing) && !empty($missing)) {
            ?>
            <p class="warning">Please complete the missing item(s) indicated.</p>
            <?php
            }
            elseif ($_POST && $linkOne) {
            ?>
            <p class="warning">Sorry, Messages that contain inappropriate data will not be sent.</p>
            <?php
            }
            elseif ($_POST && $linkTwo) {
            ?>
            <p class="warning">Sorry, Messages that contain inappropriate data will not be sent.</p>
            <?php
            }
            elseif ($_POST && $linkThree) {
            ?>
            <p class="warning">Sorry, Messages that contain inappropriate data will not be sent.</p>
            <?php
            }
            elseif ($_POST && !$mailSent) {
            ?>
            <p class="warning">Sorry, there was a problem sending your message. Please try again later.</p>
            <?php
            }
            elseif ($_POST && $Return->success == true && $Return->score > 0.5 && $mailSent) {
            ?>
            <p class="success">Your message has been sent. Thank you for your comments/questions!</p>
            <?php } ?>           
            <form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post" name="contact" id="contact" class="w3-container w3-card-4" onSubmit="MM_validateForm('name','','R','email','','RisEmail','comments','','R');return document.MM_returnValue">
            <p><input name="website" type="text" class="website"></p>
              <p>
                <label for="name">Name: <?php
                if (isset($missing) && in_array('name', $missing)) { ?>
                <span class="warning">Please enter your name</span><?php } ?>
                </label>
                <input name="name" type="text" class="textInput" id="name" 
                <?php if (isset($missing)) {
                    echo 'value="'.htmlentities($_POST['name'], ENT_QUOTES).'"';
                    } ?>
                >
              </p>
              <p>
                <label for="email">Email: <?php
                if (isset($missing) && in_array('email', $missing)) { ?>
                <span class="warning">Please enter your email address</span><?php } ?>
                </label>
                <input name="email" type="text" class="textInput" id="email"
                <?php if (isset($missing)) {
                    echo 'value="'.htmlentities($_POST['email'], ENT_QUOTES).'"';
                    } ?>
                >
              </p>
              <p>
                <label for="question">Comments:<?php
                if (isset($missing) && in_array('question', $missing)) { ?>
                <span class="warning">Please enter your comments</span><?php } ?>
                </label>
                <textarea name="question" id="question" cols="25" rows="5"><?php 
                    if (isset($missing)) {
                        echo htmlentities($_POST['question'], ENT_QUOTES);
                    } ?></textarea>
              </p>
              <p>
                        <input type="hidden" id="g-recaptcha-response" name="g-recaptcha-response">              
              </p>
              <p>
                <input class="sendButton" type="submit" name="send" id="send" value="Click to Submit Comments">
              </p>
            </form>
            <script>
                    grecaptcha.ready(function() {
                        grecaptcha.execute('<?php echo SITE_KEY; ?>', {action: 'homepage'}).then(function(token) {
                           //console.log(token);
                           document.getElementById('g-recaptcha-response').value=token;
                        });
                    });
            </script>
            <p class="welcome">Please take this time to send comments and your email address so we can stay in touch with you!</p><br><br>
    </div>
    <div class="w3-col m3 w3-padding-large">
      <img class="border" src="EditedImages/Contact.jpg" alt="Headshot for Megan Roth's Contact Webpage">
   </div>
   </div>
        
   <div class="w3-row w3-center">
            <span class="conStudio">Interested in private lessons? Please visit my <a href="http://studio.meganroth.com/" onclick="window.open(this.href, '_blank');return false;">studio site!</a></span>
   </div>
  
</div>

<!-- Footer -->
<?php include("includes/new_footer12.inc.php"); ?>

<script>
// Change style of navbar on scroll
window.onscroll = function() {myFunction()};
function myFunction() {
    var navbar = document.getElementById("myNavbar");
    if (document.body.scrollTop > 100 || document.documentElement.scrollTop > 100) {
        navbar.className = "w3-bar" + " w3-card" + " w3-animate-top" + " w3-white";
    } else {
        navbar.className = navbar.className.replace(" w3-card w3-animate-top w3-white", "");
    }
}

// Used to toggle the menu on small screens when clicking on the menu button
function toggleFunction() {
    var x = document.getElementById("navDemo");
    if (x.className.indexOf("w3-show") == -1) {
        x.className += " w3-show";
    } else {
        x.className = x.className.replace(" w3-show", "");
    }
}

// Toggle between showing and hiding the sidebar, and add overlay effect
function w3_open() {
  if (mySidebar.style.display === 'block') {
    mySidebar.style.display = 'none';
    overlayBg.style.display = "none";
  } else {
    mySidebar.style.display = 'block';
    overlayBg.style.display = "block";
  }
}

// Close the sidebar with the close button
function w3_close() {
  mySidebar.style.display = "none";
  overlayBg.style.display = "none";
}

/* When the user clicks on the button, 
toggle between hiding and showing the dropdown content */
function myFunction() {
  document.getElementById("myDropdown").classList.toggle("w3-show");
}

// Close the dropdown if the user clicks outside of it
window.onclick = function(e) {
  if (!e.target.matches('.dropbtn')) {
  var myDropdown = document.getElementById("myDropdown");
    if (myDropdown.classList.contains('w3-show')) {
      myDropdown.classList.remove('w3-show');
    }
  }
}
</script>

</body>
</html>

And now for the actual mail processing script:

<?php

        // 30 second minimum
        session_start();
        $time_limit = 30; 
        $suspect = false;

        if (isset($_SESSION['form_time']) && is_numeric($_SESSION['form_time'])) {
            $seconds_passed = time() - $_SESSION['form_time'];
            if ($seconds_passed < $time_limit) {
               $suspect = true;
            } 
        } else {
            $suspect = true;
        }

        # spam protection
        if (isset($_POST["website"]) && $_POST["website"] == "") {      
        
        if (isset($_SERVER['SCRIPT_NAME']) && strpos($_SERVER['SCRIPT_NAME'], 'inc.php')) exit;
    
        // remove escape characters from POST array
        if (get_magic_quotes_gpc()) {
            function stripslashes_deep($value) {
                $value = is_array($value) ? array_map('stripslashes_deep', $value) : stripslashes($value);
                return $value;
            }
          $_POST = array_map('stripslashes_deep', $_POST);
        }
    
        // create empty array for any missing fields
        $missing = array();
        
        // assume that there is nothing suspect
        $suspect = false;
        // create a pattern to locate suspect phrases
        $pattern = '/Content-Type:|Bcc:|CC:/i';
        // function to check for suspect phrases
        function isSuspect($val, $pattern, &$suspect) {
        // if the variable is an array, loop through each element
        // and pass it recursively back to the same function
        if (is_array($val)) {
            foreach ($val as $item) {
               isSuspect($item, $pattern, $suspect);
               if ($suspect)
                   break;
            }
        }
        else {
          // if one of the suspect phrases is found, set Boolean to true
          if (preg_match($pattern, $val)) {
            $suspect = true;
            }
          }
        }
        
        // check the $_POST array and any subarrays for suspect content
        isSuspect($_POST, $pattern, $suspect);
        
        if ($suspect ) {
            $mailSent = false;
            unset($missing);
        }
        else {
        // process the $_POST variables
            foreach ($_POST as $key => $value) {
                // assign to temporary variable and strip whitespace if not an array
                $temp = is_array($value) ? $value : trim($value);
                // if empty and required, add to $missing array
                if (empty($temp) && in_array($key, $required)) {
                    array_push($missing, $key);
                }
                // otherwise, assign to a variable of the same name as $key
                elseif (in_array($key, $expected)) {
                    ${$key} = $temp;
                }
            }
        }
        
        // validate the email address
        if (!empty($email)) {
            // regex to identify illegal characters in email address
            $checkEmail = '/^[^@]+@[^\s\r\n\'";,@%]+$/';
            // reject the email address if it doesn't match
            if (!preg_match($checkEmail, $email)) {
                $suspect = true;
                $mailSent = false;
                unset($missing);
            }
        }
        
        // validate the comments
        // regex to identify html links
        $linkOne = false;
        $checkCommentsLinks = '/\b(?:(?:https?|ftp):\/\/|www\.)[-a-z0-9+&@#\/%?=~_|!:,.;]*[-a-z0-9+&@#\/%=~_|]/i'; // '/(http:\/\/|www)/';
        if(preg_match($checkCommentsLinks, stripcslashes($question))){
            $linkOne = true;
            $suspect = true;            
            $mailSent = false;
            unset($missing);
        }
        
        //validate comments against email addresses
        $linkTwo = false;
        $checkCommentsEmail = '/^([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})$/';
        if(preg_match($checkCommentsEmail, stripcslashes($question))){
            $linkTwo = true;
            $suspect = true;            
            $mailSent = false;
            unset($missing);
        }
        
        //look for links in comments
        $linkThree = false;
        if(preg_match('/http|www/i',$question)) {
            $linkThree = true;
            $suspect = true;            
            $mailSent = false;
            unset($missing);
        }
        
        // go ahead only if not suspect and all required fields OK
        if (!$suspect && empty($missing)) {
            // initialize the $message variable
            $message = '';
            // loop through the $expected array
            foreach($expected as $item) {
                // assign the value of the current item to $val
                if (isset(${$item})) {
                    $val = ${$item};
                }
                // if it has no value, assign 'Not Selected'
                else {
                    $val = 'Not selected';
                }
                // if an array, expand as comma-separated string
                if (is_array($val)) {
                    $val = implode(', ', $val);
                }
                // add label and value to the message body
                $message .= ucfirst($item).": $val\n\n";
                }
        
            // limit line length
            $message = wordwrap($message, 70);
            
            // create Reply-To header
            if (!empty($email)) {
                $headers .= "\r\nReply-To: $email";
            }
        
            // send it
            $mailSent = mail($to, $subject, $message, $headers);
            if ($mailSent) {
                // $missing is no longer needed if the email is sent, so unset it
                unset($missing);
            }
        }
        } 
        else {
            http_response_code(400);
            exit;
        }
?>

Something I'm really banging my head against lately is the latest spam messages actually are sending an email with a subject line (don't know where a subject comes into this) that starts "SPAM". Any help will be more than appreciated!! Thanks for reading!


Solution

  • There is no future proof to prevent spam, you already use three ways to prevent it

    1. Captcha
    2. Matching certain spammy words in the data sent, you could add more spammy words
    3. Honeypot

    You can add

    1. CSRF token
    2. Measure time that takes to send the form, eg. if the form is sent in less that 30 seconds then is suspect
    3. Generate dynamic input names
    4. Validate data in the client-side with javascript

    In your isSuspect function you should break when $suspect is true, so you don't have to check all others values

    //...
    foreach ($val as $item) {
       isSuspect($item, $pattern, $suspect);
       if ($suspect)
           break;
    }
    //...
    

    Example of measuring time

    When generating the form save the time in a session:

    session_start();
    $_SESSION['form_time'] = time();
    

    When submitting the form check for the time:

    session_start();
    $time_limit = 30; 
    $suspect = false;
    
    if (isset($_SESSION['form_time']) && is_numeric($_SESSION['form_time'])) {
        $seconds_passed = time() - $_SESSION['form_time'];
        if ($seconds_passed < $time_limit) {
           $suspect = true;
        } 
    } else {
        $suspect = true;
    }
    

    NOTE: if the user opens multiple forms the values will be overwritten, the last value will be used and the previous oppened forms will be invalidated

    This is just basic way, there are many ways to implement this, the 30 seconds is just an arbitrary value that I chose