From 6f1254f6886f548e6d956a8fe653463676d5e720 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Wed, 14 Jun 2023 14:26:33 -0600 Subject: [PATCH] Add CSRF protection with remix-utils --- README.md | 65 +++++------------ app/root.tsx | 18 ++++- app/routes/settings+/profile.tsx | 24 +++++++ package-lock.json | 115 ++++++++++++++++++++++++++++++- package.json | 1 + 5 files changed, 171 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 9a0618a..e5c29fb 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,17 @@ -
-

The Epic Stack 🚀

- - Ditch analysis paralysis and start shipping Epic Web apps. - -

- This is an opinionated project starter and reference that allows teams to - ship their ideas to production faster and on a more stable foundation based - on the experience of Kent C. Dodds and - contributors. -

-
- -```sh -npx create-remix@latest --typescript --install --template epicweb-dev/epic-stack -``` - -[![The Epic Stack](https://github.com/epicweb-dev/epic-stack/assets/1500684/345a3947-54ad-481d-888a-dbc1d1f313c1)](https://www.epicweb.dev/epic-stack) - -[The Epic Stack](https://www.epicweb.dev/epic-stack) - -
- -## Watch Kent's Introduction to The Epic Stack - -[![screenshot of a YouTube video](https://github.com/epicweb-dev/epic-stack/assets/1500684/6beafa78-41c6-47e1-b999-08d3d3e5cb57)](https://www.youtube.com/watch?v=yMK5SVRASxM) - -["The Epic Stack" by Kent C. Dodds at #RemixConf 2023 💿](https://www.youtube.com/watch?v=yMK5SVRASxM) - -## Docs - -[Read the docs](https://github.com/epicweb-dev/epic-stack/blob/main/docs) -(please 🙏). - -## Support - -- 🆘 Join the - [discussion on GitHub](https://github.com/epicweb-dev/epic-stack/discussions) - and the [KCD Community on Discord](https://kcd.im/discord). -- 💡 Create an - [idea discussion](https://github.com/epicweb-dev/epic-stack/discussions/new?category=ideas) - for suggestions. -- 🐛 Open a [GitHub issue](https://github.com/epicweb-dev/epic-stack/issues) to - report a bug. - -## Thanks - -You rock 🪨 +# Epic Stack with CSRF Protection + +This is an example of how to integrate the +[`remix-utils`](https://github.com/sergiodxa/remix-utils) package utilities for +[Cross-Site Request Forgery (CSRF)](https://en.wikipedia.org/wiki/Cross-site_request_forgery) +protection with the Epic Stack. The easiest way to explore the example is to +pull up +[the commit history](https://github.com/kentcdodds/epic-stack-with-csrf/commits/main). + +Following the steps laid out in the Remix Utils docs is sufficient for this: + +1. Install `remix-utils` +2. Generate the authenticity token in the `root.tsx` loader (be certain to + commit the session to set the cookie) +3. Wrap the App in the `` and provide the token +4. Render a Form with the `` component +5. Verify in the Action using `verifyAuthenticityToken` and the session. diff --git a/app/root.tsx b/app/root.tsx index 05973ee..e5f395a 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -34,6 +34,8 @@ import { getUserImgSrc } from './utils/misc.ts' import { useNonce } from './utils/nonce-provider.ts' import { makeTimings, time } from './utils/timing.server.ts' import { useOptionalUser, useUser } from './utils/user.ts' +import { commitSession, getSession } from './utils/session.server.ts' +import { AuthenticityTokenProvider, createAuthenticityToken } from 'remix-utils' export const links: LinksFunction = () => { return [ @@ -74,6 +76,8 @@ export const meta: V2_MetaFunction = () => { } export async function loader({ request }: DataFunctionArgs) { + const cookieSession = await getSession(request.headers.get('Cookie')) + const token = createAuthenticityToken(cookieSession) const timings = makeTimings('root loader') const userId = await time(() => getUserId(request), { timings, @@ -100,6 +104,7 @@ export async function loader({ request }: DataFunctionArgs) { return json( { + csrf: token, user, requestInfo: { hints: getHints(request), @@ -114,6 +119,7 @@ export async function loader({ request }: DataFunctionArgs) { { headers: { 'Server-Timing': timings.toString(), + 'Set-Cookie': await commitSession(cookieSession), }, }, ) @@ -185,7 +191,17 @@ function App() { ) } -export default withSentry(App) + +function AppWithCSRF() { + const data = useLoaderData() + return ( + + + + ) +} + +export default withSentry(AppWithCSRF) function UserDropdown() { const user = useUser() diff --git a/app/routes/settings+/profile.tsx b/app/routes/settings+/profile.tsx index c4e8299..3767001 100644 --- a/app/routes/settings+/profile.tsx +++ b/app/routes/settings+/profile.tsx @@ -27,6 +27,9 @@ import { usernameSchema, } from '~/utils/user-validation.ts' import { twoFAVerificationType } from './profile.two-factor.tsx' +import { AuthenticityTokenInput, verifyAuthenticityToken } from 'remix-utils' +import { getSession } from '~/utils/session.server.ts' +import { GeneralErrorBoundary } from '~/components/error-boundary.tsx' const profileFormSchema = z.object({ name: nameSchema.optional(), @@ -61,6 +64,8 @@ export async function loader({ request }: DataFunctionArgs) { } export async function action({ request }: DataFunctionArgs) { + let session = await getSession(request.headers.get('Cookie')) + await verifyAuthenticityToken(request, session) const userId = await requireUserId(request) const formData = await request.formData() const submission = await parse(formData, { @@ -180,6 +185,7 @@ export default function EditUserProfile() {
+
) } + +export function ErrorBoundary() { + return ( + ( +

+ The form was submitted improperly. Please{' '} + + try again + {' '} + and be sure to not modify the form submission data. +

+ ), + }} + /> + ) +} diff --git a/package-lock.json b/package-lock.json index c83f53b..b7232e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,11 @@ { - "name": "epic-stack-template", + "name": "epic-stack-with-csrf-8c12", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "epic-stack-template", + "name": "epic-stack-with-csrf-8c12", "hasInstallScript": true, - "license": "MIT", "dependencies": { "@conform-to/react": "^0.7.0-pre.2", "@conform-to/zod": "^0.7.0-pre.2", @@ -45,6 +44,7 @@ "react-dom": "^18.2.0", "remix-auth": "^3.4.0", "remix-auth-form": "^1.3.0", + "remix-utils": "^6.4.1", "tailwind-merge": "^1.13.1", "thirty-two": "^1.0.2", "tiny-invariant": "^1.3.1", @@ -10804,6 +10804,14 @@ "node": ">= 0.4" } }, + "node_modules/intl-parse-accept-language": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/intl-parse-accept-language/-/intl-parse-accept-language-1.0.0.tgz", + "integrity": "sha512-YFMSV91JNBOSjw1cOfw2tup6hDP7mkz+2AUV7W1L1AM6ntgI75qC1ZeFpjPGMrWp+upmBRTX2fJWQ8c7jsUWpA==", + "engines": { + "node": ">=14" + } + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -10817,6 +10825,14 @@ "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==" }, + "node_modules/ip-regex": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", + "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", + "engines": { + "node": ">=8" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -11091,6 +11107,17 @@ "node": ">=8" } }, + "node_modules/is-ip": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-3.1.0.tgz", + "integrity": "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==", + "dependencies": { + "ip-regex": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-map": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", @@ -15838,6 +15865,38 @@ "node": ">=10" } }, + "node_modules/remix-utils": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/remix-utils/-/remix-utils-6.4.1.tgz", + "integrity": "sha512-qIm4bLArf7Np7rhCyI5vRT2dToNIQjModkJyiri6ydflnz6CkRHf4FokbtW187F4tqmXe/0oNyZ7rhaxY7VP8Q==", + "dependencies": { + "intl-parse-accept-language": "^1.0.0", + "is-ip": "^3.1.0", + "schema-dts": "^1.1.0", + "type-fest": "^2.5.2", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@remix-run/react": "^1.10.0", + "@remix-run/server-runtime": "^1.10.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "zod": "^3.19.1" + } + }, + "node_modules/remix-utils/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -16166,6 +16225,14 @@ "loose-envify": "^1.1.0" } }, + "node_modules/schema-dts": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/schema-dts/-/schema-dts-1.1.2.tgz", + "integrity": "sha512-MpNwH0dZJHinVxk9bT8XUdjKTxMYrA5bLtrrGmFA6PTLwlOKnhi67XoRd6/ty+Djt6ZC0slR57qFhZDNMI6DhQ==", + "peerDependencies": { + "typescript": ">=4.1.0" + } + }, "node_modules/semver": { "version": "7.3.8", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", @@ -26260,6 +26327,11 @@ "side-channel": "^1.0.4" } }, + "intl-parse-accept-language": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/intl-parse-accept-language/-/intl-parse-accept-language-1.0.0.tgz", + "integrity": "sha512-YFMSV91JNBOSjw1cOfw2tup6hDP7mkz+2AUV7W1L1AM6ntgI75qC1ZeFpjPGMrWp+upmBRTX2fJWQ8c7jsUWpA==" + }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -26273,6 +26345,11 @@ "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==" }, + "ip-regex": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", + "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==" + }, "ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -26436,6 +26513,14 @@ "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==" }, + "is-ip": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-3.1.0.tgz", + "integrity": "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==", + "requires": { + "ip-regex": "^4.0.0" + } + }, "is-map": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", @@ -29719,6 +29804,25 @@ } } }, + "remix-utils": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/remix-utils/-/remix-utils-6.4.1.tgz", + "integrity": "sha512-qIm4bLArf7Np7rhCyI5vRT2dToNIQjModkJyiri6ydflnz6CkRHf4FokbtW187F4tqmXe/0oNyZ7rhaxY7VP8Q==", + "requires": { + "intl-parse-accept-language": "^1.0.0", + "is-ip": "^3.1.0", + "schema-dts": "^1.1.0", + "type-fest": "^2.5.2", + "uuid": "^8.3.2" + }, + "dependencies": { + "type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==" + } + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -29948,6 +30052,11 @@ "loose-envify": "^1.1.0" } }, + "schema-dts": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/schema-dts/-/schema-dts-1.1.2.tgz", + "integrity": "sha512-MpNwH0dZJHinVxk9bT8XUdjKTxMYrA5bLtrrGmFA6PTLwlOKnhi67XoRd6/ty+Djt6ZC0slR57qFhZDNMI6DhQ==" + }, "semver": { "version": "7.3.8", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", diff --git a/package.json b/package.json index 40926cb..65ac05e 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "react-dom": "^18.2.0", "remix-auth": "^3.4.0", "remix-auth-form": "^1.3.0", + "remix-utils": "^6.4.1", "tailwind-merge": "^1.13.1", "thirty-two": "^1.0.2", "tiny-invariant": "^1.3.1",