diff --git a/components/FileUploader.vue b/components/FileUploader.vue index 4b3aaee22..37881cda0 100644 --- a/components/FileUploader.vue +++ b/components/FileUploader.vue @@ -67,7 +67,7 @@ export default { this.uploading = true; this.errorMessage = ''; try { - const ids = await $fetch(this.url, { + const ids = await this.$csrfFetch(this.url, { method: 'POST', body: formData, headers: { diff --git a/components/MfaConnection.vue b/components/MfaConnection.vue index afaaf05db..6e5fb4577 100644 --- a/components/MfaConnection.vue +++ b/components/MfaConnection.vue @@ -101,7 +101,7 @@ export default { async init() { this.requesting = true; try { - this.recoveryCodes = await $fetch('/api/mfa/init', { + this.recoveryCodes = await this.$csrfFetch('/api/mfa/init', { method: 'POST', body: { secret: this.secret.base32, diff --git a/components/MfaValidation.vue b/components/MfaValidation.vue index 583d19c87..722d0be32 100644 --- a/components/MfaValidation.vue +++ b/components/MfaValidation.vue @@ -87,7 +87,7 @@ export default { this.error = ''; this.saving = true; try { - const res = await $fetch('/api/mfa/validate', { + const res = await this.$csrfFetch('/api/mfa/validate', { method: 'POST', body: { code: this.recovery ? this.recoveryCode : this.code, diff --git a/composables/useDialogue.ts b/composables/useDialogue.ts index 3fe84f3b7..421792711 100644 --- a/composables/useDialogue.ts +++ b/composables/useDialogue.ts @@ -6,7 +6,7 @@ import type { DialogueMessage } from '../components/DialogueBox.vue'; import type { Color, ModalSize } from '../src/bootstrap.ts'; export default (to: string = 'body') => { - const { $translator: translator } = useNuxtApp(); + const { $csrfFetch, $translator: translator } = useNuxtApp(); const self = getCurrentInstance()!; @@ -83,10 +83,10 @@ export default (to: string = 'body') => { const postWithAlertOnError = async ( url: string, data: BodyInit | Record | null | undefined = undefined, - options: Omit = {}, + options: Omit, 'method' | 'data' | 'timeout'> = {}, timeout = 30000, ): Promise => { - return $fetch(url, { + return $csrfFetch(url, { method: 'POST', body: data, timeout, @@ -94,6 +94,10 @@ export default (to: string = 'body') => { }) .catch(async (error) => { let errorMessage = translator.translate('error.generic'); + if (error.data?.statusMessage === 'CSRF Token Mismatch') { + // access error message from csurf + error.data.error = error.data.message; + } if (typeof error.data?.error === 'string') { errorMessage = translator.translate(error.data?.error); // in case no translatable key was provided diff --git a/nuxt.config.ts b/nuxt.config.ts index 772742a36..cfe9389c3 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -103,7 +103,7 @@ export default defineNuxtConfig({ }, modules: [ '@pinia/nuxt', - '@privyid/nuxt-csrf', + 'nuxt-csurf', '@nuxtjs/plausible', '@vite-pwa/nuxt', '@nuxt/test-utils/module', @@ -254,6 +254,14 @@ exports.randomFillSync = randomFillSync`], handler: '~/server/index.ts', }, ], + routeRules: { + '/api/connect/apple/callback': { + csurf: false, + }, + '/api/sentry/tunnel': { + csurf: false, + }, + }, hooks: { 'pages:extend'(routes) { if (config.pronouns.enabled) { diff --git a/package.json b/package.json index a8deae20d..6a48ef9a4 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,6 @@ "@floating-ui/dom": "^1.6.3", "@nuxtjs/plausible": "^1.0.0", "@pinia/nuxt": "^0.5.1", - "@privyid/nuxt-csrf": "^1.0.0", "@sentry/browser": "^7.109.0", "@sentry/cli": "^2.31.0", "@sentry/node": "^7.109.0", @@ -63,6 +62,7 @@ "node-fetch": "^2.6.12", "nodemailer": "^6.7.8", "nuxt": "^3.13.1", + "nuxt-csurf": "^1.6.1", "pageres": "^6.3.1", "papaparse": "^5.4.1", "plausible-api": "https://github.com/avo7/plausible-api.git#main", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c4f74e439..e3309113b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,9 +20,6 @@ dependencies: '@pinia/nuxt': specifier: ^0.5.1 version: 0.5.4(rollup@4.21.0)(typescript@5.5.2)(vue@3.5.3) - '@privyid/nuxt-csrf': - specifier: ^1.0.0 - version: 1.0.0 '@sentry/browser': specifier: ^7.109.0 version: 7.109.0 @@ -149,6 +146,9 @@ dependencies: nuxt: specifier: ^3.13.1 version: 3.13.1(@types/node@20.11.5)(eslint@8.57.0)(rollup@4.21.0)(sass@1.32.12)(typescript@5.5.2)(vite@5.4.3)(vue-tsc@2.1.6) + nuxt-csurf: + specifier: ^1.6.1 + version: 1.6.2(rollup@4.21.0) pageres: specifier: ^6.3.1 version: 6.3.1 @@ -1237,8 +1237,8 @@ packages: resolution: {integrity: sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/traverse': 7.25.3 - '@babel/types': 7.25.2 + '@babel/traverse': 7.25.6 + '@babel/types': 7.25.6 transitivePeerDependencies: - supports-color dev: false @@ -1513,7 +1513,7 @@ packages: dependencies: '@babel/template': 7.25.0 '@babel/traverse': 7.25.6 - '@babel/types': 7.25.2 + '@babel/types': 7.25.6 transitivePeerDependencies: - supports-color dev: false @@ -1586,7 +1586,7 @@ packages: dependencies: '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.8 - '@babel/traverse': 7.25.3 + '@babel/traverse': 7.25.6 transitivePeerDependencies: - supports-color dev: false @@ -1633,7 +1633,7 @@ packages: dependencies: '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.8 - '@babel/traverse': 7.25.3 + '@babel/traverse': 7.25.6 transitivePeerDependencies: - supports-color dev: false @@ -1948,7 +1948,7 @@ packages: '@babel/core': ^7.12.0 dependencies: '@babel/core': 7.25.2 - '@babel/helper-create-class-features-plugin': 7.25.0(@babel/core@7.25.2) + '@babel/helper-create-class-features-plugin': 7.25.4(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.24.8 '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.25.2) transitivePeerDependencies: @@ -2082,7 +2082,7 @@ packages: '@babel/core': 7.25.2 '@babel/helper-compilation-targets': 7.25.2 '@babel/helper-plugin-utils': 7.24.8 - '@babel/traverse': 7.25.3 + '@babel/traverse': 7.25.6 transitivePeerDependencies: - supports-color dev: false @@ -2166,7 +2166,7 @@ packages: '@babel/helper-module-transforms': 7.25.2(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.24.8 '@babel/helper-validator-identifier': 7.24.7 - '@babel/traverse': 7.25.3 + '@babel/traverse': 7.25.6 transitivePeerDependencies: - supports-color dev: false @@ -2309,7 +2309,7 @@ packages: dependencies: '@babel/core': 7.25.2 '@babel/helper-annotate-as-pure': 7.24.7 - '@babel/helper-create-class-features-plugin': 7.25.0(@babel/core@7.25.2) + '@babel/helper-create-class-features-plugin': 7.25.4(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.24.8 '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.25.2) transitivePeerDependencies: @@ -2560,7 +2560,7 @@ packages: dependencies: '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.8 - '@babel/types': 7.25.2 + '@babel/types': 7.25.6 esutils: 2.0.3 dev: false @@ -4191,13 +4191,6 @@ packages: resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} dev: true - /@privyid/nuxt-csrf@1.0.0: - resolution: {integrity: sha512-Ql9iUYM6W77if5DEijFJQSsgRGA/RisJmz5LUY4sUz4M+99A6wVY30lW9z43Ej+QnUQjxQt/5e/JR0xmgShDFg==} - dependencies: - cookie-parser: 1.4.6 - csurf: 1.11.0 - dev: false - /@rollup/plugin-alias@5.1.0(rollup@4.21.0): resolution: {integrity: sha512-lpA3RZ9PdIG7qqhEfv79tBffNaoDuukFDrmhLqg9ifv99u/ehn+lOg30x2zmhf8AQqQUZaMk/B9fZraQ6/acDQ==} engines: {node: '>=14.0.0'} @@ -7855,11 +7848,6 @@ packages: dev: false optional: true - /cookie@0.4.0: - resolution: {integrity: sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==} - engines: {node: '>= 0.6'} - dev: false - /cookie@0.4.1: resolution: {integrity: sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==} engines: {node: '>= 0.6'} @@ -8028,15 +8016,6 @@ packages: engines: {node: '>=8'} dev: false - /csrf@3.1.0: - resolution: {integrity: sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==} - engines: {node: '>= 0.8'} - dependencies: - rndm: 1.2.0 - tsscmp: 1.0.6 - uid-safe: 2.1.5 - dev: false - /css-declaration-sorter@7.2.0(postcss@8.4.45): resolution: {integrity: sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==} engines: {node: ^14 || ^16 || >=18} @@ -8176,17 +8155,6 @@ packages: /csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - /csurf@1.11.0: - resolution: {integrity: sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==} - engines: {node: '>= 0.8.0'} - deprecated: Please use another csrf package - dependencies: - cookie: 0.4.0 - cookie-signature: 1.0.6 - csrf: 3.1.0 - http-errors: 1.7.3 - dev: false - /cuint@0.2.2: resolution: {integrity: sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==} dev: true @@ -8441,11 +8409,6 @@ packages: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} - /depd@1.1.2: - resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} - engines: {node: '>= 0.6'} - dev: false - /depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -10539,17 +10502,6 @@ packages: dev: false optional: true - /http-errors@1.7.3: - resolution: {integrity: sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==} - engines: {node: '>= 0.6'} - dependencies: - depd: 1.1.2 - inherits: 2.0.4 - setprototypeof: 1.1.1 - statuses: 1.5.0 - toidentifier: 1.0.0 - dev: false - /http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -10775,7 +10727,7 @@ packages: engines: {node: '>= 0.4'} dependencies: es-errors: 1.3.0 - hasown: 2.0.0 + hasown: 2.0.2 side-channel: 1.0.6 dev: false @@ -12799,6 +12751,19 @@ packages: fsevents: 2.3.3 dev: false + /nuxt-csurf@1.6.2(rollup@4.21.0): + resolution: {integrity: sha512-GksV+0DrnEYzQGAPb/BzLgulITqQNi3p5LUOW+zIf2GqdshW7MKoS/EBeVLDBfeT0P0a0OvewzaEGt1/OGVtAQ==} + dependencies: + '@nuxt/kit': 3.13.1(magicast@0.3.4)(rollup@4.21.0) + defu: 6.1.4 + uncsrf: 1.1.1 + transitivePeerDependencies: + - magicast + - rollup + - supports-color + - webpack-sources + dev: false + /nuxt@3.13.1(@types/node@20.11.5)(eslint@8.57.0)(rollup@4.21.0)(sass@1.32.12)(typescript@5.5.2)(vite@5.4.3)(vue-tsc@2.1.6): resolution: {integrity: sha512-En0vVrCJWu54ptShUlrqOGzXTcjhX+RnHShwdcpNqL9kmE9FWqeDYnPTgt2gJWrYSvVbmjJcVfEugNo9XpNmHA==} engines: {node: ^14.18.0 || >=16.10.0} @@ -14450,10 +14415,6 @@ packages: inherits: 2.0.4 dev: true - /rndm@1.2.0: - resolution: {integrity: sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw==} - dev: false - /rollup-plugin-visualizer@5.12.0(rollup@4.21.0): resolution: {integrity: sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==} engines: {node: '>=14'} @@ -14748,10 +14709,6 @@ packages: resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} dev: true - /setprototypeof@1.1.1: - resolution: {integrity: sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==} - dev: false - /setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -15145,11 +15102,6 @@ packages: object-copy: 0.1.0 dev: true - /statuses@1.5.0: - resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} - engines: {node: '>= 0.6'} - dev: false - /statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -15729,11 +15681,6 @@ packages: safe-regex: 1.1.0 dev: true - /toidentifier@1.0.0: - resolution: {integrity: sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==} - engines: {node: '>=0.6'} - dev: false - /toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} @@ -15892,11 +15839,6 @@ packages: /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - /tsscmp@1.0.6: - resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} - engines: {node: '>=0.6.x'} - dev: false - /tty-browserify@0.0.0: resolution: {integrity: sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw==} dev: true @@ -16086,6 +16028,10 @@ packages: /uncrypto@0.1.3: resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + /uncsrf@1.1.1: + resolution: {integrity: sha512-wH9+N3oNdr2XB3egUKxCR26XTVfYywv2STZYtZLIp8votOf0/9b0axA9gpIHdYfgq/Myfh/HS5e0lQ/+27dj9A==} + dev: false + /unctx@2.3.1: resolution: {integrity: sha512-PhKke8ZYauiqh3FEMVNm7ljvzQiph0Mt3GBRve03IJm7ukfaON2OBK795tLwhbyfzknuRRkW0+Ze+CQUmzOZ+A==} dependencies: diff --git a/routes/admin.vue b/routes/admin.vue index acbf5f896..8bce1c2d9 100644 --- a/routes/admin.vue +++ b/routes/admin.vue @@ -293,7 +293,7 @@ export default { }, watch: { async adminNotifications() { - const res = await $fetch('/api/admin/set-notification-frequency', { + const res = await this.$csrfFetch('/api/admin/set-notification-frequency', { method: 'POST', body: { frequency: parseInt(this.adminNotifications) }, }); diff --git a/routes/adminUsers.vue b/routes/adminUsers.vue index d1d7ac735..9cfc72c8f 100644 --- a/routes/adminUsers.vue +++ b/routes/adminUsers.vue @@ -212,7 +212,7 @@ export default { 'an explicit GPDR request directly from the user, ' + 'or having proof that owner is not yet 13 years old.', 'danger'); - if (await $fetch(`/api/user/data-erasure/${id}`, { method: 'POST' })) { + if (await this.$csrfFetch(`/api/user/data-erasure/${id}`, { method: 'POST' })) { await this.dialogue.alert(`Account ${email} removed successfully`, 'success'); } else { await this.dialogue.alert(this.$t('error.generic', 'danger')); diff --git a/routes/profile.vue b/routes/profile.vue index 1771492f1..252b0926f 100644 --- a/routes/profile.vue +++ b/routes/profile.vue @@ -405,7 +405,7 @@ export default defineComponent({ }, methods: { async generateCard(dark) { - await $fetch(`/api/profile/request-card?dark=${dark ? '1' : '0'}`, { method: 'POST' }); + await this.$csrfFetch(`/api/profile/request-card?dark=${dark ? '1' : '0'}`, { method: 'POST' }); this.profile[dark ? 'cardDark' : 'card'] = ''; this.startCheckingForCard(); }, diff --git a/server/index.ts b/server/index.ts index caebdd640..a6417f010 100644 --- a/server/index.ts +++ b/server/index.ts @@ -57,8 +57,6 @@ router.use(session({ checkPeriod: 86400000, // 24h }), })); -// router.use(csurf({ cookie: true })); -// router.use(csrfHandleError()) export class LazyDatabase implements Database { db: Database | null;