From e6a632cdf3954d039f1bb904ced236299c2cef25 Mon Sep 17 00:00:00 2001 From: alina sireneva Date: Sat, 3 Aug 2024 09:18:52 +0300 Subject: [PATCH] fix: better csrf handling --- astro.config.mjs | 3 -- src/backend/env.ts | 1 + src/backend/utils/csrf.ts | 40 +++++++++++++++++++ .../pages/PageMain/Shoutbox/Shoutbox.astro | 9 ++++- .../pages/PageMain/Shoutbox/Shoutbox.tsx | 9 +++-- src/pages/api/shoutbox.ts | 19 ++++++++- 6 files changed, 71 insertions(+), 10 deletions(-) create mode 100644 src/backend/utils/csrf.ts diff --git a/astro.config.mjs b/astro.config.mjs index 3073839..a518f8c 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -17,9 +17,6 @@ export default defineConfig({ adapter: node({ mode: 'standalone', }), - security: { - checkOrigin: true, - }, server: { host: true, }, diff --git a/src/backend/env.ts b/src/backend/env.ts index 33e2b89..f988b6b 100644 --- a/src/backend/env.ts +++ b/src/backend/env.ts @@ -18,6 +18,7 @@ export const env = zodValidateSync( FAKE_DEEPL_SECRET: z.string(), MK_WEBHOOK_SECRET: z.string(), QBT_WEBHOOK_SECRET: z.string(), + CSRF_SECRET: z.string(), }), process.env, ) diff --git a/src/backend/utils/csrf.ts b/src/backend/utils/csrf.ts new file mode 100644 index 0000000..fcca069 --- /dev/null +++ b/src/backend/utils/csrf.ts @@ -0,0 +1,40 @@ +import { createHmac, randomBytes } from 'node:crypto' + +import { env } from '~/backend/env' + +const secret = env.CSRF_SECRET +const validity = 300_000 + +export function getCsrfToken(ip: string) { + const data = Buffer.from(JSON.stringify([Date.now(), ip])) + const salt = randomBytes(8) + const sign = createHmac('sha256', secret).update(data).update(salt).digest() + + return Buffer.concat([ + data, + salt, + sign.subarray(0, 8), + ]).toString('base64url') +} + +export function verifyCsrfToken(ip: string, token: string) { + try { + const buf = Buffer.from(token, 'base64url') + if (buf.length < 16) return false + + const saltedData = buf.subarray(0, -8) + const correctSign = createHmac('sha256', secret).update(saltedData).digest() + + if (Buffer.compare(correctSign.subarray(0, 8), buf.subarray(-8)) !== 0) { + return false + } + + const [issued, correctIp] = JSON.parse(buf.subarray(0, -16).toString()) + if (issued + validity < Date.now()) return false + if (ip !== correctIp) return false + + return true + } catch { + return false + } +} diff --git a/src/components/pages/PageMain/Shoutbox/Shoutbox.astro b/src/components/pages/PageMain/Shoutbox/Shoutbox.astro index 6e0faee..043ef3e 100644 --- a/src/components/pages/PageMain/Shoutbox/Shoutbox.astro +++ b/src/components/pages/PageMain/Shoutbox/Shoutbox.astro @@ -1,6 +1,7 @@ --- import { fetchShouts } from '~/backend/service/shoutbox' import { getRequestIp } from '~/backend/utils/request' +import { getCsrfToken } from '~/backend/utils/csrf' import { Shoutbox as ShoutboxSolid } from './Shoutbox' @@ -8,11 +9,17 @@ const url = new URL(Astro.request.url) let page = Number(url.searchParams.get('shouts_page')) if (Number.isNaN(page)) page = 0 -const data = fetchShouts(page, getRequestIp(Astro)) +const shoutError = url.searchParams.get('shout_error') ?? undefined + +const ip = getRequestIp(Astro) +const data = fetchShouts(page, ip) +const csrf = getCsrfToken(ip) --- diff --git a/src/components/pages/PageMain/Shoutbox/Shoutbox.tsx b/src/components/pages/PageMain/Shoutbox/Shoutbox.tsx index fe58db0..3a35f33 100644 --- a/src/components/pages/PageMain/Shoutbox/Shoutbox.tsx +++ b/src/components/pages/PageMain/Shoutbox/Shoutbox.tsx @@ -24,6 +24,8 @@ async function fetchShouts(page: number): Promise { function ShoutboxInner(props: { initPage: number initPageData: ShoutsData + shoutError?: string + csrf: string }) { // eslint-disable-next-line solid/reactivity const [page, setPage] = createSignal(props.initPage) @@ -89,7 +91,7 @@ function ShoutboxInner(props: { 'Content-Type': 'application/json', }, body: JSON.stringify({ - // _csrf: shouts.data?.csrf, + _csrf: props.csrf, message: (form.elements.namedItem('message') as HTMLInputElement).value, private: isPrivate ? '' : undefined, }), @@ -124,7 +126,7 @@ function ShoutboxInner(props: {
- {/* */} +