Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[LiveComponent] CSRF token not valid after form submit #2527

Open
redflo opened this issue Jan 25, 2025 · 4 comments
Open

[LiveComponent] CSRF token not valid after form submit #2527

redflo opened this issue Jan 25, 2025 · 4 comments
Labels

Comments

@redflo
Copy link

redflo commented Jan 25, 2025

I have a issue with live components and CSRF Tokens.
I have a form with some components and i use dynamic form builder, which works great. If i fill the form correctly and push submit (which is a live action), all works great.
If i do not fill the form correctly, i get validation errors on submit. All good until now. But when i enter data in this situation, and i tab out of a input field, i get a csrf error.

Using Symfony 7.2 and default csrf config.

@redflo redflo added the Bug Bug Fix label Jan 25, 2025
@carsonbot carsonbot added the Status: Needs Review Needs to be reviewed label Jan 25, 2025
@redflo redflo changed the title Live components CSRF toke not valid after form submit Live components CSRF token not valid after form submit Jan 25, 2025
@smnandre
Copy link
Member

Hi @redflo, thank you for opening this issue!

Just to be sure, can you tell us:

  • the version of Live Component you use: composer show "symfony/ux-*"
  • the content of the file assets/controllers/csrf_protection_controller.js (if it exists)

Thanks!

@smnandre smnandre added LiveComponent and removed Status: Needs Review Needs to be reviewed labels Jan 25, 2025
@smnandre smnandre changed the title Live components CSRF token not valid after form submit [LiveComponent] CSRF token not valid after form submit Jan 25, 2025
@redflo
Copy link
Author

redflo commented Jan 25, 2025

composer show "symfony/ux-*"
symfony/ux-autocomplete   v2.22.1 JavaScript Autocomplete functionality for Symfony
symfony/ux-dropzone       v2.22.1 File input dropzones for Symfony Forms
symfony/ux-live-component v2.22.1 Live components for Symfony
symfony/ux-twig-component v2.22.1 Twig components for Symfony

Contents of assets/controllers/csrf_protection_controller.js

const nameCheck = /^[-_a-zA-Z0-9]{4,22}$/;
const tokenCheck = /^[-_\/+a-zA-Z0-9]{24,}$/;

// Generate and double-submit a CSRF token in a form field and a cookie, as defined by Symfony's SameOriginCsrfTokenManager
document.addEventListener('submit', function (event) {
    generateCsrfToken(event.target);
}, true);

// When @hotwired/turbo handles form submissions, send the CSRF token in a header in addition to a cookie
// The `framework.csrf_protection.check_header` config option needs to be enabled for the header to be checked
document.addEventListener('turbo:submit-start', function (event) {
    const h = generateCsrfHeaders(event.detail.formSubmission.formElement);
    Object.keys(h).map(function (k) {
        event.detail.formSubmission.fetchRequest.headers[k] = h[k];
    });
});

// When @hotwired/turbo handles form submissions, remove the CSRF cookie once a form has been submitted
document.addEventListener('turbo:submit-end', function (event) {
    removeCsrfToken(event.detail.formSubmission.formElement);
});

export function generateCsrfToken (formElement) {
    const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');

    if (!csrfField) {
        return;
    }

    let csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
    let csrfToken = csrfField.value;

    if (!csrfCookie && nameCheck.test(csrfToken)) {
        csrfField.setAttribute('data-csrf-protection-cookie-value', csrfCookie = csrfToken);
        csrfField.defaultValue = csrfToken = btoa(String.fromCharCode.apply(null, (window.crypto || window.msCrypto).getRandomValues(new Uint8Array(18))));
        csrfField.dispatchEvent(new Event('change', { bubbles: true }));
    }

    if (csrfCookie && tokenCheck.test(csrfToken)) {
        const cookie = csrfCookie + '_' + csrfToken + '=' + csrfCookie + '; path=/; samesite=strict';
        document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
    }
}

export function generateCsrfHeaders (formElement) {
    const headers = {};
    const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');

    if (!csrfField) {
        return headers;
    }

    const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');

    if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
        headers[csrfCookie] = csrfField.value;
    }

    return headers;
}

export function removeCsrfToken (formElement) {
    const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');

    if (!csrfField) {
        return;
    }

    const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');

    if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
        const cookie = csrfCookie + '_' + csrfField.value + '=0; path=/; samesite=strict; max-age=0';

        document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
    }
}

/* stimulusFetch: 'lazy' */
export default 'csrf-protection-controller';

@redflo
Copy link
Author

redflo commented Jan 26, 2025

I also found out, that i have two locations with csrf configs. I'm not sure if the originate from flex recipes or if i did that:
config/packages/csrf.yaml

# Enable stateless CSRF protection for forms and logins/logouts
framework:
    form:
        csrf_protection:
            token_id: submit

    csrf_protection:
        stateless_token_ids:
            - submit
            - authenticate
            - logout

and in config/packages/framework.yaml:

framework:
    secret: '%env(APP_SECRET)%'

    form: { csrf_protection: { token_id: 'submit' } }
    csrf_protection:
       stateless_token_ids: ['submit', 'authenticate', 'logout']

Which is basically the same, but makes it hard to switch off or alter the csrf config.

@nicolas-grekas
Copy link
Member

Legacy recipe I suppose. You can remove the duplicate in config/packages/framework.yaml and keep csrf.yaml

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants