|
1 | 1 | const nameCheck = /^[-_a-zA-Z0-9]{4,22}$/;
|
2 |
| -const tokenCheck = /^[-_\/+a-zA-Z0-9]{24,}$/; |
3 |
| - |
4 |
| -// Generate and double-submit a CSRF token in a form field and a cookie, as defined by Symfony's SameOriginCsrfTokenManager |
5 |
| -document.addEventListener('submit', function (event) { |
6 |
| - generateCsrfToken(event.target); |
7 |
| -}, true); |
8 |
| - |
9 |
| -// When @hotwired/turbo handles form submissions, send the CSRF token in a header in addition to a cookie |
10 |
| -// The `framework.csrf_protection.check_header` config option needs to be enabled for the header to be checked |
11 |
| -document.addEventListener('turbo:submit-start', function (event) { |
12 |
| - const h = generateCsrfHeaders(event.detail.formSubmission.formElement); |
13 |
| - Object.keys(h).map(function (k) { |
14 |
| - event.detail.formSubmission.fetchRequest.headers[k] = h[k]; |
15 |
| - }); |
16 |
| -}); |
17 |
| - |
18 |
| -// When @hotwired/turbo handles form submissions, remove the CSRF cookie once a form has been submitted |
19 |
| -document.addEventListener('turbo:submit-end', function (event) { |
20 |
| - removeCsrfToken(event.detail.formSubmission.formElement); |
21 |
| -}); |
| 2 | +const tokenCheck = /^[-_/+a-zA-Z0-9]{24,}$/; |
| 3 | + |
| 4 | +export function generateCsrfToken(formElement) { |
| 5 | + const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]'); |
| 6 | + |
| 7 | + if (!csrfField) { |
| 8 | + return; |
| 9 | + } |
| 10 | + |
| 11 | + let csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value'); |
| 12 | + let csrfToken = csrfField.value; |
| 13 | + |
| 14 | + if (!csrfCookie && nameCheck.test(csrfToken)) { |
| 15 | + csrfField.setAttribute('data-csrf-protection-cookie-value', (csrfCookie = csrfToken)); |
| 16 | + csrfToken = btoa( |
| 17 | + String.fromCharCode.apply(null, (window.crypto || window.msCrypto).getRandomValues(new Uint8Array(18))), |
| 18 | + ); |
| 19 | + csrfField.defaultValue = csrfToken; |
| 20 | + csrfField.dispatchEvent(new Event('change', { bubbles: true })); |
| 21 | + } |
| 22 | + |
| 23 | + if (csrfCookie && tokenCheck.test(csrfToken)) { |
| 24 | + const cookie = `${csrfCookie}_${csrfToken}=${csrfCookie}; path=/; samesite=strict`; |
| 25 | + document.cookie = window.location.protocol === 'https:' ? `__Host-${cookie}; secure` : cookie; |
| 26 | + } |
| 27 | +} |
22 | 28 |
|
23 |
| -export function generateCsrfToken (formElement) { |
24 |
| - const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]'); |
| 29 | +export function generateCsrfHeaders(formElement) { |
| 30 | + const headers = {}; |
| 31 | + const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]'); |
25 | 32 |
|
26 |
| - if (!csrfField) { |
27 |
| - return; |
28 |
| - } |
| 33 | + if (!csrfField) { |
| 34 | + return headers; |
| 35 | + } |
29 | 36 |
|
30 |
| - let csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value'); |
31 |
| - let csrfToken = csrfField.value; |
| 37 | + const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value'); |
32 | 38 |
|
33 |
| - if (!csrfCookie && nameCheck.test(csrfToken)) { |
34 |
| - csrfField.setAttribute('data-csrf-protection-cookie-value', csrfCookie = csrfToken); |
35 |
| - csrfField.defaultValue = csrfToken = btoa(String.fromCharCode.apply(null, (window.crypto || window.msCrypto).getRandomValues(new Uint8Array(18)))); |
36 |
| - csrfField.dispatchEvent(new Event('change', { bubbles: true })); |
37 |
| - } |
| 39 | + if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) { |
| 40 | + headers[csrfCookie] = csrfField.value; |
| 41 | + } |
38 | 42 |
|
39 |
| - if (csrfCookie && tokenCheck.test(csrfToken)) { |
40 |
| - const cookie = csrfCookie + '_' + csrfToken + '=' + csrfCookie + '; path=/; samesite=strict'; |
41 |
| - document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie; |
42 |
| - } |
| 43 | + return headers; |
43 | 44 | }
|
44 | 45 |
|
45 |
| -export function generateCsrfHeaders (formElement) { |
46 |
| - const headers = {}; |
47 |
| - const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]'); |
| 46 | +export function removeCsrfToken(formElement) { |
| 47 | + const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]'); |
48 | 48 |
|
49 |
| - if (!csrfField) { |
50 |
| - return headers; |
51 |
| - } |
| 49 | + if (!csrfField) { |
| 50 | + return; |
| 51 | + } |
52 | 52 |
|
53 |
| - const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value'); |
| 53 | + const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value'); |
54 | 54 |
|
55 |
| - if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) { |
56 |
| - headers[csrfCookie] = csrfField.value; |
57 |
| - } |
| 55 | + if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) { |
| 56 | + const cookie = `${csrfCookie}_${csrfField.value}=0; path=/; samesite=strict; max-age=0`; |
58 | 57 |
|
59 |
| - return headers; |
| 58 | + document.cookie = window.location.protocol === 'https:' ? `__Host-${cookie}; secure` : cookie; |
| 59 | + } |
60 | 60 | }
|
61 | 61 |
|
62 |
| -export function removeCsrfToken (formElement) { |
63 |
| - const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]'); |
64 |
| - |
65 |
| - if (!csrfField) { |
66 |
| - return; |
67 |
| - } |
68 |
| - |
69 |
| - const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value'); |
| 62 | +// Generate and double-submit a CSRF token in a form field and a cookie, as defined by Symfony's SameOriginCsrfTokenManager |
| 63 | +document.addEventListener( |
| 64 | + 'submit', |
| 65 | + (event) => { |
| 66 | + generateCsrfToken(event.target); |
| 67 | + }, |
| 68 | + true, |
| 69 | +); |
70 | 70 |
|
71 |
| - if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) { |
72 |
| - const cookie = csrfCookie + '_' + csrfField.value + '=0; path=/; samesite=strict; max-age=0'; |
| 71 | +// When @hotwired/turbo handles form submissions, send the CSRF token in a header in addition to a cookie |
| 72 | +// The `framework.csrf_protection.check_header` config option needs to be enabled for the header to be checked |
| 73 | +document.addEventListener('turbo:submit-start', (event) => { |
| 74 | + const h = generateCsrfHeaders(event.detail.formSubmission.formElement); |
| 75 | + // eslint-disable-next-line array-callback-return |
| 76 | + Object.keys(h).map((k) => { |
| 77 | + // eslint-disable-next-line no-param-reassign |
| 78 | + event.detail.formSubmission.fetchRequest.headers[k] = h[k]; |
| 79 | + }); |
| 80 | +}); |
73 | 81 |
|
74 |
| - document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie; |
75 |
| - } |
76 |
| -} |
| 82 | +// When @hotwired/turbo handles form submissions, remove the CSRF cookie once a form has been submitted |
| 83 | +document.addEventListener('turbo:submit-end', (event) => { |
| 84 | + removeCsrfToken(event.detail.formSubmission.formElement); |
| 85 | +}); |
77 | 86 |
|
78 | 87 | /* stimulusFetch: 'lazy' */
|
79 | 88 | export default 'csrf-protection-controller';
|
0 commit comments