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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ jobs:
run: docker compose -f compose.yml -f compose.dev.yml up -d mongo redis

- name: Start server
run: LETSCUBE_TEST_AUTH=true SOCIAL_FEATURES_ENABLED=true POSTGRES_ENABLED=false yarn workspace letscube-server node index.js > server.log 2>&1 &
run: CORS_ORIGINS=http://localhost:3000,http://localhost:8080 LETSCUBE_TEST_AUTH=true SOCIAL_FEATURES_ENABLED=true POSTGRES_ENABLED=false yarn workspace letscube-server node index.js > server.log 2>&1 &

- name: Start socket server
run: SOCIAL_FEATURES_ENABLED=true POSTGRES_ENABLED=false yarn workspace letscube-server node socket/ > socket.log 2>&1 &
Expand Down
7 changes: 2 additions & 5 deletions server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const { connect } = require('./database');
const { createHealthHandler, createHealthReporter } = require('./health');
const { initializePostgres, pool, startPostgresMaintenance } = require('./postgres');
const session = require('./middlewares/session');
const { createCorsOptions } = require('./middlewares/cors');
const logger = require('./logger');
const auth = require('./auth');
const api = require('./api');
Expand Down Expand Up @@ -73,11 +74,7 @@ const init = async () => {

/* Cors */

app.use(cors({
credentials: true,
origin: true,
// config.cors.origin.map((o) => new RegExp(o)),
}));
app.use(cors(createCorsOptions(config.cors.origin)));

app.get('/api/csrf-token', (req, res) => res.json({ csrfToken: req.csrfToken() }));

Expand Down
10 changes: 10 additions & 0 deletions server/middlewares/cors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const createCorsOptions = (origins) => {
const allowedOrigins = new Set(origins);

return {
credentials: true,
origin: (origin, callback) => callback(null, !origin || allowedOrigins.has(origin)),
};
};

module.exports = { createCorsOptions };
62 changes: 62 additions & 0 deletions server/middlewares/cors.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/** @jest-environment node */
/* eslint-env jest */

const http = require('http');
const cors = require('cors');
const express = require('express');
const { createCorsOptions } = require('./cors');

const request = (server, origin) => new Promise((resolve, reject) => {
const req = http.request({
headers: origin ? { origin } : {},
host: '127.0.0.1',
method: 'GET',
path: '/',
port: server.address().port,
}, (res) => {
res.resume();
res.on('end', () => resolve({ headers: res.headers, status: res.statusCode }));
});
req.on('error', reject);
req.end();
});

describe('CORS origin allowlist', () => {
let server;

beforeEach((done) => {
const app = express();
app.use(cors(createCorsOptions(['https://letscube.net', 'http://localhost:3000'])));
app.get('/', (req, res) => res.json({ ok: true }));
server = app.listen(0, '127.0.0.1', () => done());
});

afterEach((done) => {
server.close(() => done());
});

it('permits configured credentialed browser origins', async () => {
const response = await request(server, 'https://letscube.net');

expect(response).toEqual(expect.objectContaining({ status: 200 }));
expect(response.headers).toEqual(expect.objectContaining({
'access-control-allow-credentials': 'true',
'access-control-allow-origin': 'https://letscube.net',
}));
});

it('does not grant CORS permission to an unconfigured origin', async () => {
const response = await request(server, 'https://untrusted.example');

expect(response).toEqual(expect.objectContaining({ status: 200 }));
expect(response.headers).not.toHaveProperty('access-control-allow-credentials');
expect(response.headers).not.toHaveProperty('access-control-allow-origin');
});

it('leaves same-origin and non-browser requests working', async () => {
const response = await request(server);

expect(response).toEqual(expect.objectContaining({ status: 200 }));
expect(response.headers).not.toHaveProperty('access-control-allow-origin');
});
});
Loading