Skip to content

Commit 2d0da17

Browse files
Merge pull request #467 from Ridanshi/fix/cors-allowlist-session-cookie-hardening
fix(security): replace wildcard CORS with allowlist and harden session cookies
2 parents 6f83e96 + 54017d6 commit 2d0da17

11 files changed

Lines changed: 416 additions & 115 deletions

File tree

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# URL of the backend API (no trailing slash).
2+
# Must match the origin the backend server listens on.
3+
VITE_BACKEND_URL=http://localhost:5000

README.md

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,27 @@ Install backend dependencies:
103103
npm install
104104
```
105105

106-
Start the backend server:
106+
3. Configure environment variables
107107

108+
Copy the example files and fill in your values:
109+
```bash
110+
# Frontend (.env in the repo root)
111+
cp .env.example .env
112+
113+
# Backend (.env inside backend/)
114+
cp backend/.env.example backend/.env
115+
```
116+
117+
Key variables to set:
118+
119+
| Variable | Where | Description |
120+
|---|---|---|
121+
| `VITE_BACKEND_URL` | root `.env` | URL of the backend (default: `http://localhost:5000`) |
122+
| `MONGO_URI` | `backend/.env` | MongoDB connection string |
123+
| `SESSION_SECRET` | `backend/.env` | Long random string used to sign session cookies |
124+
| `FRONTEND_ORIGIN` | `backend/.env` | URL of the frontend — restricts CORS. **Required in production.** Defaults to `http://localhost:5173` in development. |
125+
126+
4. Run the frontend
108127
```bash
109128
npm run dev
110129
```
@@ -115,18 +134,11 @@ The backend server will run on:
115134
http://localhost:5000
116135
```
117136

118-
---
119-
120-
# 🐳 Docker Development Workflow
121-
122-
The project includes Docker configurations for both development and production environments.
123-
124-
## 📦 Development Environment
125-
126-
Run the complete development environment using Docker:
127-
137+
5. Run the backend
128138
```bash
129-
npm run docker:dev
139+
$ cd backend
140+
$ npm i
141+
$ npm start
130142
```
131143

132144
This command:

backend/.env.example

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# ---------------------------------------------------------------
2+
# Server
3+
# ---------------------------------------------------------------
4+
PORT=5000
5+
NODE_ENV=development
6+
7+
# ---------------------------------------------------------------
8+
# MongoDB
9+
# ---------------------------------------------------------------
10+
MONGO_URI=mongodb://127.0.0.1:27017/github_tracker
11+
12+
# ---------------------------------------------------------------
13+
# Session
14+
# Generate a long random string for production, e.g.:
15+
# node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
16+
# ---------------------------------------------------------------
17+
SESSION_SECRET=replace-with-a-long-random-string
18+
19+
# ---------------------------------------------------------------
20+
# CORS — Frontend origin allowlist
21+
#
22+
# Set this to the exact URL of your frontend (no trailing slash).
23+
# REQUIRED in production: the server will refuse to start without it.
24+
# In development, the server defaults to http://localhost:5173 when
25+
# this variable is not set.
26+
#
27+
# Examples:
28+
# Development : FRONTEND_ORIGIN=http://localhost:5173
29+
# Production : FRONTEND_ORIGIN=https://app.example.com
30+
# ---------------------------------------------------------------
31+
FRONTEND_ORIGIN=http://localhost:5173
32+
33+
# ---------------------------------------------------------------
34+
# Logging
35+
# Accepted values: error | warn | info | debug
36+
# ---------------------------------------------------------------
37+
LOG_LEVEL=debug

backend/config/validateEnv.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* Validates required environment variables before the server starts.
3+
* Throws so callers can decide whether to log + exit or handle otherwise,
4+
* which keeps the logic unit-testable without spawning child processes.
5+
*/
6+
function validateEnv() {
7+
if (process.env.NODE_ENV === 'production' && !process.env.FRONTEND_ORIGIN) {
8+
throw new Error(
9+
'FRONTEND_ORIGIN environment variable is required in production. ' +
10+
'Set it to the URL of your frontend (e.g., https://app.example.com).'
11+
);
12+
}
13+
}
14+
15+
module.exports = { validateEnv };

backend/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
"zod": "^4.4.3"
2626
},
2727
"devDependencies": {
28-
"nodemon": "^3.1.9"
28+
"jasmine": "^5.0.0",
29+
"nodemon": "^3.1.9",
30+
"supertest": "^7.0.0"
2931
}
3032
}

backend/server.js

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,32 +6,62 @@ const bodyParser = require('body-parser');
66
require('dotenv').config();
77
const cors = require('cors');
88

9+
const { validateEnv } = require('./config/validateEnv');
10+
const logger = require('./logger');
11+
12+
// Fail fast in production when required env vars are absent.
13+
try {
14+
validateEnv();
15+
} catch (err) {
16+
logger.error(`[FATAL] ${err.message}`);
17+
process.exit(1);
18+
}
19+
920
// Passport configuration
1021
require('./config/passportConfig');
1122

12-
const logger = require('./logger');
13-
1423
const app = express();
1524

16-
// CORS configuration
17-
const allowedOrigins = ['http://localhost:5173', 'https://github-spy.etlify.app'];
25+
// In development, fall back to localhost:5173 if FRONTEND_ORIGIN is not set so
26+
// that contributors can run the stack without a full .env file.
27+
const corsOrigin = process.env.FRONTEND_ORIGIN || 'http://localhost:5173';
28+
29+
if (!process.env.FRONTEND_ORIGIN) {
30+
logger.warn(
31+
'FRONTEND_ORIGIN is not set; defaulting to http://localhost:5173. ' +
32+
'Set this variable in production.'
33+
);
34+
}
35+
36+
// CORS — explicit allowlist with credentials support.
37+
// A function-based origin is required so that the header is only set (and
38+
// reflected) for allowed origins; a static string would send the header on
39+
// every response regardless of the requesting origin.
1840
app.use(cors({
19-
origin: function (origin, callback) {
20-
if (!origin || allowedOrigins.indexOf(origin) !== -1) {
21-
callback(null, true);
22-
} else{
23-
callback(new Error('Blocked by CORS policy'));
24-
}
25-
},
26-
credentials: true
41+
origin: (requestOrigin, callback) => {
42+
// Allow same-origin requests (no Origin header) and the configured origin.
43+
if (!requestOrigin || requestOrigin === corsOrigin) {
44+
return callback(null, true);
45+
}
46+
callback(null, false);
47+
},
48+
credentials: true,
49+
methods: ['GET', 'POST'],
50+
allowedHeaders: ['Content-Type'],
2751
}));
2852

2953
// Middleware
3054
app.use(bodyParser.json());
3155
app.use(session({
32-
secret: process.env.SESSION_SECRET,
33-
resave: false,
34-
saveUninitialized: false,
56+
secret: process.env.SESSION_SECRET,
57+
resave: false,
58+
saveUninitialized: false,
59+
cookie: {
60+
httpOnly: true,
61+
// Only transmit the cookie over HTTPS in production.
62+
secure: process.env.NODE_ENV === 'production',
63+
sameSite: 'strict',
64+
},
3565
}));
3666
app.use(passport.initialize());
3767
app.use(passport.session());
@@ -42,12 +72,10 @@ app.use('/api/auth', authRoutes);
4272

4373
// Connect to MongoDB
4474
mongoose.connect(process.env.MONGO_URI, {}).then(() => {
45-
logger.info('Connected to MongoDB');
46-
47-
const PORT = process.env.PORT || 5000;
48-
app.listen(PORT, () => {
49-
logger.info(`Server running on port ${PORT}`);
50-
});
75+
logger.info('Connected to MongoDB');
76+
app.listen(process.env.PORT, () => {
77+
logger.info(`Server running on port ${process.env.PORT}`);
78+
});
5179
}).catch((err) => {
52-
logger.error('MongoDB connection error', err);
80+
logger.error('MongoDB connection error', err);
5381
});

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454

5555
"autoprefixer": "^10.4.20",
5656
"bcryptjs": "^3.0.3",
57+
"cors": "^2.8.5",
5758

5859
"eslint": "^9.13.0",
5960
"eslint-plugin-react": "^7.37.2",

0 commit comments

Comments
 (0)