diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 00000000..6a580de4 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2024-06-15 - Add missing security headers and CORS +**Vulnerability:** The Fastify API server lacked essential security configurations (`helmet` for security headers and `cors` for cross-origin access control). Additionally, if CORS was to be configured, there was a risk of insecurely setting `origin: true` instead of explicitly managing trusted origins. +**Learning:** Monorepo API setups often default to open cross-origin access or missing headers if standard security plugins are not explicitly added. +**Prevention:** Always register standard security plugins (`@fastify/helmet` and `@fastify/cors`) in Fastify and enforce explicit allowlists for `origin` rather than `true`. diff --git a/apps/api/package.json b/apps/api/package.json index f30d34c2..150f1154 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -9,6 +9,8 @@ "typecheck": "cd ../.. && tsc -p tsconfig.json --noEmit" }, "dependencies": { + "@fastify/cors": "~8.5.0", + "@fastify/helmet": "~11.1.1", "@vooster/contracts": "workspace:*" } } diff --git a/apps/api/src/http/server.ts b/apps/api/src/http/server.ts index c04e47f9..bc0c116c 100644 --- a/apps/api/src/http/server.ts +++ b/apps/api/src/http/server.ts @@ -1,4 +1,6 @@ import Fastify, { type FastifyInstance } from "fastify"; +import fastifyCors from "@fastify/cors"; +import fastifyHelmet from "@fastify/helmet"; import { healthResponseSchema } from "@vooster/contracts"; import { createMemoryApiKeyStore } from "../infrastructure/memory-api-key-store.js"; import { createMemoryActorStore } from "../infrastructure/memory-actor-store.js"; @@ -63,6 +65,18 @@ import type { UserStore } from "../ports/user-store.js"; export async function createServer(options: ServerOptions): Promise { const serverOptions = withGithubOAuthFromEnv(options); const app = Fastify({ logger: false }); + + // 🛡️ Sentinel: Add security headers to protect against common web vulnerabilities like XSS, clickjacking, etc. + await app.register(fastifyHelmet); + + // 🛡️ Sentinel: Configure CORS strictly using explicit allowed origins. + // Prevents unauthorized cross-origin requests. Fails secure (origin: false) if no origins are configured. + const allowedOriginsRaw = process.env.VSPEC_ALLOWED_ORIGINS; + const allowedOrigins = allowedOriginsRaw ? allowedOriginsRaw.split(",") : []; + + await app.register(fastifyCors, { + origin: allowedOrigins.length > 0 ? allowedOrigins : false + }); const state = initialState(); const apiKeyStore = serverOptions.signupStore ?? createMemoryApiKeyStore(); const actorStore = serverOptions.signupStore ?? createMemoryActorStore(); diff --git a/apps/api/tests/integration/persistence-matrix-helpers.ts b/apps/api/tests/integration/persistence-matrix-helpers.ts index be21161e..e0084cb5 100644 --- a/apps/api/tests/integration/persistence-matrix-helpers.ts +++ b/apps/api/tests/integration/persistence-matrix-helpers.ts @@ -70,7 +70,8 @@ export async function bootServer(databaseUrl: string) { ...process.env, DATABASE_URL: databaseUrl, PORT: String(port), - VSPEC_AUTH_STUB: "1" + VSPEC_AUTH_STUB: "1", + VSPEC_ALLOWED_ORIGINS: "http://127.0.0.1:3000" }, stdio: ["ignore", "pipe", "pipe"] }); diff --git a/package.json b/package.json index 84bab428..dd8a71c9 100644 --- a/package.json +++ b/package.json @@ -36,31 +36,31 @@ "homepage": "https://github.com/vibemafiaclub/vooster#readme", "devDependencies": { "@eslint/js": "^10.0.1", + "@oclif/core": "^4.11.3", "@prisma/client": "^5.22.0", "@stryker-mutator/core": "^9.6.1", "@stryker-mutator/vitest-runner": "^9.6.1", "@types/node": "^20.19.41", "@vitest/coverage-v8": "^4.1.6", "@vooster/contracts": "workspace:*", - "eslint": "^10.4.0", - "eslint-plugin-boundaries": "^6.0.2", - "prettier": "^3.8.3", - "prisma": "^5.22.0", - "tsx": "^4.22.2", - "typescript": "^5.9.3", - "typescript-eslint": "^8.59.3", - "vitest": "^4.1.6", - "@oclif/core": "^4.11.3", "argon2": "^0.44.0", "boxen": "^8.0.1", "chalk": "^5.6.2", "cli-table3": "^0.6.5", "diff": "^9.0.0", "dotenv": "^17.4.2", + "eslint": "^10.4.0", + "eslint-plugin-boundaries": "^6.0.2", "fastify": "^4.29.1", "gray-matter": "^4.0.3", "marked": "^18.0.3", "pino": "^10.3.1", + "prettier": "^3.8.3", + "prisma": "^5.22.0", + "tsx": "^4.22.2", + "typescript": "^5.9.3", + "typescript-eslint": "^8.59.3", + "vitest": "^4.1.6", "zod": "^4.4.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2d2064ce..060e47d5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,6 +92,12 @@ importers: apps/api: dependencies: + '@fastify/cors': + specifier: ~8.5.0 + version: 8.5.0 + '@fastify/helmet': + specifier: ~11.1.1 + version: 11.1.1 '@vooster/contracts': specifier: workspace:* version: link:../../packages/contracts @@ -957,12 +963,18 @@ packages: '@fastify/ajv-compiler@3.6.0': resolution: {integrity: sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ==} + '@fastify/cors@8.5.0': + resolution: {integrity: sha512-/oZ1QSb02XjP0IK1U0IXktEsw/dUBTxJOW7IpIeO8c/tNalw/KjoNSJv1Sf6eqoBPO+TDGkifq6ynFK3v68HFQ==} + '@fastify/error@3.4.1': resolution: {integrity: sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==} '@fastify/fast-json-stringify-compiler@4.3.0': resolution: {integrity: sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==} + '@fastify/helmet@11.1.1': + resolution: {integrity: sha512-pjJxjk6SLEimITWadtYIXt6wBMfFC1I6OQyH/jYVCqSAn36sgAIFjeNiibHtifjCd+e25442pObis3Rjtame6A==} + '@fastify/merge-json-schemas@0.1.1': resolution: {integrity: sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==} @@ -3525,6 +3537,9 @@ packages: fast-wrap-ansi@0.2.2: resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + fastify-plugin@4.5.1: + resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==} + fastify@4.29.1: resolution: {integrity: sha512-m2kMNHIG92tSNWv+Z3UeTR9AWLLuo7KctC7mlFPtMEVrfjIhmQhkQnT9v15qA/BfVq3vvj134Y0jl9SBje3jXQ==} @@ -3740,6 +3755,10 @@ packages: headers-polyfill@5.0.1: resolution: {integrity: sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==} + helmet@7.2.0: + resolution: {integrity: sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==} + engines: {node: '>=16.0.0'} + hono@4.12.21: resolution: {integrity: sha512-uV63apnb0kyPtAUwoWgaGh9HyIFcv8lgmzPZSiTBQAFOFGIzka5EZ1dZocmGnn0XdX0+XTqJ6Tqv7selMuGLRQ==} engines: {node: '>=16.9.0'} @@ -4329,6 +4348,9 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + mnemonist@0.39.6: + resolution: {integrity: sha512-A/0v5Z59y63US00cRSLiloEIw3t5G+MiKz4BhX21FI+YBJXBOGW0ohFxTxO08dsOYlzxo87T7vGfZKYp2bcAWA==} + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -4462,6 +4484,9 @@ packages: resolution: {integrity: sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==} engines: {node: '>= 10'} + obliterator@2.0.5: + resolution: {integrity: sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==} + obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} @@ -6474,12 +6499,22 @@ snapshots: ajv-formats: 2.1.1(ajv@8.20.0) fast-uri: 2.4.0 + '@fastify/cors@8.5.0': + dependencies: + fastify-plugin: 4.5.1 + mnemonist: 0.39.6 + '@fastify/error@3.4.1': {} '@fastify/fast-json-stringify-compiler@4.3.0': dependencies: fast-json-stringify: 5.16.1 + '@fastify/helmet@11.1.1': + dependencies: + fastify-plugin: 4.5.1 + helmet: 7.2.0 + '@fastify/merge-json-schemas@0.1.1': dependencies: fast-deep-equal: 3.1.3 @@ -9171,6 +9206,8 @@ snapshots: dependencies: fast-string-width: 3.0.2 + fastify-plugin@4.5.1: {} + fastify@4.29.1: dependencies: '@fastify/ajv-compiler': 3.6.0 @@ -9463,6 +9500,8 @@ snapshots: '@types/set-cookie-parser': 2.4.10 set-cookie-parser: 3.1.0 + helmet@7.2.0: {} + hono@4.12.21: {} html-escaper@2.0.2: {} @@ -10120,6 +10159,10 @@ snapshots: minimist@1.2.8: {} + mnemonist@0.39.6: + dependencies: + obliterator: 2.0.5 + mrmime@2.0.1: {} ms@2.1.3: {} @@ -10242,6 +10285,8 @@ snapshots: object-treeify@1.1.33: {} + obliterator@2.0.5: {} + obug@2.1.1: {} ofetch@1.5.1: diff --git a/test_db.sh b/test_db.sh new file mode 100755 index 00000000..430bfdf9 --- /dev/null +++ b/test_db.sh @@ -0,0 +1,9 @@ +#!/bin/bash +docker pull postgres:16-alpine +docker run --name vspec-db-test -e POSTGRES_USER=vspec -e POSTGRES_PASSWORD=vspec -e POSTGRES_DB=vspec_test -p 5433:5432 -d postgres:16-alpine +sleep 5 +export DATABASE_URL="postgresql://vspec:vspec@127.0.0.1:5433/vspec_test" +export TEST_DATABASE_URL="postgresql://vspec:vspec@127.0.0.1:5433/vspec_test" +export NODE_OPTIONS="--max-old-space-size=4096" +export VSPEC_TEST_USE_DIST=1 +pnpm --filter @vooster/api run test -- apps/api/tests/integration/persistence-matrix-identity.test.ts