Skip to content
This repository has been archived by the owner on Jun 10, 2024. It is now read-only.

Can't undersand how you example of recaptcha v3 will work #69

Open
fabiomlferreira opened this issue Jan 20, 2022 · 11 comments
Open

Can't undersand how you example of recaptcha v3 will work #69

fabiomlferreira opened this issue Jan 20, 2022 · 11 comments

Comments

@fabiomlferreira
Copy link

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?

@nocodelab
Copy link

Hello, same doubts here :)
Is there any validation to be implemented in the Request/Controllers?

@lamualfa
Copy link

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?

@fabiomlferreira
Copy link
Author

@biscolab please give us some info about this?

@loranger
Copy link

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 ?

@JonathanZWeber
Copy link

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.

@alexandreMesle
Copy link

@biscolab Thanks for your fast answer.

@alexandreMesle
Copy link

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.

@fabiomlferreira
Copy link
Author

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.

@biscolab
Copy link
Owner

@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

@LiamKarlMitchell
Copy link

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?
}

Main problem - Client side check can be bypassed

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

Issues/thoughts

  1. Validation in server side is returning an array for v3 should be returning a bool
    solution make custom validator that calls the original validate.
  2. Need to pass token on form input to server side don't check verification and trigger the action client side.
  3. Validator should return false if success is false.
  4. Validator should check a threshold that can be configurable as a parameter to the validator, defaulting at value of 0.5, if the score is >= this threshold and successful response then it is allowed.
  5. Validator should verify action on response from recaptcha service is the expected value.
  6. Promise support and simpler use?
  7. The client side validate Function should actually send the mutation/post request to the server with the recaptcha-token and csrf-token if necessary. This should probably be the same function that does the work of submitting the form/mutation but also include the recaptcha_token.
  8. Validation of the response from recaptcha service should be done on the server-side as it is easily bypassed if not protecting the mutation/post request to the Laravel API.
  9. I feel that the existing script implementation is not developer friendly to use on the JS side. It should return a promise with the resolved response from the server API call when submitting forms/requests etc.
  10. 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.

Lighthouse mutation validation rules

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.

Source changes

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!

@LiamKarlMitchell
Copy link

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 :)

LiamKarlMitchell added a commit to LiamKarlMitchell/laravel-recaptcha that referenced this issue Jul 11, 2023
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
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants