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

[stimulus-bundle] Use defaultValue to change the value of hidden CSRF fields #1371

Merged
merged 1 commit into from
Jan 6, 2025

Conversation

nicolas-grekas
Copy link
Member

Q A
License MIT
Doc issue/PR -

In addition to symfony/symfony#59296
(see discussion there)

Copy link

github-actions bot commented Dec 29, 2024

Thanks for the PR 😍

How to test these changes in your application

  1. Define the SYMFONY_ENDPOINT environment variable:

    # On Unix-like (BSD, Linux and macOS)
    export SYMFONY_ENDPOINT=https://raw.githubusercontent.com/symfony/recipes/flex/pull-1371/index.json
    # On Windows
    SET SYMFONY_ENDPOINT=https://raw.githubusercontent.com/symfony/recipes/flex/pull-1371/index.json
  2. Install the package(s) related to this recipe:

    composer req symfony/flex
    composer req 'symfony/stimulus-bundle:^2.20'
  3. Don't forget to unset the SYMFONY_ENDPOINT environment variable when done:

    # On Unix-like (BSD, Linux and macOS)
    unset SYMFONY_ENDPOINT
    # On Windows
    SET SYMFONY_ENDPOINT=

Diff between recipe versions

In order to help with the review stage, I'm in charge of computing the diff between the various versions of patched recipes.
I'm going keep this comment up to date with any updates of the attached patch.

symfony/stimulus-bundle

2.8 vs 2.9
diff --git a/symfony/stimulus-bundle/2.9/assets/bootstrap.js b/symfony/stimulus-bundle/2.9/assets/bootstrap.js
new file mode 100644
index 0000000..2689398
--- /dev/null
+++ b/symfony/stimulus-bundle/2.9/assets/bootstrap.js
@@ -0,0 +1,2 @@
+// register any custom, 3rd party controllers here
+// app.register('some_controller_name', SomeImportedController);
diff --git a/symfony/stimulus-bundle/2.9/assets/controllers/hello_controller.js b/symfony/stimulus-bundle/2.9/assets/controllers/hello_controller.js
new file mode 100644
index 0000000..e847027
--- /dev/null
+++ b/symfony/stimulus-bundle/2.9/assets/controllers/hello_controller.js
@@ -0,0 +1,16 @@
+import { Controller } from '@hotwired/stimulus';
+
+/*
+ * This is an example Stimulus controller!
+ *
+ * Any element with a data-controller="hello" attribute will cause
+ * this controller to be executed. The name "hello" comes from the filename:
+ * hello_controller.js -> "hello"
+ *
+ * Delete this file or adapt it for your use!
+ */
+export default class extends Controller {
+    connect() {
+        this.element.textContent = 'Hello Stimulus! Edit me in assets/controllers/hello_controller.js';
+    }
+}
diff --git a/symfony/stimulus-bundle/2.9/assets/controllers.json b/symfony/stimulus-bundle/2.9/assets/controllers.json
new file mode 100644
index 0000000..a1c6e90
--- /dev/null
+++ b/symfony/stimulus-bundle/2.9/assets/controllers.json
@@ -0,0 +1,4 @@
+{
+    "controllers": [],
+    "entrypoints": []
+}
diff --git a/symfony/stimulus-bundle/2.8/manifest.json b/symfony/stimulus-bundle/2.9/manifest.json
index ff66e87..60e0ddb 100644
--- a/symfony/stimulus-bundle/2.8/manifest.json
+++ b/symfony/stimulus-bundle/2.9/manifest.json
@@ -2,5 +2,46 @@
     "bundles": {
         "Symfony\\UX\\StimulusBundle\\StimulusBundle": ["all"]
     },
-    "aliases": ["stimulus", "stimulus-bundle"]
+    "copy-from-recipe": {
+        "assets/": "assets/"
+    },
+    "aliases": ["stimulus", "stimulus-bundle"],
+    "conflict": {
+        "symfony/webpack-encore-bundle": "<2.0",
+        "symfony/flex": "<1.20.0 || >=2.0.0,<2.3.0"
+    },
+    "add-lines": [
+        {
+            "file": "webpack.config.js",
+            "content": "\n    // enables the Symfony UX Stimulus bridge (used in assets/bootstrap.js)\n    .enableStimulusBridge('./assets/controllers.json')",
+            "position": "after_target",
+            "target": ".splitEntryChunks()"
+        },
+        {
+            "file": "assets/app.js",
+            "content": "import './bootstrap.js';",
+            "position": "top",
+            "warn_if_missing": true
+        },
+        {
+            "file": "assets/bootstrap.js",
+            "content": "import { startStimulusApp } from '@symfony/stimulus-bridge';\n\n// Registers Stimulus controllers from controllers.json and in the controllers/ directory\nexport const app = startStimulusApp(require.context(\n    '@symfony/stimulus-bridge/lazy-controller-loader!./controllers',\n    true,\n    /\\.[jt]sx?$/\n));",
+            "position": "top",
+            "requires": "symfony/webpack-encore-bundle"
+        },
+        {
+            "file": "assets/bootstrap.js",
+            "content": "import { startStimulusApp } from '@symfony/stimulus-bundle';\n\nconst app = startStimulusApp();",
+            "position": "top",
+            "requires": "symfony/asset-mapper"
+        },
+        {
+            "file": "templates/base.html.twig",
+            "content": "            {{ ux_controller_link_tags() }}",
+            "position": "after_target",
+            "target": "{% block stylesheets %}",
+            "warn_if_missing": true,
+            "requires": "symfony/asset-mapper"
+        }
+    ]
 }
