From d88b742096c869646729676c28ad03d5eba7d2ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kol=C3=A1rik?= Date: Wed, 21 Aug 2024 17:18:57 +0200 Subject: [PATCH] fix: allow credentials for trusted CORS requests --- config/default.cjs | 6 +++ config/development.cjs | 7 ++++ src/lib/http/middleware/cors.ts | 37 +++++++++++++++++-- src/measurement/route/create-measurement.ts | 7 ++-- .../tests/integration/middleware/cors.test.ts | 19 +++++++++- 5 files changed, 68 insertions(+), 8 deletions(-) diff --git a/config/default.cjs b/config/default.cjs index 0080ee36..1c517d83 100644 --- a/config/default.cjs +++ b/config/default.cjs @@ -4,6 +4,12 @@ module.exports = { docsHost: 'https://www.jsdelivr.com', port: 3000, processes: 2, + cors: { + trustedOrigins: [ + 'https://globalping.io', + 'https://staging.globalping.io', + ], + }, session: { cookieName: 'dash_session_token', cookieSecret: '', diff --git a/config/development.cjs b/config/development.cjs index 63ad5173..d8201493 100644 --- a/config/development.cjs +++ b/config/development.cjs @@ -1,4 +1,11 @@ module.exports = { + server: { + cors: { + trustedOrigins: [ + 'http://localhost:13000', + ], + }, + }, redis: { url: 'redis://localhost:16379', socket: { diff --git a/src/lib/http/middleware/cors.ts b/src/lib/http/middleware/cors.ts index bcf22b99..91fcf9a0 100644 --- a/src/lib/http/middleware/cors.ts +++ b/src/lib/http/middleware/cors.ts @@ -12,8 +12,39 @@ export const corsHandler = () => async (ctx: Context, next: Next) => { return next(); }; -export const corsAuthHandler = () => async (ctx: Context, next: Next) => { - ctx.set('Access-Control-Allow-Headers', '*, Authorization'); +export const corsAuthHandler = ({ trustedOrigins = [] }: CorsOptions) => { + const exposeHeaders = [ + 'ETag', + 'Link', + 'Location', + 'Retry-After', + 'X-RateLimit-Limit', + 'X-RateLimit-Consumed', + 'X-RateLimit-Remaining', + 'X-RateLimit-Reset', + 'X-Credits-Consumed', + 'X-Credits-Remaining', + 'X-Request-Cost', + 'X-Response-Time', + 'Deprecation', + 'Sunset', + ].join(', '); - return next(); + return async (ctx: Context, next: Next) => { + const origin = ctx.get('Origin'); + + // Allow credentials only if the request is coming from a trusted origin. + if (trustedOrigins.includes(origin)) { + ctx.set('Access-Control-Allow-Origin', ctx.get('Origin')); + ctx.set('Access-Control-Allow-Credentials', 'true'); + ctx.append('Vary', 'Origin'); + } + + ctx.set('Access-Control-Allow-Headers', 'Authorization, Content-Type'); + ctx.set('Access-Control-Expose-Headers', exposeHeaders); + + return next(); + }; }; + +export type CorsOptions = { trustedOrigins?: string[] }; diff --git a/src/measurement/route/create-measurement.ts b/src/measurement/route/create-measurement.ts index a462e7c5..18fa6d9d 100644 --- a/src/measurement/route/create-measurement.ts +++ b/src/measurement/route/create-measurement.ts @@ -2,12 +2,13 @@ import config from 'config'; import type Router from '@koa/router'; import { getMeasurementRunner } from '../runner.js'; import { bodyParser } from '../../lib/http/middleware/body-parser.js'; -import { corsAuthHandler } from '../../lib/http/middleware/cors.js'; +import { corsAuthHandler, CorsOptions } from '../../lib/http/middleware/cors.js'; import { validate } from '../../lib/http/middleware/validate.js'; import { authenticate, AuthenticateOptions } from '../../lib/http/middleware/authenticate.js'; import { schema } from '../schema/global-schema.js'; import type { ExtendedContext } from '../../types.js'; +const corsConfig = config.get('server.cors'); const sessionConfig = config.get('server.session'); const hostConfig = config.get('server.host'); const runner = getMeasurementRunner(); @@ -26,6 +27,6 @@ const handle = async (ctx: ExtendedContext): Promise => { export const registerCreateMeasurementRoute = (router: Router): void => { router - .options('/measurements', '/measurements', corsAuthHandler()) - .post('/measurements', '/measurements', authenticate({ session: sessionConfig }), bodyParser(), validate(schema), handle); + .options('/measurements', '/measurements', corsAuthHandler(corsConfig)) + .post('/measurements', '/measurements', corsAuthHandler(corsConfig), authenticate({ session: sessionConfig }), bodyParser(), validate(schema), handle); }; diff --git a/test/tests/integration/middleware/cors.test.ts b/test/tests/integration/middleware/cors.test.ts index 97609bf8..4bc91384 100644 --- a/test/tests/integration/middleware/cors.test.ts +++ b/test/tests/integration/middleware/cors.test.ts @@ -25,6 +25,21 @@ describe('cors', () => { expect(response.headers['access-control-allow-origin']).to.equal('*'); }); + + describe('POST /v1/measurements', () => { + it('should include the explicit origin if it is trusted', async () => { + const response = await requestAgent.options('/v1/measurements').set('Origin', 'https://globalping.io').send() as Response; + + expect(response.headers['access-control-allow-origin']).to.equal('https://globalping.io'); + expect(response.headers['vary']).to.include('Origin'); + }); + + it('should include the wildcard if the origin is not trusted', async () => { + const response = await requestAgent.options('/v1/measurements').send() as Response; + + expect(response.headers['access-control-allow-origin']).to.equal('*'); + }); + }); }); describe('Access-Control-Allow-Headers header', () => { @@ -34,10 +49,10 @@ describe('cors', () => { expect(response.headers['access-control-allow-headers']).to.equal('*'); }); - it('should include the header with value of *, Authorization', async () => { + it('should include the header with value of Authorization, Content-Type', async () => { const response = await requestAgent.options('/v1/measurements').send() as Response; - expect(response.headers['access-control-allow-headers']).to.equal('*, Authorization'); + expect(response.headers['access-control-allow-headers']).to.equal('Authorization, Content-Type'); }); }); });