Hi,
I'm experiencing inconsistencies with getting Google ReCAPTCHA v3 user score to populate in a hidden field when a form is submitted. The score is added correctly to the hidden field most of the time but is empty roughly 10-15% of the time (based on our lead submissions this month). I've followed the recommended Marketo implementation of recaptcha and will place the current implementation below. The implementation below is used on all of our pages that include Marketo forms, and I we have seen issues on all of them (not finding any trends that would lead me to believe it's a specific page-related issue). NOTE: We are NOT using Marketo for the validation process - this is built on Adobe Runtime, and as you can see is called inside the grecaptcha.execute function after the recaptchaFinger is returned (in .then()). Also, we only recently changed the captchaResponse field (the hidden field that is added with recaptcha score) to be readOnly, so this shouldn't be an issue.
If anyone has any thoughts or tips to resolve this it would be greatly appreciated! Thanks!
// if Marketo Forms function available, and document contains a marketo form
if (typeof MktoForms2 !== "undefined" && document.querySelectorAll("form[id^=\"mktoForm_\"]").length > 0) {
/* ======= START RECAPTCHA ======= */
// user config for recaptcha
var userConfig = {
apiKeys : {
recaptcha : <REMOVED_FOR_PRIVACY>
},
fields : {
recaptchaFinger : "lastRecaptchaUserInput"
},
actions : {
formSubmit : "form"
}
};
/* inject the reCAPTCHA library */
recaptchaLib = document.createElement("script");
recaptchaLib.src="https://www.google.com/recaptcha/api.js?render=" + userConfig.apiKeys.recaptcha + "&onload=grecaptchaListeners_ready";
document.head.appendChild(recaptchaLib);
MktoForms2.whenReady(function(mktoForm) {
var formEl = mktoForm.getFormElem()[0],
submitButtonEl = formEl.querySelector("button[type='submit']"),
moveEl = formEl.querySelector('.google-recaptcha-disclaimer');
// move disclaimer text row below submit button
[].forEach.call(formEl.querySelectorAll('.mktoForm > .mktoFormRow'), function(row) {
!row.contains(moveEl) || formEl.appendChild(row);
});
/* pending reCAPTCHA widget ready */
submitButtonEl.disabled = true;
/* pending reCAPTCHA verify */
mktoForm.submittable(false);
mktoForm.locked = false;
mktoForm.onValidate(function(native) {
if (!native) return;
// generate recaptcha token
grecaptcha.ready(function() {
grecaptcha.execute(userConfig.apiKeys.recaptcha, {
action: userConfig.actions.formSubmit
})
.then(function(recaptchaFinger) {
// =====================================
// Validation built on Adobe I/O Runtime
// =====================================
fetch('https://adobeioruntime.net/api/v1/web/23476-166aquamarinetoucan/default/validate.json?token='+ recaptchaFinger)
.then(function(response) { return response.json(); })
.then(function(data) { // access to response object here: score, success, challenge_ts, hostname
_satellite.logger.log('recieved captcha data')
var mktoFields = {};
// log errors, add errors to mktofield
if (data["error-codes"]) {
_satellite.logger.error('Error(s) validating recaptcha: ', data["error-codes"]);
_satellite.setVar('recaptchaError', data["error-codes"].join('|'));
mktoFields["recaptchaError"] = data["error-codes"].join('|');
}
if (mktoForm.locked == false) {
mktoForm.locked = true;
// set hidden fields with response data OR error
mktoFields["captchaResponse"] = data.score || data["error-codes"].join('|');
mktoForm.addHiddenFields(mktoFields);
_satellite.logger.log('hidden fields added: ', document.querySelector('input[name=captchaResponse]'))
document.querySelector('input[name=captchaResponse]').readOnly = true;
_satellite.logger.log('in recaptcha fetch making form submittable, submitting');
// submit the form
mktoForm.submittable(true);
mktoForm.submit();
_satellite.logger.log('FORM SUBMITTING (in recaptcha tag)', _satellite.getVar('recaptchaResponse'))
}
})
.catch(function(err) {
_satellite.logger.error('Error getting recaptcha response from Adobe Runtime API: ', err)
});
});
});
});
});
var recaptchaListeners = {
ready : function() {
MktoForms2.whenReady(function(mktoForm){
var formEl = mktoForm.getFormElem()[0],
submitButtonEl = formEl.querySelector("button[type='submit']");
submitButtonEl.disabled = false;
});
}
};
Object.keys(recaptchaListeners).forEach(function globalize(fnName){
window["grecaptchaListeners_" + fnName] = recaptchaListeners[fnName];
});
/* ======== END RECAPTCHA ======== */
}
I've followed the recommended Marketo implementation of recaptcha and will place the current implementation below.
? This is certainly not the recommended implementation because you can trivially work around the reCAPTCHA!
The whole idea of reCAPTCHA is the validation occurs outside of the user’s control. What you’ve posted here allows the end user to spoof a passed/high-confidence reCAPTCHA.
Hi Sanford, I'll provide a little more context and please let me know if you have any recommendations.
We originally used Marketo for our validation process - passed in the recaptcha token generated by grecaptcha.execute into a hidden field and then handled the validation once inside Marketo. This led to inconsistencies as well, where empty values were getting passed into that hidden field (not sure why, but I saw another community post explaining that the validation process on Marketo is also not fool proof).
The other thing to mention is we have multiple places that we want to share the response from the recaptcha (Adobe Analytics, paid media gtags, etc that fire on form submit) so it helps to have access to the response on the front end. I do not believe this would be possible if we did Marketo validation. We are also not blocking submissions on the front end, we are passing in the score to Marketo and then handling the cleaning process of those leads from that point.
Could you elaborate on how a bot would be able to tap into the current process? I imagine we could set up a CORs policy to block usage of that API endpoint from outside sources. Aside from the fact that the API endpoint is exposed, I can't identify any timing issues.. the response from the validation is returned before the value is added to the hidden field and the form is submitted. Any ideas?
This led to inconsistencies as well, where empty values were getting passed into that hidden field (not sure why, but I saw another community post explaining that the validation process on Marketo is also not fool proof).
Absolutely no reason for this to happen in a proper setup. The population of the user fingerprint (client-side “response”) field is a matter of setting correctly it via JS.
Could you elaborate on how a bot would be able to tap into the current process? I imagine we could set up a CORs policy to block usage of that API endpoint from outside sources. Aside from the fact that the API endpoint is exposed, I can't identify any timing issues.. the response from the validation is returned before the value is added to the hidden field and the form is submitted. Any ideas?
Has nothing to do with CORS. I can trivially add whatever captchaResponse value I want to the form data and submit it, totally bypassing the reCAPTCHA.
The server-side response from Google must be retrieved on the server. You can’t let the client send it, that’s kind of the main idea.
Okay I'm starting to catch on, and I appreciate your feedback - just a few more questions if you don't mind.
- I was under the impression that regardless of whether a bot tried to hardcode a value in the captchaResponse field, it would trigger the Marketo submission process when they submit the form, and thus overwrite that field with the correct value when submitted. If this is not the case, then I understand why everything needs to handled via serverside.
- As I mentioned, we have multiple places where we would ideally have the validation response (Adobe Analytics, paid media gtags, etc) to ensure all of these sources have the same data, otherwise for instance a paid media partner might claim they were driving real leads that turn out to be bots, or Adobe Analytics data might be inflated. Perhaps Salesforce would need to be our source of truth, but we are basically trying to avoid the need to clean multiple databases. Do you have any thoughts on this?
- I've provided an updated code snippet below which reflects how we originally used Marketo for our validation. Does this look like the correct implementation? This was the implementation where we were still getting an empty value in that hidden field, and the Marketo validation was not always working (not necessarily returning an error, just not working).
/* ======= START RECAPTCHA ======= */
// user config for recaptcha
var userConfig = {
apiKeys : {
recaptcha : "6LcmDNkaAAAAANWk6B6LIUw8z_X2_u0Fw4TxvZtX"
},
fields : {
recaptchaFinger : "lastRecaptchaUserInput"
},
actions : {
formSubmit : "form"
}
};
/* inject the reCAPTCHA library */
recaptchaLib = document.createElement("script");
recaptchaLib.src="https://www.google.com/recaptcha/api.js?render=" + userConfig.apiKeys.recaptcha + "&onload=grecaptchaListeners_ready";
document.head.appendChild(recaptchaLib);
// Add recaptcha process
MktoForms2.whenReady(function(mktoForm) {
var formEl = mktoForm.getFormElem()[0],
submitButtonEl = formEl.querySelector("button[type='submit']"),
moveEl = formEl.querySelector('.google-recaptcha-disclaimer');
// move disclaimer text row below submit button
[].forEach.call(formEl.querySelectorAll('.mktoForm > .mktoFormRow'), function(row) {
!row.contains(moveEl) || formEl.appendChild(row);
});
/* pending reCAPTCHA widget ready */
submitButtonEl.disabled = true;
/* pending reCAPTCHA verify */
mktoForm.submittable(false);
mktoForm.locked = false;
mktoForm.onValidate(function(native) {
_satellite.logger.log('In form onValidate function, initializing ReCAPTCHA')
if (!native) return;
grecaptcha.ready(function() {
grecaptcha.execute(userConfig.apiKeys.recaptcha, {
action: userConfig.actions.formSubmit
})
.then(function(recaptchaFinger) {
var mktoFields = {};
if (mktoForm.locked == false) {
_satellite.logger.log("primary recaptcha response resolved");
mktoForm.locked = true;
mktoFields[userConfig.fields.recaptchaFinger] = recaptchaFinger;
mktoForm.addHiddenFields(mktoFields);
mktoForm.submittable(true);
mktoForm.submit();
} else {
_satellite.logger.log("secondary recaptcha response resolved");
}
// save token in data element for reference in googleEvent helper
_satellite.setVar('recaptchaToken', recaptchaFinger);
});
});
});
});
var recaptchaListeners = {
ready : function() {
MktoForms2.whenReady(function(mktoForm){
var formEl = mktoForm.getFormElem()[0],
submitButtonEl = formEl.querySelector("button[type='submit']");
submitButtonEl.disabled = false;
});
}
};
Object.keys(recaptchaListeners).forEach(function globalize(fnName){
window["grecaptchaListeners_" + fnName] = recaptchaListeners[fnName];
});
/* ======== END RECAPTCHA ======== */
- I was under the impression that regardless of whether a bot tried to hardcode a value in the captchaResponse field, it would trigger the Marketo submission process when they submit the form, and thus overwrite that field with the correct value when submitted. If this is not the case, then I understand why everything needs to handled via serverside.
Not at all, a bot doesn’t need to run your special JS. It can just post the form directly, running only the standard form submission process and popping in its own forged captchaResponse.
- As I mentioned, we have multiple places where we would ideally have the validation response (Adobe Analytics, paid media gtags, etc) to ensure all of these sources have the same data, otherwise for instance a paid media partner might claim they were driving real leads that turn out to be bots, or Adobe Analytics data might be inflated. Perhaps Salesforce would need to be our source of truth, but we are basically trying to avoid the need to clean multiple databases. Do you have any thoughts on this?
I understand the goal, but you simply can’t check the response on the client side. You have to find some way to federate the server-side response to your other systems, perhaps via webhook or some other sync process.
Recommend you start with this exact code, no alterations other than in the config section:
MktoForms2 :: reCAPTCHA v3 v1.1.2
https://codepen.io/figureone/pen/yLgKXge?editors=0010
Thank you, we are reimplementing this solution. Given the fact that bots can still submit the form without running the recaptcha js, is it safe to say that any leads that are submitted without the recaptcha finger (token) are likely bot traffic? Is it common to make the hidden field required so that no submissions can go through without it?
Thank you Sanford!