2.9 vs 2.13
diff --git a/symfony/stimulus-bundle/2.9/manifest.json b/symfony/stimulus-bundle/2.13/manifest.json
index 60e0ddb..4701215 100644
--- a/symfony/stimulus-bundle/2.9/manifest.json
+++ b/symfony/stimulus-bundle/2.13/manifest.json
@@ -34,14 +34,6 @@
             "content": "import { startStimulusApp } from '@symfony/stimulus-bundle';\n\nconst app = startStimulusApp();",
             "position": "top",
             "requires": "symfony/asset-mapper"
-        },
-        {
-            "file": "templates/base.html.twig",
-            "content": "            {{ ux_controller_link_tags() }}",
-            "position": "after_target",
-            "target": "{% block stylesheets %}",
-            "warn_if_missing": true,
-            "requires": "symfony/asset-mapper"
         }
     ]
 }
2.13 vs 2.20
diff --git a/symfony/stimulus-bundle/2.20/assets/controllers/csrf_protection_controller.js b/symfony/stimulus-bundle/2.20/assets/controllers/csrf_protection_controller.js
new file mode 100644
index 0000000..075d06c
--- /dev/null
+++ b/symfony/stimulus-bundle/2.20/assets/controllers/csrf_protection_controller.js
@@ -0,0 +1,60 @@
+var nameCheck = /^[-_a-zA-Z0-9]{4,22}$/;
+var 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) {
+    var csrfField = event.target.querySelector('input[data-controller="csrf-protection"]');
+
+    if (!csrfField) {
+        return;
+    }
+
+    var csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
+    var 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))));
+    }
+
+    if (csrfCookie && tokenCheck.test(csrfToken)) {
+        var cookie = csrfCookie + '_' + csrfToken + '=' + csrfCookie + '; path=/; samesite=strict';
+        document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
+    }
+});
+
+// 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) {
+    var csrfField = event.detail.formSubmission.formElement.querySelector('input[data-controller="csrf-protection"]');
+
+    if (!csrfField) {
+        return;
+    }
+
+    var csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
+
+    if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
+        event.detail.formSubmission.fetchRequest.headers[csrfCookie] = csrfField.value;
+    }
+});
+
+// When @hotwired/turbo handles form submissions, remove the CSRF cookie once a form has been submitted
+document.addEventListener('turbo:submit-end', function (event) {
+    var csrfField = event.detail.formSubmission.formElement.querySelector('input[data-controller="csrf-protection"]');
+
+    if (!csrfField) {
+        return;
+    }
+
+    var csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
+
+    if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
+        var 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';
diff --git a/symfony/stimulus-bundle/2.13/manifest.json b/symfony/stimulus-bundle/2.20/manifest.json
index 4701215..4289495 100644
--- a/symfony/stimulus-bundle/2.13/manifest.json
+++ b/symfony/stimulus-bundle/2.20/manifest.json
@@ -7,6 +7,8 @@
     },
     "aliases": ["stimulus", "stimulus-bundle"],
     "conflict": {
+        "symfony/framework-bundle": "<7.2",
+        "symfony/security-csrf": "<7.2",
         "symfony/webpack-encore-bundle": "<2.0",
         "symfony/flex": "<1.20.0 || >=2.0.0,<2.3.0"
     },

@nicolas-grekas nicolas-grekas changed the title [stimulus-bundle] Set autocomplete=off attribute using JS on hidden CSRF fields [stimulus-bundle] Use get/setAttribute() to change the value of hidden CSRF fields Jan 5, 2025
chalasr
chalasr previously approved these changes Jan 5, 2025
@MatTheCat
Copy link
Contributor

From my testing it appears you only need to replace the value property line 17 with defaultValue:

csrfField.value = csrfToken = btoa(String.fromCharCode.apply(null, (window.crypto || window.msCrypto).getRandomValues(new Uint8Array(18))));
would become

        csrfField.defaultValue = csrfToken = btoa(String.fromCharCode.apply(null, (window.crypto || window.msCrypto).getRandomValues(new Uint8Array(18))));

@nicolas-grekas nicolas-grekas changed the title [stimulus-bundle] Use get/setAttribute() to change the value of hidden CSRF fields [stimulus-bundle] Use defaultValue to change the value of hidden CSRF fields Jan 6, 2025
@nicolas-grekas
Copy link
Member Author

PR updated, thanks

@MatTheCat
Copy link
Contributor

Currently rechecking ⏳

@MatTheCat
Copy link
Contributor

Yep: this PR’s change combined with symfony/symfony#59296 prevent the submitted CSRF token to stay in the field after clicking the back button.

@nicolas-grekas
Copy link
Member Author

combined with symfony/symfony#59296

but this PR is not required, right? it still works without?

@MatTheCat
Copy link
Contributor

Yep symfony/symfony#59296 depends on this one but not the other way around.

@fabpot fabpot disabled auto-merge January 6, 2025 10:15
@fabpot fabpot merged commit 7e10c1d into main Jan 6, 2025
1 of 2 checks passed
@fabpot fabpot deleted the autocomplete-off branch January 6, 2025 10:15
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