diff --git a/.gitignore b/.gitignore index 69f4f29..8ae6551 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 e69de29..0000000 diff --git a/config/db/.gitkeep b/config/db/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/config/logs/.gitkeep b/config/logs/.gitkeep deleted file mode 100644 index e69de29..0000000 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 5e90910..173984f 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_OPTIONS=--enable-source-maps 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 9d549e5..0000000 --- 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); diff --git a/server/apiServer.ts b/server/apiServer.ts index 8fc131a..d4fe98f 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,21 @@ 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"] + }; + + apiServer.use(cors(corsOptions)); apiServer.use(helmet()); + apiServer.use(csrfProtectionMiddleware); } + apiServer.use(cookieParser()); apiServer.use(express.json()); @@ -40,8 +49,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 +71,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/auth/index.ts b/server/auth/index.ts index 54ba89a..d5279aa 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 964b384..90d85d8 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}`; } } diff --git a/server/middlewares/csrfProtection.ts b/server/middlewares/csrfProtection.ts new file mode 100644 index 0000000..33150d6 --- /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 32d0df6..b59445d 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" } });