dwyl / learn-to-send-email-via-google-script-html-no-server

:email: An Example of using an HTML form (e.g: "Contact Us" on a website) to send Email without a Backend Server (using a Google Script) perfect for static websites that need to collect data.
GNU General Public License v2.0
3.15k stars 910 forks source link

Implementing g-recaptcha-response validation #417

Open heliusAurelius opened 3 years ago

heliusAurelius commented 3 years ago

I managed to implement a simple Google reCAPTCHA validation in the form by adding in the reCAPTCHA, and using javascript to enable the submit button once it is solved. Doing so, captures the reCAPTCHA response in the sheet and email notification. But it seems that there are some bots which account for this and 're-enable' disabled submit buttons as well as ignoring my honeypot in the form.

Is there a way to add the required attribute to this reCAPTCHA response to prevent users/bots from simply re-enabling the submit button by editing the page code? When I add 'required' to the reCAPTCHA div, it doesn't validate the same way as the rest of the fields, and adding a script to return onsubmit="false" for the form seems to fire after the action="[google script]".

My form code is as follows:

<form class="gform" action="[google script URL]" method="POST">
    <div class="form-group" hidden>
        <label>
            Validation
        </label>
        <input name="honeypot" class="form-control"/>
    </div>
    <div class="form-group">
        <label>
            Name
        </label>
        <input name="Name" class="form-control" required />
    </div>
    <div class="form-group">
        <label>
            Email
        </label>
        <input name="Email" class="form-control" type="email" required />
    </div>
    <div class="form-group">
        <label>
            Message
        </label><textarea name="Message" class="form-control" rows="4" cols="50" required ></textarea>
    </div>
    <div align="center" class="g-recaptcha" data-sitekey="[captcha public key]" data-callback="callback"></div>
    <br />
    <button id="submit-button" class="btn-islamic-green btn-rd bloc-button btn btn-d btn-lg btn-block" type="submit" disabled="disabled">
            Submit
    </button>
    <div style="display:none" class="thankyou_message">
    <p class="text-center">Thanks for reaching out! I look forward to connecting with you soon!</p>
    </div>
</form>

I use page-level JS to re-enable the submit button once the reCAPTCHA is solved

<script type="text/javascript">
    function callback() {
      const submitButton = document.getElementById("submit-button");
      submitButton.disabled = false;
    }
</script>
bledatunay commented 3 years ago

Doesn't really matter if you disable or even completely hide the submit button (from human eyes), bots just care about the gform link inside the HTML. Using JavaScript for validation is meaningless, as bots bypass these steps anyways - I've seen bots spamming with honeypot field filled (Fun fact: It even adds a new honeypot column to the contact form sheet).

As far as I can tell, validation absolutely needs to happen inside the Google Apps Script (.gs), and CORS is simply not letting it. Anything besides making that happen looks like a dead end to me.

heliusAurelius commented 2 years ago

Has anyone been successful implementing this validation in Google scripts yet? I found this tutorial, but haven’t been successful with my project yet http://www.googleappsscript.org/recent-additions/recaptchawithgoogleappsscript

bledatunay commented 2 years ago

In the link you have provided, it would work because everything happens (including the user interaction) in the Google Script thus inside the parent domain (which is x.google.com), but again due to CORS restrictions, this won't work when your website on your own domain (which can't be x.google.com) requests it.

mckennapsean commented 2 years ago

the tutorial that @heliusAurelius posted does seem to allow custom domains (does that still not work with CORS though? it is possible that could still be a blocker)

I'd be curious if it could work, but I have not tried to see what happens in this case. it would be neat to have recaptcha built in, and it would make a nice enhancement for an advanced section of our tutorial here!

bledatunay commented 2 years ago

@mckennapsean Okay, so after you have said that it seems to allow custom domains, I have examined it again. It honestly looked like you were right, so I decided to give it another go. This time, voilà, it worked. My previous attempt may have been troubled by a completely different error, of which I may have falsely presumed as CORS related - I honestly don't know at this point.

Anyways, here's how I did it.

In form-submission-handler.js, I passed the g-recaptcha-response as a seperate form-specific value.

data.gRecaptchaResponse = document.getElementById("g-recaptcha-response").value;

In .gs, I assigned a variable to it in the very first line of doPost.

var r = String(e.parameters.gRecaptchaResponse);

Verified it with the function below just like in the tutorial @heliusAurelius posted.

function verifyCaptcha(r){
  var pl = {
    'secret' : secret,
    'response': r
  }
  var url = 'https://www.google.com/recaptcha/api/siteverify';
  var resp = UrlFetchApp.fetch(url, {
    payload : pl,
    method : 'POST'
  }).getContentText();
  return JSON.parse(resp).success;
}

Then bounded our main function to the verification.

if (verifyCaptcha(r)){
  try { 
  ...
  } else return;
}

Also, we can add the following into form-submission-handler.js to catch errors and give visitors some feedback.

xhr.onerror = function() {
  //error stuff
}

This works in a variety of conditions, like an expired captcha or a JavaScript blocker on the visitors browser.

Any submission attempts without the solved recaptcha will also return this, but it is preferable that we enable the submit button after user solves the recaptcha anyways.

We can disable the button in the function loaded() of form-submission-handler.js, enable it with data-callback when captcha is solved, and disable it again with data-expired-callback when it expires.

Only bots will be able to attempt submitting a response without solving the recaptcha, but we finally don't need to care about them.

I will now push it to my master and try it out for a few days - will try to give an update with my results.

bledatunay commented 2 years ago

Update: Not a single spam for 5 days straight (it was around 2-5 spams/day). I have tried submitting forms to myself from different devices (Android, iOS, Windows 10, Windows 7) with different browsers (Firefox, Chrome, Opera) - no issues so far.

heliusAurelius commented 2 years ago

@bledatunay I was able to follow along with your guide up until the "bounded our main function to the verification" section. Where in the gscript do I add this code?

bledatunay commented 2 years ago

You will be putting your whole doPost into an if statement which verifies the captcha. It will look like this:

function doPost(e) {
  var r = String(e.parameters.gresp);
  if (verifyCaptcha(r)){
  try {
    Logger.log(e);
    record_data(e);

    var mailData = e.parameters;

    var orderParameter = e.parameters.formDataNameOrder;
    var dataOrder;
    if (orderParameter) {
      dataOrder = JSON.parse(orderParameter);
    }
    var sendEmailTo = (typeof TO_ADDRESS !== "undefined") ? TO_ADDRESS : mailData.formGoogleSendEmail;

    if (sendEmailTo) {
      MailApp.sendEmail({
        to: String(sendEmailTo),
        subject: "Contact form submitted",
        htmlBody: formatMailBody(mailData, dataOrder)
      });
    }

    return ContentService
          .createTextOutput(
            JSON.stringify({"result":"success",
                            "data": JSON.stringify(e.parameters) }))
          .setMimeType(ContentService.MimeType.JSON);
  } catch(error) {
    Logger.log(error);
    return ContentService
          .createTextOutput(JSON.stringify({"result":"error", "error": error}))
          .setMimeType(ContentService.MimeType.JSON);
  }} else return;
}
heliusAurelius commented 2 years ago

@bledatunay worked like a charm! Thank you!

I'll be testing over the next few days to ensure this recaptcha is working to prevent spam submissions as well