Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 39 additions & 0 deletions app/vercel.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,44 @@
{
"rewrites": [
{ "source": "/(.*)", "destination": "/index.html" }
],
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "Content-Security-Policy",
"value": "default-src 'self'; script-src 'self' 'unsafe-eval' https://accounts.google.com https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net; connect-src 'self' ws: wss: https://algoforge-2-0.onrender.com http://localhost:5000 https://accounts.google.com https://www.googleapis.com https://cdn.jsdelivr.net https://fonts.googleapis.com https://fonts.gstatic.com; font-src 'self' https://fonts.gstatic.com https://cdn.jsdelivr.net; img-src 'self' data: blob: https://lh3.googleusercontent.com https://*.googleusercontent.com https://cdn.jsdelivr.net; frame-src 'self' https://accounts.google.com; child-src 'self' blob: https://accounts.google.com; worker-src 'self' blob: https://cdn.jsdelivr.net; form-action 'self'; frame-ancestors 'self';"
Comment thread
ayyushh-18 marked this conversation as resolved.
},
{
"key": "Cross-Origin-Opener-Policy",
"value": "same-origin-allow-popups"
},
{
"key": "Cross-Origin-Resource-Policy",
"value": "same-origin"
},
{
"key": "Referrer-Policy",
"value": "strict-origin-when-cross-origin"
},
{
"key": "Permissions-Policy",
"value": "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"
},
{
"key": "X-Content-Type-Options",
"value": "nosniff"
},
{
"key": "X-Frame-Options",
"value": "SAMEORIGIN"
},
{
"key": "Strict-Transport-Security",
"value": "max-age=31536000; includeSubDomains; preload"
}
]
}
]
}
26 changes: 25 additions & 1 deletion app/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,30 @@ import { defineConfig } from "vite"
// https://vite.dev/config/
export default defineConfig(() => ({
base: '/',
server: {
headers: {
'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-eval' https://accounts.google.com https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net; connect-src 'self' ws: wss: https://algoforge-2-0.onrender.com http://localhost:5000 https://accounts.google.com https://www.googleapis.com https://cdn.jsdelivr.net https://fonts.googleapis.com https://fonts.gstatic.com; font-src 'self' https://fonts.gstatic.com https://cdn.jsdelivr.net; img-src 'self' data: blob: https://lh3.googleusercontent.com https://*.googleusercontent.com https://cdn.jsdelivr.net; frame-src 'self' https://accounts.google.com; child-src 'self' blob: https://accounts.google.com; worker-src 'self' blob: https://cdn.jsdelivr.net; form-action 'self'; frame-ancestors 'self';",
Comment thread
ayyushh-18 marked this conversation as resolved.
'Cross-Origin-Opener-Policy': 'same-origin-allow-popups',
'Cross-Origin-Resource-Policy': 'same-origin',
'Referrer-Policy': 'strict-origin-when-cross-origin',
'Permissions-Policy': 'accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()',
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'SAMEORIGIN',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload',
}
},
preview: {
headers: {
'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-eval' https://accounts.google.com https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net; connect-src 'self' ws: wss: https://algoforge-2-0.onrender.com http://localhost:5000 https://accounts.google.com https://www.googleapis.com https://cdn.jsdelivr.net https://fonts.googleapis.com https://fonts.gstatic.com; font-src 'self' https://fonts.gstatic.com https://cdn.jsdelivr.net; img-src 'self' data: blob: https://lh3.googleusercontent.com https://*.googleusercontent.com https://cdn.jsdelivr.net; frame-src 'self' https://accounts.google.com; child-src 'self' blob: https://accounts.google.com; worker-src 'self' blob: https://cdn.jsdelivr.net; form-action 'self'; frame-ancestors 'self';",
'Cross-Origin-Opener-Policy': 'same-origin-allow-popups',
'Cross-Origin-Resource-Policy': 'same-origin',
'Referrer-Policy': 'strict-origin-when-cross-origin',
'Permissions-Policy': 'accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()',
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'SAMEORIGIN',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload',
}
},
plugins: [
react(),
],
Expand All @@ -15,7 +39,7 @@ export default defineConfig(() => ({
},
define: {
'import.meta.env.VITE_API_BASE_URL': JSON.stringify(
process.env.VITE_API_BASE_URL || 'https://algoforge-2-0.onrender.com'
process.env.VITE_API_BASE_URL || (process.env.VERCEL ? 'https://algoforge-2-0.onrender.com' : '')
Comment thread
ayyushh-18 marked this conversation as resolved.
),
},
build: {
Expand Down
13 changes: 13 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"express": "^4.19.2",
"express-rate-limit": "^8.5.2",
"google-auth-library": "^10.5.0",
"helmet": "^8.2.0",
"jsonwebtoken": "^9.0.3",
"mongodb": "^7.2.0",
"prisma": "^6.4.1"
Expand Down
53 changes: 50 additions & 3 deletions backend/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import express, { Express, Request, Response } from 'express';
import dotenv from 'dotenv';
import cors from 'cors';
import helmet from 'helmet';
import connectDB from './config/db';

dotenv.config();
Expand All @@ -18,6 +19,7 @@ const allowedOrigins = [
'https://algo-forge-2-0.vercel.app',
'https://algoforge-2-0.onrender.com',
'http://localhost:5173',
'http://localhost:4173',
'http://localhost:3000',
process.env.CLIENT_URL,
].filter(Boolean) as string[];
Expand All @@ -35,6 +37,46 @@ app.use(cors({
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
}));

// Remove Express fingerprint header
app.disable('x-powered-by');

// Security headers config
app.use(helmet({
contentSecurityPolicy: {
useDefaults: false,
directives: {
defaultSrc: ["'none'"],
scriptSrc: ["'none'"],
connectSrc: ["'self'"],
imgSrc: ["'self'"],
styleSrc: ["'none'"],
frameAncestors: ["'none'"],
formAction: ["'none'"],
}
},
crossOriginOpenerPolicy: { policy: "same-origin-allow-popups" },
crossOriginResourcePolicy: { policy: "cross-origin" },
referrerPolicy: { policy: "strict-origin-when-cross-origin" },
noSniff: true,
frameguard: { action: 'deny' },
hsts: process.env.NODE_ENV === 'production' ? {
maxAge: 31536000,
includeSubDomains: true,
preload: true
} : false,
xssFilter: true,
}));

// Permissions-Policy header configuration
app.use((req, res, next) => {
res.setHeader(
'Permissions-Policy',
'accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()'
);
next();
});

app.use(express.json());

// Database Connection
Expand Down Expand Up @@ -70,6 +112,11 @@ app.get('/api/health', (req: Request, res: Response) => {
res.json({ status: 'ok', message: 'Server is healthy' });
});

app.listen(port, () => {
console.log(`[server]: Server is running at http://localhost:${port}`);
});
let server: any;
if (process.env.NODE_ENV !== 'test') {
server = app.listen(port, () => {
console.log(`[server]: Server is running at http://localhost:${port}`);
});
}

export { app, server };
78 changes: 78 additions & 0 deletions backend/tests/securityHeaders.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
import http from 'http';

// Mock database connection config to avoid real MongoDB connections during test runs
vi.mock('../src/config/db', () => ({
__esModule: true,
prisma: {
user: {
findUnique: vi.fn(),
create: vi.fn(),
update: vi.fn(),
}
},
default: vi.fn(),
}));

import { app } from '../src/server';

describe('Security Headers Integration Test', () => {
let server: http.Server;
let url: string;

beforeAll(() => {
return new Promise<void>((resolve) => {
// Spin up the app on an ephemeral dynamic port
server = app.listen(0, () => {
const address = server.address();
const port = typeof address === 'string' ? 0 : address?.port;
url = `http://localhost:${port}`;
resolve();
});
});
});

afterAll(() => {
return new Promise<void>((resolve) => {
server.close(() => resolve());
});
});

it('should set appropriate security headers on /api/health', async () => {
const response = await fetch(`${url}/api/health`);
expect(response.status).toBe(200);

const headers = response.headers;

// Express default fingerprint header X-Powered-By must be removed
expect(headers.get('x-powered-by')).toBeNull();

// Content-Security-Policy (CSP) custom directives
const csp = headers.get('content-security-policy');
expect(csp).not.toBeNull();
expect(csp).toContain("default-src 'none'");
expect(csp).toContain("frame-ancestors 'none'");

// Cross-Origin-Opener-Policy (COOP)
expect(headers.get('cross-origin-opener-policy')).toBe('same-origin-allow-popups');

// Cross-Origin-Resource-Policy (CORP)
expect(headers.get('cross-origin-resource-policy')).toBe('cross-origin');

// Referrer-Policy
expect(headers.get('referrer-policy')).toBe('strict-origin-when-cross-origin');

// X-Content-Type-Options
expect(headers.get('x-content-type-options')).toBe('nosniff');

// X-Frame-Options
expect(headers.get('x-frame-options')).toBe('DENY');

// Permissions-Policy
const permissions = headers.get('permissions-policy');
expect(permissions).not.toBeNull();
expect(permissions).toContain('accelerometer=()');
expect(permissions).toContain('camera=()');
expect(permissions).toContain('microphone=()');
});
});
Loading