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
26 changes: 18 additions & 8 deletions backend/package-lock.json

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

3 changes: 2 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"dotenv": "^17.2.3",
"express": "^5.1.0",
"express-rate-limit": "^8.2.0",
"express-validator": "^7.2.1",
"express-validator": "^7.3.0",
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.18.3",
"socket.io": "^4.8.1"
Expand Down
43 changes: 26 additions & 17 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import express from "express";
import { createServer } from "http";
import cors from "cors";
import {
corsConfig,
helmetConfig,
generalLimiter,
speedLimiter,
securityHeaders,
requestSizeLimiter
} from "./middleware/security";
import healthRoutes from "./routes/health";
import authRoutes from "./routes/auth";
import projectRoutes from "./routes/project";
Expand All @@ -18,18 +26,19 @@ export function createApp() {
// Initialize Socket.IO service
const socketService = new SocketService(server);

// Middleware
app.use(cors({
origin: [
"http://localhost:5173",
"http://localhost:5174",
"http://127.0.0.1:5173",
"http://127.0.0.1:5174",
"https://collab-code-review-5gi4vw7jd-ram-prasads-projects-12031425.vercel.app"
],
credentials: true
}));
app.use(express.json());
// Security Middleware (order matters!)
app.use(helmetConfig); // Security headers
app.use(securityHeaders); // Additional custom security headers
app.use(corsConfig); // CORS policy
app.use(requestSizeLimiter); // Request size limiting
// Note: Rate limiting now applied per-route for better control

// Body parsing middleware
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));

// Trust proxy for accurate IP addresses (important for rate limiting)
app.set('trust proxy', 1);

// Routes
app.get("/", (req, res) => {
Expand All @@ -38,11 +47,11 @@ export function createApp() {

app.use("/health", healthRoutes);
app.use("/api/auth", authRoutes);
app.use("/api/projects", projectRoutes);
app.use("/api/snippets", snippetRoutes);
app.use("/api/pull-requests", pullRequestRoutes);
app.use("/api/notifications", notificationRoutes);
app.use("/api/users", userRoutes);
app.use("/api/projects", generalLimiter, projectRoutes);
app.use("/api/snippets", generalLimiter, snippetRoutes);
app.use("/api/pull-requests", generalLimiter, pullRequestRoutes);
app.use("/api/notifications", generalLimiter, notificationRoutes);
app.use("/api/users", generalLimiter, userRoutes);
app.use("/api/branch-protection", branchProtectionRoutes);

return { app, server, socketService };
Expand Down
185 changes: 185 additions & 0 deletions backend/src/middleware/security.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
import cors from 'cors';
import { Request, Response, NextFunction } from 'express';

// CORS Configuration
export const corsConfig = cors({
origin: function (origin, callback) {
// Allow requests with no origin (mobile apps, etc.)
if (!origin) return callback(null, true);

const allowedOrigins = [
'http://localhost:3000',
'http://localhost:5173',
'http://127.0.0.1:3000',
'http://127.0.0.1:5173',
// Add your production domains here
process.env.FRONTEND_URL || 'http://localhost:5173'
];

if (allowedOrigins.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: [
'Origin',
'X-Requested-With',
'Content-Type',
'Accept',
'Authorization',
'Cache-Control',
'X-HTTP-Method-Override'
],
maxAge: 86400 // Cache preflight for 24 hours
});

// Helmet Configuration for Security Headers
export const helmetConfig = helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
imgSrc: ["'self'", "data:", "https:"],
scriptSrc: ["'self'"],
connectSrc: ["'self'", "ws:", "wss:"], // Allow WebSocket connections
frameAncestors: ["'none'"],
objectSrc: ["'none'"],
upgradeInsecureRequests: []
}
},
crossOriginEmbedderPolicy: false, // Disable for WebSocket compatibility
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
});

// Rate Limiting Configurations
export const createRateLimit = (windowMs: number, max: number, message: string) =>
rateLimit({
windowMs,
max,
message: { error: message, retryAfter: Math.ceil(windowMs / 1000) },
standardHeaders: true,
legacyHeaders: false,
handler: (req: Request, res: Response) => {
res.status(429).json({
error: message,
retryAfter: Math.ceil(windowMs / 1000),
limit: max,
windowMs
});
}
});

// Tiered Rate Limiting
export const authLimiter = createRateLimit(
15 * 60 * 1000, // 15 minutes
10, // 10 attempts per window (increased from 5)
'Too many authentication attempts, please try again later'
);

export const generalLimiter = createRateLimit(
15 * 60 * 1000, // 15 minutes
500, // 500 requests per window (increased for better UX)
'Too many requests, please slow down'
);

export const strictLimiter = createRateLimit(
15 * 60 * 1000, // 15 minutes
50, // 50 requests per window (increased)
'Rate limit exceeded for this operation'
);

// Lenient limiter for health and status endpoints
export const healthLimiter = createRateLimit(
5 * 60 * 1000, // 5 minutes
1000, // 1000 requests per window
'Health endpoint rate limit exceeded'
);

// Custom Progressive Delay Middleware
interface DelayStore {
[key: string]: { hits: number; windowStart: number };
}

const delayStore: DelayStore = {};

export const speedLimiter = (req: Request, res: Response, next: NextFunction) => {
const ip = req.ip || 'unknown';
const now = Date.now();
const windowMs = 15 * 60 * 1000; // 15 minutes
const delayThreshold = 50; // Start delaying after 50 requests
const delayIncrement = 100; // Add 100ms per excess request
const maxDelay = 3000; // Maximum 3 seconds delay

// Clean old entries
if (!delayStore[ip] || now - delayStore[ip].windowStart > windowMs) {
delayStore[ip] = { hits: 0, windowStart: now };
}

delayStore[ip].hits++;
const hits = delayStore[ip].hits;

if (hits > delayThreshold) {
const excessHits = hits - delayThreshold;
const delay = Math.min(excessHits * delayIncrement, maxDelay);

setTimeout(() => {
next();
}, delay);
} else {
next();
}
};

// IP Whitelist Middleware
export const ipWhitelist = (whitelist: string[] = []) => {
return (req: Request, res: Response, next: NextFunction) => {
if (process.env.NODE_ENV === 'development') {
return next(); // Skip IP checking in development
}

const clientIp = req.ip || req.connection.remoteAddress || req.socket.remoteAddress;

if (whitelist.length === 0 || whitelist.includes(clientIp as string)) {
next();
} else {
res.status(403).json({ error: 'IP address not allowed' });
}
};
};

// Request Size Limiter
export const requestSizeLimiter = (req: Request, res: Response, next: NextFunction) => {
const contentLength = req.headers['content-length'];
const maxSize = 10 * 1024 * 1024; // 10MB limit

if (contentLength && parseInt(contentLength) > maxSize) {
return res.status(413).json({ error: 'Request entity too large' });
}

next();
};

// Security Headers Middleware
export const securityHeaders = (req: Request, res: Response, next: NextFunction) => {
// Remove powered by header
res.removeHeader('X-Powered-By');

// Add custom security headers
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');

next();
};
Loading
Loading