From 4e4b8744b589ba3fe7670832be566413d8b79a84 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Wed, 25 Dec 2024 22:04:20 -0500 Subject: [PATCH 1/4] CSRF prevention --- server/apiServer.ts | 24 +++++++++++++++++------- server/middlewares/csrfProtection.ts | 24 ++++++++++++++++++++++++ src/api/index.ts | 3 ++- 3 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 server/middlewares/csrfProtection.ts diff --git a/server/apiServer.ts b/server/apiServer.ts index 8fc131a5..6f05aae5 100644 --- a/server/apiServer.ts +++ b/server/apiServer.ts @@ -6,11 +6,12 @@ import logger from "@server/logger"; import { errorHandlerMiddleware, notFoundMiddleware, - rateLimitMiddleware, + rateLimitMiddleware } from "@server/middlewares"; import { authenticated, unauthenticated } from "@server/routers/external"; import { router as wsRouter, handleWSUpgrade } from "@server/routers/ws"; import { logIncomingMiddleware } from "./middlewares/logIncoming"; +import { csrfProtectionMiddleware } from "./middlewares/csrfProtection"; import helmet from "helmet"; const dev = process.env.ENVIRONMENT !== "prod"; @@ -25,13 +26,22 @@ export function createApiServer() { apiServer.use( cors({ origin: `http://localhost:${config.server.next_port}`, - credentials: true, - }), + credentials: true + }) ); } else { - apiServer.use(cors()); + const corsOptions = { + origin: config.app.base_url, + methods: ["GET", "POST", "PUT", "DELETE", "PATCH"], + allowedHeaders: ["Content-Type", "X-CSRF-Token"], + credentials: true + }; + + apiServer.use(cors(corsOptions)); apiServer.use(helmet()); + apiServer.use(csrfProtectionMiddleware); } + apiServer.use(cookieParser()); apiServer.use(express.json()); @@ -40,8 +50,8 @@ export function createApiServer() { rateLimitMiddleware({ windowMin: config.rate_limits.global.window_minutes, max: config.rate_limits.global.max_requests, - type: "IP_AND_PATH", - }), + type: "IP_AND_PATH" + }) ); } @@ -62,7 +72,7 @@ export function createApiServer() { const httpServer = apiServer.listen(externalPort, (err?: any) => { if (err) throw err; logger.info( - `API server is running on http://localhost:${externalPort}`, + `API server is running on http://localhost:${externalPort}` ); }); diff --git a/server/middlewares/csrfProtection.ts b/server/middlewares/csrfProtection.ts new file mode 100644 index 00000000..33150d65 --- /dev/null +++ b/server/middlewares/csrfProtection.ts @@ -0,0 +1,24 @@ +import { NextFunction, Request, Response } from "express"; + +export function csrfProtectionMiddleware( + req: Request, + res: Response, + next: NextFunction +) { + const csrfToken = req.headers["x-csrf-token"]; + + // Skip CSRF check for GET requests as they should be idempotent + if (req.method === "GET") { + next(); + return; + } + + if (!csrfToken || csrfToken !== "x-csrf-protection") { + res.status(403).json({ + error: "CSRF token missing or invalid" + }); + return; + } + + next(); +} diff --git a/src/api/index.ts b/src/api/index.ts index 32d0df6e..b59445db 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -32,7 +32,8 @@ export function createApiClient({ env }: { env: env }): AxiosInstance { baseURL, timeout: 10000, headers: { - "Content-Type": "application/json" + "Content-Type": "application/json", + "X-CSRF-Token": "x-csrf-protection" } }); From d75222626e5eba98db41609fe0043e277e6e632e Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Wed, 25 Dec 2024 22:08:52 -0500 Subject: [PATCH 2/4] Use samesite strict --- server/auth/index.ts | 8 ++++---- server/auth/resource.ts | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/server/auth/index.ts b/server/auth/index.ts index 54ba89a2..d5279aaf 100644 --- a/server/auth/index.ts +++ b/server/auth/index.ts @@ -87,17 +87,17 @@ export async function invalidateAllSessions(userId: string): Promise { export function serializeSessionCookie(token: string): string { if (SECURE_COOKIES) { - return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`; + return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`; } else { - return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Domain=${COOKIE_DOMAIN}`; + return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Domain=${COOKIE_DOMAIN}`; } } export function createBlankSessionTokenCookie(): string { if (SECURE_COOKIES) { - return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`; + return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`; } else { - return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Domain=${COOKIE_DOMAIN}`; + return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Domain=${COOKIE_DOMAIN}`; } } diff --git a/server/auth/resource.ts b/server/auth/resource.ts index 964b3840..90d85d83 100644 --- a/server/auth/resource.ts +++ b/server/auth/resource.ts @@ -166,9 +166,9 @@ export function serializeResourceSessionCookie( token: string ): string { if (SECURE_COOKIES) { - return `${cookieName}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`; + return `${cookieName}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`; } else { - return `${cookieName}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Domain=${COOKIE_DOMAIN}`; + return `${cookieName}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Domain=${COOKIE_DOMAIN}`; } } @@ -176,9 +176,9 @@ export function createBlankResourceSessionTokenCookie( cookieName: string ): string { if (SECURE_COOKIES) { - return `${cookieName}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`; + return `${cookieName}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`; } else { - return `${cookieName}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Domain=${COOKIE_DOMAIN}`; + return `${cookieName}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Domain=${COOKIE_DOMAIN}`; } } From 7d615d00d8ae62a8d3c2ca562e5dfcddea1719a8 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 26 Dec 2024 11:27:48 -0500 Subject: [PATCH 3/4] Remove credentials: true --- server/apiServer.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/apiServer.ts b/server/apiServer.ts index 6f05aae5..d4fe98f8 100644 --- a/server/apiServer.ts +++ b/server/apiServer.ts @@ -33,8 +33,7 @@ export function createApiServer() { const corsOptions = { origin: config.app.base_url, methods: ["GET", "POST", "PUT", "DELETE", "PATCH"], - allowedHeaders: ["Content-Type", "X-CSRF-Token"], - credentials: true + allowedHeaders: ["Content-Type", "X-CSRF-Token"] }; apiServer.use(cors(corsOptions)); From 34e3e7c819b401c86aa546586602ce5fa9e52917 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 26 Dec 2024 11:32:42 -0500 Subject: [PATCH 4/4] Remove config, scripts, hydrate --- .gitignore | 1 + config/.gitkeep | 0 config/db/.gitkeep | 0 config/logs/.gitkeep | 0 scripts/esbuild.mjs => esbuild.mjs | 0 package.json | 3 +- scripts/hydrate.ts | 130 ----------------------------- 7 files changed, 2 insertions(+), 132 deletions(-) delete mode 100644 config/.gitkeep delete mode 100644 config/db/.gitkeep delete mode 100644 config/logs/.gitkeep rename scripts/esbuild.mjs => esbuild.mjs (100%) delete mode 100644 scripts/hydrate.ts diff --git a/.gitignore b/.gitignore index 69f4f292..8ae65513 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ next-env.d.ts migrations package-lock.json tsconfig.tsbuildinfo +config/ config.yml dist .dist \ No newline at end of file diff --git a/config/.gitkeep b/config/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/config/db/.gitkeep b/config/db/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/config/logs/.gitkeep b/config/logs/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/scripts/esbuild.mjs b/esbuild.mjs similarity index 100% rename from scripts/esbuild.mjs rename to esbuild.mjs diff --git a/package.json b/package.json index 25ce1f7f..b4f99539 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,8 @@ "dev": "NODE_ENV=development ENVIRONMENT=dev tsx watch server/index.ts", "db:generate": "drizzle-kit generate", "db:push": "npx tsx server/db/migrate.ts", - "db:hydrate": "npx tsx scripts/hydrate.ts", "db:studio": "drizzle-kit studio", - "build": "mkdir -p dist && next build && node scripts/esbuild.mjs -e server/index.ts -o dist/server.mjs", + "build": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs", "start": "NODE_ENV=development ENVIRONMENT=prod node dist/server.mjs", "email": "email dev --dir server/emails/templates --port 3005" }, diff --git a/scripts/hydrate.ts b/scripts/hydrate.ts deleted file mode 100644 index 9d549e5c..00000000 --- a/scripts/hydrate.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { orgs, sites, resources, exitNodes, targets } from "@server/db/schema"; -// import db from "@server/db"; -// import { createAdminRole } from "@server/db/ensureActions"; - -// async function insertDummyData() { -// const org1 = db -// .insert(orgs) -// .values({ -// orgId: "fossorial", -// name: "Fossorial", -// domain: "fossorial.io", -// }) -// .returning() -// .get(); - -// await createAdminRole(org1.orgId!); - -// // Insert dummy exit nodes -// const exitNode1 = db -// .insert(exitNodes) -// .values({ -// name: "Exit Node 1", -// address: "10.0.0.1/24", -// publicKey: "sKQlCNErB2n+dV8eLp5Yw/avsjK/zkrxJE0n48hjb10=", -// listenPort: 51820, -// endpoint: "exitnode1.fossorial.io", -// }) -// .returning() -// .get(); - -// // Insert dummy sites -// const site1 = db -// .insert(sites) -// .values({ -// orgId: org1.orgId, - -// exitNodeId: exitNode1.exitNodeId, -// name: "Main Site", -// subdomain: "main", -// pubKey: "Kn4eD0kvcTwjO//zqH/CtNVkMNdMiUkbqFxysEym2D8=", -// subnet: "10.0.0.16/28", -// }) -// .returning() -// .get(); - -// const site2 = db -// .insert(sites) -// .values({ -// orgId: org2.orgId, -// exitNode: exitNode2.exitNodeId, -// name: "Dev Site", -// subdomain: "dev", -// pubKey: "V329Uf/vhnBwYxAuT/ZlMZuLokHy5tug/sGsLfIMK1w=", -// subnet: "172.16.1.16/28", -// }) -// .returning() -// .get(); - -// // Insert dummy resources -// const resource1 = db -// .insert(resources) -// .values({ -// resourceId: `web.${site1.subdomain}.${org1.domain}`, -// siteId: site1.siteId, -// orgId: site1.orgId, -// name: "Web Server", -// subdomain: "web", -// }) -// .returning() -// .get(); - -// const resource2 = db -// .insert(resources) -// .values({ -// resourceId: `web2.${site1.subdomain}.${org1.domain}`, -// siteId: site1.siteId, -// orgId: site1.orgId, -// name: "Web Server 2", -// subdomain: "web2", -// }) -// .returning() -// .get(); - -// const resource3 = db -// .insert(resources) -// .values({ -// resourceId: `db.${site2.subdomain}.${org2.domain}`, -// siteId: site2.siteId, -// orgId: site2.orgId, -// name: "Database", -// subdomain: "db", -// }) -// .returning() -// .get(); - -// // Insert dummy routes -// await db.insert(routes).values([ -// { exitNodeId: exitNode1.exitNodeId, subnet: "10.0.0.0/24" }, -// { exitNodeId: exitNode2.exitNodeId, subnet: "172.16.1.1/24" }, -// ]); - -// // Insert dummy targets -// await db.insert(targets).values([ -// { -// resourceId: resource1.resourceId, -// ip: "10.0.0.16", -// method: "http", -// port: 4200, -// protocol: "TCP", -// }, -// { -// resourceId: resource2.resourceId, -// ip: "10.0.0.17", -// method: "https", -// port: 443, -// protocol: "TCP", -// }, -// { -// resourceId: resource3.resourceId, -// ip: "172.16.1.16", -// method: "http", -// port: 80, -// protocol: "TCP", -// }, -// ]); - -// console.log("Dummy data inserted successfully"); -// } - -// insertDummyData().catch(console.error);