Open fabiomlferreira opened 2 years ago
Hello, same doubts here :) Is there any validation to be implemented in the Request/Controllers?
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.
I desesperatly need to get rid of spam bots, but I can't figure out how to make it work.
Am I suppose to add anything to my validator ? Do I have to block form submission ? Where is the success
return handled ?
@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 : https://github.com/biscolab/laravel-recaptcha/issues/34#issuecomment-599194777).
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. I had a thought that maybe v2 is better for my use-case but wanted to check out v3.
With the existing implementation, it appears that all the validation logic is done on the client-side when using v3. Such as rejecting the form submission based on score is to be handled completely on the client side and the method would then call the API call to own server for example submitting a form as a secondary request.
if (response.success && response.score >= 0.5) {
// Call server API for post/mutation.
} else {
// Deny?
}
This 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. https://javascript.plainenglish.io/hahow-to-integrate-google-recaptcha-v3-correctly-in-2022-15a4fd186d7
0.5
, if the score is >= this threshold and successful response then it is allowed.recaptcha_token
.validateV3
in the ReCaptchaController is not necessarily the best name although I suppose some people might have reasons to use it, it would not protect anything? And it would not be able to verify the action
matches what is expected but it does seem like it may be useful for ReCaptcha service for gathering information about the page being viewed.The response from recaptcha validation is not a boolean, so lighthouse seems to allow it as a truthy.
{"success":false,"error-codes":["invalid-input-response"]}
The error here makes sense as the recaptcha-token
is not being passed to the server on my mutation call with the current implementation.
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.
reCAPTCHA works best when it has the most context about interactions with your site, which comes from seeing both legitimate and abusive behavior. For this reason, we recommend including reCAPTCHA verification on forms or actions as well as in the background of pages for analytics.
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
Also Send the token immediately to your backend with the request to [verify](https://developers.google.com/recaptcha/docs/verify).
When ending to recaptcha/api/siteverify, sending the users IP address is optional. This could be a configurable option.
remoteip | Optional. The user's IP address.
Whilst well intentioned for easier development I suspect, skip_by_ip
is not good to use in real world servers.
As X-Forwarded-For
header can easily be spoofed to be 127.0.0.1
for example which might be common setting for developers to whitelist and get left in by mistake, so a note towards this might be worthwhile in the README.md.
A validation rule with parameters like this might suffice.
input CreateContactUsInput {
contact_number: String!
message: String!
email: String!
recaptcha_token: String! @rules(apply: ["recaptcha:0.5,contact_form_submit"])
}
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://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve
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.
And it does not verify the challenge_ts
.
Improving the validator rule implementation. ReCaptchaServiceProvider.php
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 Seems like this validate function should be in the V3 builder however?
/**
* 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. ReCaptchaBuilderV3.php
/**
* 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. Facades/ReCaptcha.php
* @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. I can make a fork/pr but I must admit I don't know how to write good test for this behaviour at least not end-to-end, and think there is still some improvements that can be had. But nice to have something working phew!
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 :)
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?