Change form tampering error message string (CakePHP4)

Howdy.

For a customer who needs a highly secure login I have set the cookie expire to 15 minutes, and have enabled form protection. Therefore if the user has the login form in front of them, but doesn’t fill it out until after 15 minutes they get confronted with the scary message “Form tampering protection token validation failed.”

Now I know I can modify that constant string in ./vendor/cakephp/cakephp/src/Controller/Component/FormProtectionComponent.php
but I really don’t want to go digging around and modifying the guts of CakePHP.

So I decided to think outside the box and use the translation routines to do this for me. So I set I18n::setLocale('en'); in AppController and in ./resources/locale/en/default.po I put: -

msgid "Form tampering protection token validation failed."
msgstr "This page has expired, please try refreshing the page."

and I know it works as I threw a <?= __('Form tampering protection token validation failed.') ?> in my template to test and it translated it.

But, when the form protection kicks in it still says “Form tampering protection token validation failed.”

So does that mean I cannot “translate” this to a polite and friendly message? (as opposed the default intimidating one, and perhaps worse giving a hacker a clue as to what blocked them). Also what is concerning is that when I do translate my site for other languages those users will still see this error in English only?

Is there some other means of changing the DEFAULT_EXCEPTION_MESSAGE? (Bearing in mind it may still need to dynamically translate it.)

Have I missed something here?

Please help!

Cheers
Jonathan

That scary text is the message of the exception BadRequestException.

This exception is thrown by FormProtectionComponent::validationFailure() which runs as part of FormProtectionComponent::startup().

If you can figure out where in your controller code you trigger this sequence, you can wrap that controller code in a try/catch block. When you catch the BadRequestException your can re-throw it with a new message.

I don’t have any reason to believe the process runs base on Controller::initialize() and the loadComponent() call, but assuming it did as an example, your code could look like this:

Poking around a little and putting some debugs to get as stacktrace to see when startup() runs, it appears this core Controller code does the deed:

You can override this method in your AppController (or even is just one specific controller if the problem is isolated).

In the controller of your choice you can put this code (mostly a copy of the core method with just the try/catch added)

Thanks for you replies :slight_smile:

I got this error: -
Fatal error: Declaration of App\Controller\UsersController::startupProcess(): ?App\Controller\ResponseInterface must be compatible with Cake\Controller\Controller::startupProcess(): ?Psr\Http\Message\ResponseInterface in /.../src/Controller/UsersController.php on line 15

But even still without that I am not comfortable with the idea of copying the framework code and putting in my own twist. The reason being is I intend this app to be upgradable, in that the only place I am putting code etc is in config, resources, src, templates & webroot. I can start a new project, drop those in and maybe fix a depreciated call or 2, and keep going. Copying blocks of code will negate any changes done in the framework there.

Also, it still is less translate proof - I say less because I know I need only do throw new BadRequestException(__('Form tampering protection token validation failed.')); and hard-code the error string which is hard-coded deep within CakePHP.

What I’ve done for now, which is equally hacky, but less invasive, is in my ./templates/layout/error.php is put this code just above the </body>

  <script>
    <?php $DEFAULT_EXCEPTION_MESSAGE = 'Form tampering protection token validation failed.'; ?>
    document.body.innerHTML = 
      document.body.innerHTML.replace(/<?= $DEFAULT_EXCEPTION_MESSAGE ?>/g,
        "<?= __($DEFAULT_EXCEPTION_MESSAGE) ?>");
  </script>

Distasteful!!! But, it does translate, is in an obvious spot - and, has the same level of approach to a hard-coded string as the form protector error message has.

If this error message actual is a resource I can reference, instead of one I need to duplicate to fix, please let me know. Because one slight change to that hard-coded error message string and my hack stops working!

Cheers

Doing a bit more digging on this I came across this request: -


and I accept the last comment made being “To my experience one should not show exception messages, even translated, to their users. Exceptions are for developers, to help them debug problem, not for users to make sense of.”

And if we really are determined to show our exceptions to the user, then we can use the error templates, which is what that is for!

The only one gripe I have is the actual constant string is called DEFAULT_EXCEPTION_MESSAGE. It cannot be a “default” value (dictionary: “a preselected option adopted by a computer program or other mechanism when no alternative is specified by the user or programmer”) if it cannot be overwritten in situ. It is the only value, not a default one; the choice of that name raises expectations!

While I understand your objection, I see overriding an occasional bit of code for and ‘important’ function to be a valid use of OOP and one advantage of extensions. Maybe this version with much lower coupling?

    /**
     * Perform the startup process for this controller.
     * Fire the Components and Controller callbacks in the correct order.
     *
     * - Initializes components, which fires their `initialize` callback
     * - Calls the controller `beforeFilter`.
     * - triggers Component `startup` methods.
     *
     * @return \Psr\Http\Message\ResponseInterface|null
     */
    public function startupProcess(): ?ResponseInterface
    {
        try {
            return parent::startupProcess();
        } catch (BadRequestException $e) {
            $msg = 'message'; //derive your translated message here
            throw new BadRequestException($msg);
        }
    }

Or don’t rethrow the exception but redirect with a flash message.

Since this error is actually triggered by a listener, maybe you could register your own listener in Application.php or a config that you are willing to modify and intervene in that way.

I’m not super experienced with controlling the order of execution of the listeners or controlling the continued or disrupted flow through the various listeners on an event, but I do know Cake provides tools to do all those things.

Both very good ideas, I like your calling the parent code, catching the exception, as the error string can be changed and the catch will still work. I’ll also look into the listener, as that’s a clever use for it. Thanks again for helping me out with this :slight_smile:

Ok, so I have a complete solution, it’s OO friendly, it’s future-proof[ish], and it can handle translation - and I feel it uses the same level approach to the constant string as the source code. (it’s also 3AM, so forgive typos!)

It uses the callback to handle the exception, I feel a cleaner solution than error trapping a call to the parent.

In AppController.php

//At the top below the namespace
use Cake\I18n\I18n;
use Cake\Http\Exception\BadRequestException;

...

//immediately after the class declaration
    public const DEFAULT_EXCEPTION_MESSAGE = 'Form tampering protection token validation failed.';

...

//In beforeFilter() after its parent call
        I18n::setLocale('en'); //you set your locale however needed
        $this->FormProtection->setConfig('validationFailureCallback', function() {
            throw new BadRequestException(__(static::DEFAULT_EXCEPTION_MESSAGE));
        });

Even though I am using the exact same string as what’s hard-coded deep within the framework, its now my string, so any future changes to that in the framework won’t touch my code here. Also I’ve put it in AppController, a rather generic approach to what could be a specific problem - but I suspect multiple controllers could also raise this exception due to the high level of security the web-page utilises.

Now I want to friendly up my string, in my resources/locales/en/default.po I have that: -

msgid "Form tampering protection token validation failed."
msgstr "This page has expired, please try refreshing the page."

I have what I hope here is the correct CakePHP solution to my request.

Although, final thought, I still do stand by that statement of ravage84 where we really shouldn’t be presenting the end-user with our exceptions, especially ones we know can happen!

1 Like