more csrf protection (i'm paranoid)

This commit is contained in:
Joonas 2023-01-21 02:37:32 +02:00
parent 60f1beff17
commit 7b861454ff
7 changed files with 117 additions and 16 deletions

View File

@ -5,11 +5,16 @@ import {
Outlet,
Scripts,
ScrollRestoration,
useLoaderData,
} from "@remix-run/react";
import { useState } from "react";
import {
AuthenticityTokenProvider,
createAuthenticityToken,
} from "remix-utils";
import styles from "./styles/app.css";
import { json } from "@remix-run/node";
import { getSession, commitSession } from "./sessions";
export function links() {
return [{ rel: "stylesheet", href: styles }];
}
@ -20,7 +25,17 @@ export const meta = () => ({
viewport: "width=device-width,initial-scale=1",
});
export async function loader({ request }) {
let session = await getSession(request.headers.get("cookie"));
let token = createAuthenticityToken(session);
return json(
{ csrf: token },
{ headers: { "Set-Cookie": await commitSession(session) } }
);
}
export default function App() {
let { csrf } = useLoaderData();
const [dark, setDark] = useState(true);
return (
@ -53,7 +68,9 @@ export default function App() {
</svg>
)}
</button>
<Outlet />
<AuthenticityTokenProvider token={csrf}>
<Outlet />
</AuthenticityTokenProvider>
<ScrollRestoration />
<Scripts />
<LiveReload />

View File

@ -5,6 +5,7 @@ import { prisma } from "~/db.server";
import { Form } from "@remix-run/react";
import { json } from "@remix-run/node";
import { County } from "~/components/County";
import { AuthenticityTokenInput, verifyAuthenticityToken } from "remix-utils";
const countys = [
"Ahvenanmaa",
@ -28,13 +29,19 @@ const countys = [
];
export async function action({ request }) {
const session = await getSession(request.headers.get("Cookie"));
try {
await verifyAuthenticityToken(request, session);
} catch {
throw new Error("something went wrong");
}
const formData = await request.formData();
const county = formData.get("county");
if (!county || typeof county !== "string" || !countys.includes(county))
throw new Error("bad county");
const session = await getSession(request.headers.get("Cookie"));
if (!session.has("userId"))
throw new Error("OAuth token not found in cookie");
@ -149,25 +156,29 @@ export default function Index() {
/>
<h1>{data.me.username}</h1>
{data.selfData ? (
<Link
<Form
method="post"
className="text-sky-400 hover:text-sky-600 hover:underline"
to={"/update"}
action="/update"
>
Update stats
</Link>
<AuthenticityTokenInput />
<button type="submit">Update stats</button>
</Form>
) : (
""
)}
<Link
<Form
method="post"
className="text-sky-400 hover:text-sky-600 hover:underline"
to={"/logout"}
action="/logout"
>
Logout
</Link>
<AuthenticityTokenInput />
<button type="submit">Logout</button>
</Form>
</div>
) : (
<a
href="https://osu.ppy.sh/oauth/authorize?client_id=20062&redirect_uri=http://localhost:3000/auth&response_type=code"
href="https://osu.ppy.sh/oauth/authorize?client_id=20062&redirect_uri=http://localhost:3000/auth&response_type=code&state=dsadfwoefwpkf"
referrerPolicy="no-referrer"
className="rounded-full border border-ctp-surface0 bg-ctp-surface0 px-4 py-2 shadow-md
shadow-ctp-overlay0 hover:bg-ctp-surface1 hover:shadow-ctp-overlay2"
@ -186,6 +197,7 @@ export default function Index() {
className="flex flex-col items-center justify-center space-y-4 p-4 text-center"
method="post"
>
<AuthenticityTokenInput />
<label>
<details>
<summary className="cursor-pointer text-2xl font-semibold tracking-wide">

View File

@ -1,8 +1,16 @@
import { redirect } from "@remix-run/node";
import { verifyAuthenticityToken } from "remix-utils";
import { destroySession, getSession } from "~/sessions";
export async function loader({ request }) {
export async function action({ request }) {
const session = await getSession(request.headers.get("Cookie"));
try {
await verifyAuthenticityToken(request, session);
} catch {
throw new Error("something went wrong");
}
return redirect("/", {
headers: {
"Set-Cookie": await destroySession(session),

View File

@ -2,9 +2,16 @@ import { redirect } from "@remix-run/node";
import { getSession } from "~/sessions";
import { getMe } from "~/utils";
import { prisma } from "~/db.server";
import { verifyAuthenticityToken } from "remix-utils";
export async function loader({ request }) {
export async function action({ request }) {
const session = await getSession(request.headers.get("Cookie"));
try {
await verifyAuthenticityToken(request, session);
} catch {
throw new Error("something went wrong");
}
if (!session.has("userId"))
throw new Error("OAuth token not found in cookie");

View File

@ -5,6 +5,7 @@ const secret = process.env.COOKIE_SECRET;
const { getSession, commitSession, destroySession } =
createCookieSessionStorage({
cookie: {
name: "oauth",
maxAge: 86400,
secrets: [secret],
path: "/",

View File

@ -16,7 +16,8 @@
"dotenv-cli": "^7.0.0",
"isbot": "^3.6.5",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"remix-utils": "^6.0.0"
},
"devDependencies": {
"@catppuccin/tailwindcss": "^0.1.1",

View File

@ -17,6 +17,7 @@ specifiers:
prisma: ^4.9.0
react: ^18.2.0
react-dom: ^18.2.0
remix-utils: ^6.0.0
tailwindcss: ^3.2.4
dependencies:
@ -28,6 +29,7 @@ dependencies:
isbot: 3.6.5
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
remix-utils: 6.0.0_gd6xnhzlbmgwjklncuy626fj3e
devDependencies:
'@catppuccin/tailwindcss': 0.1.1
@ -4665,6 +4667,16 @@ packages:
side-channel: 1.0.4
dev: true
/intl-parse-accept-language/1.0.0:
resolution: {integrity: sha512-YFMSV91JNBOSjw1cOfw2tup6hDP7mkz+2AUV7W1L1AM6ntgI75qC1ZeFpjPGMrWp+upmBRTX2fJWQ8c7jsUWpA==}
engines: {node: '>=14'}
dev: false
/ip-regex/4.3.0:
resolution: {integrity: sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==}
engines: {node: '>=8'}
dev: false
/ip/1.1.8:
resolution: {integrity: sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==}
dev: true
@ -4859,6 +4871,13 @@ packages:
engines: {node: '>=8'}
dev: true
/is-ip/3.1.0:
resolution: {integrity: sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==}
engines: {node: '>=8'}
dependencies:
ip-regex: 4.3.0
dev: false
/is-map/2.0.2:
resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==}
dev: true
@ -6794,6 +6813,26 @@ packages:
unified: 10.1.2
dev: true
/remix-utils/6.0.0_gd6xnhzlbmgwjklncuy626fj3e:
resolution: {integrity: sha512-S7Xec0YHZxGFEDawWpIbU7HQAZC0j51FmAvcCyBRuxjo71aAIMdmez47dgF8T91yxpHV1xlIKPL7LhBzmYsOZw==}
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
dependencies:
'@remix-run/react': 1.10.1_biqbaboplfbrettd7655fr4n2y
intl-parse-accept-language: 1.0.0
is-ip: 3.1.0
react: 18.2.0
schema-dts: 1.1.0
type-fest: 2.19.0
uuid: 8.3.2
transitivePeerDependencies:
- typescript
dev: false
/repeat-element/1.1.4:
resolution: {integrity: sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==}
engines: {node: '>=0.10.0'}
@ -6957,6 +6996,12 @@ packages:
dependencies:
loose-envify: 1.4.0
/schema-dts/1.1.0:
resolution: {integrity: sha512-vdmbs/5ycj4zyKpZIDqTcy+IZi4s7c38RVAYuDmRi7zgxUT8wRWPMLzg0jr7FjdVunYu9yZ00F3+XcZTTFcTOQ==}
peerDependencies:
typescript: '>=4.1.0'
dev: false
/semver/5.7.1:
resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==}
hasBin: true
@ -7574,6 +7619,11 @@ packages:
engines: {node: '>=10'}
dev: true
/type-fest/2.19.0:
resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
engines: {node: '>=12.20'}
dev: false
/type-is/1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
@ -7783,6 +7833,11 @@ packages:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'}
/uuid/8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
hasBin: true
dev: false
/uvu/0.5.6:
resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==}
engines: {node: '>=8'}