-
Notifications
You must be signed in to change notification settings - Fork 64
Can't undersand how you example of recaptcha v3 will work #69
Comments
Hello, same doubts here :) |
Same problem. I also don't understand how v3 works in this library. Verification should be done on the server side right in the middleware. @biscolab any idea about it? |
@biscolab please give us some info about this? |
Same here. @biscolab Could you please explain us ? |
Agreed, I noticed this issue while installing and as a result we are using a different implementation. There doesn't seem to be any support for server-side validation, at least from what is mentioned in the docs. |
@biscolab Thanks for your fast answer. |
I browsed the issues, and v3 backend validation does not seem to be supplied by this package (source : #34 (comment)). The author does not state it clearly, but since he keeps asking us to just read the f** manual on google website, I suppose he is not planning to help us. So it is useless to ask, you will never be given an answer. Just find an other package if you want to use v3. |
The best approach is to select other package. In my case I have changed the package https://github.com/josiasmontag/laravel-recaptchav3 and use it, I don't remember exactly what I change but when I have some time I will do a properly package or submit a pull request to update that package. At the moment my package work very well and it's very easy to add in every form. |
@alexandreMesle @fabiomlferreira I'm sorry but I'm very busy...you can help me to improve the package if you need. I'm keen to check your pull request. The current package is based on the ReCaptcha V3 documentation. I ask your help to improve it. Thanks |
Trying to use this package with Recaptcha v3 and lighthouse graphql to prevent spam submissions on my contact request form and signup pages. In my case I need to call it programmatically from JS and have server-side validation check as part of the validation rules not client side. With the existing implementation, it appears that all the validation logic is done on the client-side when using v3. if (response.success && response.score >= 0.5) {
// Call server API for post/mutation.
} else {
// Deny?
} Main problem - Client side check can be bypassedThis seems wrong as a script or bot could simply bypass the client-side verify function that processes recaptcha response from server API check, and then proceed to spam the real request to Laravel API. I suspect that this current implementation may be incorrect, at least for the purpose of submitting from forms and preventing bots/spam. Reading this backs up my thinking. Issues/thoughts
The response from recaptcha validation is not a boolean, so lighthouse seems to allow it as a truthy.
The error here makes sense as the Whilst verifying v3 client-side is bogus, sending it from document ready is something recommended as it gives Recaptcha service more information to analyze and can show on their dashboard, this might help detect scrapers/bots but I have no further knowledge of their implementation for how this works.
That the validation rule could also take an 'action' argument to compare against the action in the response as its "important to verify" according to Google's documentation. The score should have a threshold value defaulting to 0.5 When ending to recaptcha/api/siteverify, sending the users IP address is optional.
Whilst well intentioned for easier development I suspect, Lighthouse mutation validation rulesA validation rule with parameters like this might suffice.
We can provide 'custom_validation' to the configuration for htmlScriptTagJsApi which replaces the default validation function that does the fetch, this is good, but it seems lacking in that its complicated to add and needs to be done at the page/view generating time in PHP so it can't be called multiple times and does not seem to work with promises. It could wrap with Promise.resolve but seems too complex. https://github.com/josiasmontag/laravel-recaptchav3/blob/master/src/RecaptchaV3.php does look like a better alternative, however I can't see how to programmatically call it on there either as it looks to only have field support. Source changesImproving the validator rule implementation. use Illuminate\Support\Carbon;
/**
* Extends Validator to include a recaptcha type
*/
public function addValidationRule()
{
$message = null;
if (!config('recaptcha.empty_message')) {
$message = trans(config('recaptcha.error_message_key'));
}
switch (config('recaptcha.version')) {
case 'v3':
Validator::extendImplicit(recaptchaRuleName(), function ($attribute, $value, $parameters) {
//info("Parameters for recaptcha validation are ".json_encode($parameters));
$threshold = floatval($parameters[0] ?? 0.5);
$action = $parameters[1] ?? '';
$response = app('recaptcha')->validate($value);
//info("recaptcha response is: ".json_encode($response));
//info("action is expected to be {$action} and score is expected to be >= {$threshold}");
if (isset($response['skip_by_ip']) && filled($response['skip_by_ip'])) {
return true;
}
// Verify action if present.
if (filled($action) && isset($response['action']) && $response['action'] !== $action) {
// info("recaptcha action verification failed");
return false;
}
// Verify that challenge_ts is within the last 2 minutes if present.
if (isset($response['challenge_ts']) && filled($response['challenge_ts'])) {
$challengeTimestamp = Carbon::parse($response['challenge_ts']);
$currentTimestamp = Carbon::now();
if ($challengeTimestamp->diffInMinutes($currentTimestamp) > 2) {
// info("recaptcha challenge_ts verification failed");
return false;
}
}
return (isset($response['success']) && isset($response['score']) && $response['success']
&& $response['score'] >= $threshold);
}, $message);
break;
case 'v2':
case 'invisible':
Validator::extendImplicit(recaptchaRuleName(), function ($attribute, $value) {
return app('recaptcha')->validate($value);
}, $message);
break;
}
} ReCaptchaBuilder.php /**
* Call out to reCAPTCHA and process the response
*
* @param string $response
*
* @return boolean|array
*/
public function validate($response)
{
// info("Recaptcha Validation called: ".$response);
if ($this->skip_by_ip) {
if ($this->returnArray()) {
// Add 'skip_by_ip' field to response
return [
'skip_by_ip' => true,
'score' => 0.9,
'success' => true
];
}
return true;
}
$params = http_build_query([
'secret' => $this->api_secret_key,
'remoteip' => request()->getClientIp(),
'response' => $response,
]);
$url = $this->api_url . '?' . $params;
if (function_exists('curl_version')) {
$curl = curl_init($url);
curl_setopt($curl, CURLOPT_HEADER, false);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_TIMEOUT, $this->getCurlTimeout());
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
$curl_response = curl_exec($curl);
} else {
$curl_response = file_get_contents($url);
}
if (is_null($curl_response) || empty($curl_response)) {
if ($this->returnArray()) {
// Add 'error' field to response
return [
'error' => 'cURL response empty',
'score' => 0.1,
'success' => false
];
}
return false;
}
$response = json_decode(trim($curl_response), true);
//info('Response from recaptcha'.json_encode($response));
if ($this->returnArray()) {
return $response;
}
return $response['success'];
} Added methods to expose an object we can call execute on in a way that supports promises. /**
* Writes a HTML script tag that exposes a ReCaptchaV3 object for resolving the reCAPTCHA token.
* Insert this before the closing </head> tag, following the htmlScriptTagJsApi call, as it does not load the reCAPTCHA script.
*
* The ReCaptchaV3 object in JavaScript has a method called execute that returns a promise resolving with a reCAPTCHA token.
* - action: string, defaults to 'homepage'.
* You may set this to a specific action, such as "contact_form_submit", based on the user's action.
*
* @return string The generated script HTML tag.
*/
public function htmlScriptTagJsObjectV3(): string
{
$html = '';
if ($this->skip_by_ip) {
$html .= "<script>
ReCaptchaV3 = {
execute: async (action = 'homepage') => return 'skip_by_ip'
};
</script>";
return $html;
}
$html .= "<script>
ReCaptchaV3 = {
execute: async (action = 'homepage') => {
return new Promise((resolve, reject) => {
grecaptcha.ready(function() {
grecaptcha.execute('{$this->api_site_key}', {action: action})
.then(token => resolve(token))
.catch(err => reject(err));
})
});
}
};
</script>";
return $html;
}
/***
* The same as htmlScriptTagJsObjectV3 but it loads the reCAPTCHA script if the user is not skipped by IP.
* Can be used if you only want to include on specific pages but not send on page load.
*
* @return string
*/
public function htmlScriptTagJsObjectV3WithDependency(): string
{
$html = '';
if (!$this->skip_by_ip) {
$html = "<script src=\"".$this->api_js_url."?render={$this->api_site_key}\"></script>";
return $html;
}
$html .= $this->htmlScriptTagJsObjectV3();
return $html;
} Might be nicer if empty handler callback had a dummy function that threw an error. Add to helpers.php for facade access? if (!function_exists('htmlScriptTagJsObjectV3')) {
/**
* Writes a HTML script tag that exposes a ReCaptchaV3 object for resolving the reCAPTCHA token.
* Insert this before the closing </head> tag, following the htmlScriptTagJsApi call, as it does not load the reCAPTCHA script.
*
* The ReCaptchaV3 object in JavaScript has a method called execute that returns a promise resolving with a reCAPTCHA token.
* - action: string, defaults to 'homepage'.
* You may set this to a specific action, such as "contact_form_submit", based on the user's action.
*
* Note: This is only valid for v3.
*
* @return string The generated script HTML tag.
*/
function htmlScriptTagJsObjectV3(): string
{
return ReCaptcha::htmlScriptTagJsObjectV3();
}
}
if (!function_exists('htmlScriptTagJsObjectV3WithDependency')) {
/***
* The same as htmlScriptTagJsObjectV3 but it loads the reCAPTCHA script if the user is not skipped by IP.
* Can be used if you only want to include on specific pages but not send on page load.
*
* @return string
*/
function htmlScriptTagJsObjectV3WithDependency(): string
{
return ReCaptcha::htmlScriptTagJsObjectV3WithDependency();
}
} And add to facade. * @method static string htmlScriptTagJsObjectV3()
* @method static string htmlScriptTagJsObjectV3WithDependency() Anyway with this change calling manually from js works I just don't have auto binding or blade template/helper to embed the button in form or as a hidden field as some implementations do, but I don't really need that for my use case :) And the validation from the rule works with Lighthouse + is a server-side check. Not 100% on the names of these functions though, init with optional flags in that configurations part might be nicer and calling it RecaptchaInitJs? Anywho, just my two cents. |
To summarize the above, got it working server-side for the verify check and validation rule with v3, a promise based way to call it on the JS side which can be done programmatically, and checking action, threshold, challenge_ts accordingly working with lighthouse graphql at least haven't tried standard post request in controller but should be ok. Thanks for the repo :) |
fix biscolab#69 - server side validation of the recaptcha v3 verify response. - validator now returns true/false - validator checks action if specified - validator checks challenge_ts within 2 minutes - validator has configurable parameters for threshold of score and action - js object with execute method able to be generated which can facilitate promise based calling
I have implemented the example that you have in the documentation page of V3, and I understand that using the callback_then and callback_catch you will make a request for a specific library endpoint that returns the score of the user, but even if the user have a really bad score how this will prevent the form to be submitted.
I think you should create a real example of a simple contact form protected with the recapctha v2, invisible and V3.
In the V2 example you append the g-recaptcha-response on the form that will be submitted and this make sense, but in V3 example I think you can always submit.
Can you explaining me if I'm seeing it wrong?
How this code will protect the form (in v3 example) if for example it is submitted for a bot that don't even run javascript?
The text was updated successfully, but these errors were encountered: