Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@aerni
Copy link
Contributor

@aerni aerni commented Oct 25, 2024

What it does

This PR takes another stab at #10306 which was reverted in #10898. The nocache_js_position config option introduced in the latter PR isn't optimal, as you've got to pick your poison and choose between Livewire or nocache to work as expected.

This PR picks up on the idea pointed out here and extracts the CSRF token replacer script from the nocache replacer script. The CSRF replacer script is inserted as the first script in the head, while the nocache replacer script is placed at the end of the body. This way, you don't have to pick the script's position and can have both Livewire and nocache work alongside each other.

Note

This PR has undergone several iterations. Previously, I put the decoupling of the scripts behind a feature flag. This wasn't optimal but allowed for a non-breaking update. With the release of Statamic 6 coming close, I figured I might as well make this a breaking change.

Breaking changes

Events

The statamic:nocache.replaced event is no longer dispatched when the CSRF token is replaced. It is now only dispatched by the nocache script. Instead, you should use the new statamic:csrf.replaced event.

Script replacement

The StaticCache::nocacheJs($script) method now only replaces the nocache script. It doesn't touch the CSRF token script. Use the new StaticCache::csrfTokenJs($script) method if you want to customize the CSRF script.

Removed config option

The nocache_js_position config option introduced in #10898 is obsolete now and has been removed.

Testing

Here's a basic layout that you can use for testing. Note, that the CSRF token is replaced and nocache also works. Make sure to enable the new config option and full static caching.

<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">
    </head>
    <body>
        {{ nocache }}
            {{ now | iso_format('Y-m-d H:i:s') }}
        {{ /nocache }}
        <script data-csrf="{{ csrf_token }}"></script>
    </body>
</html>

@aerni aerni force-pushed the feature/csrf-script branch from 4a73e63 to 13f1ad4 Compare October 31, 2024 15:06
@aerni aerni changed the base branch from 5.x to master February 12, 2025 17:58
@aerni aerni changed the title [5.x] Decouple CSRF token from nocache script [6.x] Decouple CSRF token from nocache script Feb 12, 2025
@aerni
Copy link
Contributor Author

aerni commented Mar 17, 2025

@jasonvarga Just tagging you here to ensure you take a look at it before the release of v6 as it introduces a couple of small breaking changes.

@duncanmcclean
Copy link
Member

@aerni It looks like there's a couple of merge conflicts. Would you be able to take a look?

@aerni
Copy link
Contributor Author

aerni commented Aug 13, 2025

Done. I had to move the code from PR #11650 from the getCsrfTokenJs() to the new getNocacheJs() method. Functionally, it should be the same.

@duncanmcclean
Copy link
Member

Thanks!

@godismyjudge95
Copy link
Contributor

I could be entirely wrong, but isn't the whole point of CSRF that you can't get a token so you can't make backend requests from other domains? Wouldn't this allow anyone to request a CSRF token and then make cross site requests?

@jasonvarga
Copy link
Member

The csrf token is only for that user. Its attached to their session. If you hit that route to get a new csrf token, it would be yours and attached to your session.

@godismyjudge95
Copy link
Contributor

To follow up, I think in theory this allows this attack:

// On attacker.com
fetch('https://statamic.com/!/csrf', {
    credentials: 'include',
})
.then(r => r.json())
.then(data => {
    fetch('https://statamic.com/any-malicious-request', {
        method: 'POST',
        credentials: 'include',
        headers: {
            'X-CSRF-TOKEN': data.token
        }
    });
});

This is the kind of attack CSRF is meant to protect against.

@jasonvarga
Copy link
Member

jasonvarga commented Oct 20, 2025

Unless you explicitly allow it, CORS will prevent cross-site requests to these routes.

(I was attacking from statamic.com.test, which is different from statamic.test - sorry, I should have picked more different domains for this example)

CleanShot 2025-10-20 at 14 56 51

@godismyjudge95
Copy link
Contributor

Unless you explicitly allow it, CORS will prevent cross-site requests to these routes.

CleanShot 2025-10-20 at 14 56 51

No, I think that's a browser side prevention. You can still make those requests manually via something like Postman or curl. If we need to move this discussion to Discord for more rapid response we can. I am spinning up a proof of concept as I think this might be a bigger security issue.

@jasonvarga
Copy link
Member

jasonvarga commented Oct 20, 2025

This has been this way for a long time. This PR isn't really introducing anything new. If you want to consider this a security issue please send it appropriately and not just as a public comment. Thanks!

Being in the browser is kind of the point though. That's what it's protecting against. If you do it through postman/curl, you will not have the user's session. You'd just have your own. No different from you manually visiting the site.

@jasonvarga jasonvarga merged commit 99b22f7 into statamic:master Oct 20, 2025
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants