diff --git a/.env.example b/.env.example index d117323c2..46659864a 100644 --- a/.env.example +++ b/.env.example @@ -3,6 +3,7 @@ DATABASE_PATH="./prisma/data.db" DATABASE_URL="file:./data.db?connection_limit=1" CACHE_DATABASE_PATH="./other/cache.db" SESSION_SECRET="super-duper-s3cret" +HONEYPOT_SECRET="super-duper-s3cret" INTERNAL_COMMAND_TOKEN="some-made-up-token" RESEND_API_KEY="re_blAh_blaHBlaHblahBLAhBlAh" SENTRY_DSN="your-dsn" diff --git a/app/root.tsx b/app/root.tsx index edede0c33..46e83bf27 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -25,6 +25,8 @@ import { } from '@remix-run/react' import { withSentry } from '@sentry/remix' import { useRef } from 'react' +import { AuthenticityTokenProvider } from 'remix-utils/csrf/react' +import { HoneypotProvider } from 'remix-utils/honeypot/react' import { z } from 'zod' import { Confetti } from './components/confetti.tsx' import { GeneralErrorBoundary } from './components/error-boundary.tsx' @@ -46,8 +48,10 @@ import tailwindStyleSheetUrl from './styles/tailwind.css' import { authenticator, getUserId } from './utils/auth.server.ts' import { ClientHintCheck, getHints, useHints } from './utils/client-hints.tsx' import { getConfetti } from './utils/confetti.server.ts' +import { csrf } from './utils/csrf.server.ts' import { prisma } from './utils/db.server.ts' import { getEnv } from './utils/env.server.ts' +import { honeypot } from './utils/honeypot.server.ts' import { combineHeaders, getDomainUrl, getUserImgSrc } from './utils/misc.tsx' import { useNonce } from './utils/nonce-provider.ts' import { useRequestInfo } from './utils/request-info.ts' @@ -130,6 +134,8 @@ export async function loader({ request }: DataFunctionArgs) { } const { toast, headers: toastHeaders } = await getToast(request) const { confettiId, headers: confettiHeaders } = getConfetti(request) + const honeyProps = honeypot.getInputProps() + const [csrfToken, csrfCookieHeader] = await csrf.commitToken() return json( { @@ -145,12 +151,15 @@ export async function loader({ request }: DataFunctionArgs) { ENV: getEnv(), toast, confettiId, + honeyProps, + csrfToken, }, { headers: combineHeaders( { 'Server-Timing': timings.toString() }, toastHeaders, confettiHeaders, + csrfCookieHeader ? { 'set-cookie': csrfCookieHeader } : null, ), }, ) @@ -276,7 +285,19 @@ function App() { ) } -export default withSentry(App) + +function AppWithProviders() { + const data = useLoaderData() + return ( + + + + + + ) +} + +export default withSentry(AppWithProviders) function UserDropdown() { const user = useUser() diff --git a/app/routes/_auth+/forgot-password.tsx b/app/routes/_auth+/forgot-password.tsx index 82ad1aa21..85e939a4c 100644 --- a/app/routes/_auth+/forgot-password.tsx +++ b/app/routes/_auth+/forgot-password.tsx @@ -8,12 +8,16 @@ import { type MetaFunction, } from '@remix-run/node' import { Link, useFetcher } from '@remix-run/react' +import { AuthenticityTokenInput } from 'remix-utils/csrf/react' +import { HoneypotInputs } from 'remix-utils/honeypot/react' import { z } from 'zod' import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx' import { ErrorList, Field } from '#app/components/forms.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' +import { validateCSRF } from '#app/utils/csrf.server.ts' import { prisma } from '#app/utils/db.server.ts' import { sendEmail } from '#app/utils/email.server.ts' +import { checkHoneypot } from '#app/utils/honeypot.server.ts' import { EmailSchema, UsernameSchema } from '#app/utils/user-validation.ts' import { prepareVerification } from './verify.tsx' @@ -23,6 +27,8 @@ const ForgotPasswordSchema = z.object({ export async function action({ request }: DataFunctionArgs) { const formData = await request.formData() + await validateCSRF(formData, request.headers) + checkHoneypot(formData) const submission = await parse(formData, { schema: ForgotPasswordSchema.superRefine(async (data, ctx) => { const user = await prisma.user.findFirst({ @@ -136,6 +142,8 @@ export default function ForgotPasswordRoute() {
+ +
LoginFormSchema.transform(async (data, ctx) => { @@ -266,6 +272,8 @@ export default function LoginPage() {
+ + SignupFormSchema.superRefine(async (data, ctx) => { @@ -165,6 +171,8 @@ export default function SignupRoute() { className="mx-auto min-w-[368px] max-w-sm" {...form.props} > + + { const existingUser = await prisma.user.findUnique({ @@ -131,6 +139,8 @@ export default function SignupRoute() {
+ +
+ + { const existingUser = await prisma.user.findUnique({ @@ -225,6 +228,7 @@ export default function ChangeEmailIndex() {

+ +
{otherSessionsCount ? ( + + + { @@ -151,6 +154,7 @@ export default function PhotoRoute() { onReset={() => setNewImageSrc(null)} {...form.props} > + - + +

Disabling two factor authentication is not recommended. However, if you would like to do so, click here: diff --git a/app/routes/settings+/profile.two-factor.index.tsx b/app/routes/settings+/profile.two-factor.index.tsx index c8e90b42b..46c8f74b5 100644 --- a/app/routes/settings+/profile.two-factor.index.tsx +++ b/app/routes/settings+/profile.two-factor.index.tsx @@ -2,9 +2,11 @@ import { generateTOTP } from '@epic-web/totp' import { type SEOHandle } from '@nasa-gcn/remix-seo' import { json, redirect, type DataFunctionArgs } from '@remix-run/node' import { Link, useFetcher, useLoaderData } from '@remix-run/react' +import { AuthenticityTokenInput } from 'remix-utils/csrf/react' import { Icon } from '#app/components/ui/icon.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' import { requireUserId } from '#app/utils/auth.server.ts' +import { validateCSRF } from '#app/utils/csrf.server.ts' import { prisma } from '#app/utils/db.server.ts' import { twoFAVerificationType } from './profile.two-factor.tsx' import { twoFAVerifyVerificationType } from './profile.two-factor.verify.tsx' @@ -24,6 +26,7 @@ export async function loader({ request }: DataFunctionArgs) { export async function action({ request }: DataFunctionArgs) { const userId = await requireUserId(request) + await validateCSRF(await request.formData(), request.headers) const { otp: _otp, ...config } = generateTOTP() const verificationData = { ...config, @@ -73,7 +76,8 @@ export default function TwoFactorRoute() { {' '} to log in.

- + + @@ -172,6 +175,7 @@ export default function TwoFactorRoute() {

+ { @@ -209,6 +212,7 @@ export function NoteEditor({ {...form.props} encType="multipart/form-data" > + {/* This hidden submit button is here to ensure that when the user hits "enter" on an input field, the primary form function is submitted @@ -241,7 +245,7 @@ export function NoteEditor({ className="relative border-b-2 border-muted-foreground" >