From 8280c19fd4377be38037c005a33a8db3eb14f870 Mon Sep 17 00:00:00 2001 From: Michalis Apostolou Date: Wed, 30 Sep 2026 13:09:58 +0300 Subject: [PATCH 1/2] PROD-88182: fix add-visitor-id middleware to use ctx.origin correctly after koa v3 upgrade --- examples/default-middlewares-example/app.js | 16 +++ .../default-middlewares-example/config.js | 26 ++++ .../default-middlewares-example/routes.js | 5 + src/initializers/koa/add-visitor-id.ts | 25 +++- .../default-middlewares-example.test.ts | 125 ++++++++++++++++++ test/initializers/koa/add-visitor-id.test.ts | 2 +- 6 files changed, 193 insertions(+), 6 deletions(-) create mode 100644 examples/default-middlewares-example/app.js create mode 100644 examples/default-middlewares-example/config.js create mode 100644 examples/default-middlewares-example/routes.js create mode 100644 test/examples/default-middlewares-example.test.ts diff --git a/examples/default-middlewares-example/app.js b/examples/default-middlewares-example/app.js new file mode 100644 index 00000000..55078475 --- /dev/null +++ b/examples/default-middlewares-example/app.js @@ -0,0 +1,16 @@ +const { orka } = require('../../build'); + +const w = orka({ + diamorphosis: { configFolder: './examples/default-middlewares-example' }, + routesPath: './examples/default-middlewares-example/routes.js', + beforeStart: () => { + const config = require('./config'); + console.log(`Going to start env: ${config.nodeEnv}`); + } +}); + +if (!module.parent) { + w.start(); +} + +module.exports = w; diff --git a/examples/default-middlewares-example/config.js b/examples/default-middlewares-example/config.js new file mode 100644 index 00000000..7a1232a8 --- /dev/null +++ b/examples/default-middlewares-example/config.js @@ -0,0 +1,26 @@ +module.exports = { + nodeEnv: 'demo', + log: { + json: true + }, + app: { + name: 'foo' + }, + cors: { + publicPrefixes: ['/api/allowAll'], + allowedOrigins: ['localhost:3220', 'lvh.me', '*.lvh.me'], + }, + riviere: { + bodyKeysRegex: '.*' + }, + visitor: { + setCookie: true, + cookie: 'wmc', + maxAge: '7d', + variant: 'v1', + secure: true, + forwarded_headers: ['user-agent', 'x-request-id', 'referer', 'origin'], + waitForAttribution: false, + }, + port: 3220 +}; diff --git a/examples/default-middlewares-example/routes.js b/examples/default-middlewares-example/routes.js new file mode 100644 index 00000000..ee902705 --- /dev/null +++ b/examples/default-middlewares-example/routes.js @@ -0,0 +1,5 @@ +module.exports = { + get: { + '/test': async (ctx, next) => (ctx.body = 'ok') + } +}; diff --git a/src/initializers/koa/add-visitor-id.ts b/src/initializers/koa/add-visitor-id.ts index ba4a1352..b12bd6e7 100644 --- a/src/initializers/koa/add-visitor-id.ts +++ b/src/initializers/koa/add-visitor-id.ts @@ -38,13 +38,9 @@ export default function (config): Middleware { ctx.cookies.secure = true; } - let cookieDomain = domain((ctx.origin && new URL(ctx.origin).hostname) || ctx.hostname); - if (config.visitor.getCookieDomain && typeof config.visitor.getCookieDomain === 'function') { - cookieDomain = config.visitor.getCookieDomain(ctx); - } const options: SetOption = { - domain: cookieDomain, + domain: getCookieDomain(ctx, config), maxAge: config.visitor.maxAge && ms(config.visitor.maxAge), httpOnly: false, secure: config.visitor.secure, @@ -59,4 +55,23 @@ export default function (config): Middleware { } return next(); }; + + function getCookieDomain(ctx: Context, config): string { + // custom cb function takes precedence + if (config.visitor.getCookieDomain && typeof config.visitor.getCookieDomain === 'function') { + return config.visitor.getCookieDomain(ctx); + } + + // utilizes the original origin + // even when app is behind proxy + // https://github.com/koajs/koa/issues/1008#issuecomment-2872014479 + if (URL.canParse(ctx.request.URL.origin)) { + return domain(ctx.request.URL.hostname); + } else { + // get hostname from Host header + // or X-Forwarded-Host when is proxy enabled + // https://github.com/koajs/koa/blob/master/lib/request.js#L281 + return domain(ctx.hostname); + } + } } diff --git a/test/examples/default-middlewares-example.test.ts b/test/examples/default-middlewares-example.test.ts new file mode 100644 index 00000000..d0908888 --- /dev/null +++ b/test/examples/default-middlewares-example.test.ts @@ -0,0 +1,125 @@ +import * as should from 'should'; +import * as supertest from 'supertest'; +import * as uuid from 'uuid'; + +describe('Default middlewares examples', () => { + let server; + after(async () => { + if (server) await server.stop(); + }); + + before(async () => { + const serverPath = '../../examples/default-middlewares-example/app'; + delete require.cache[require.resolve(serverPath)]; + server = require(serverPath); + await server.start(); + await new Promise(t => setTimeout(t, 1000)); + }); + + describe('add-visitor-id middleware', function() { + it('/test sets a visitor cookie', async () => { + const response = await supertest('localhost:3220') + .get('/test') + .expect(200); + + const cookies = response.headers['set-cookie']; + should(cookies).not.be.undefined(); + + const visitorCookie = cookies.find(cookie => cookie.startsWith('wmc=')); + should(visitorCookie).not.be.undefined(); + visitorCookie.should.match(/domain=localhost/i); + visitorCookie.should.match(/secure/i); + visitorCookie.should.match(/samesite=none/i); + + const [, encoded] = visitorCookie.split(';')[0].split('='); + const decoded = JSON.parse(decodeURIComponent(encoded)); + uuid.validate(decoded.cookie_id).should.be.true(); + }); + + it('/test reuses the visitor cookie when already present', async () => { + const visitor = uuid.v4(); + const cookie = encodeURIComponent(JSON.stringify({ cookie_id: visitor })); + + const response = await supertest('localhost:3220') + .get('/test') + .set('Cookie', `wmc=${cookie}`) + .expect(200); + + should(response.headers['set-cookie']).be.undefined(); + }); + + describe('when origin header is invalid', function() { + it('/test sets a visitor cookie', async () => { + const response = await supertest('localhost:3220') + .get('/test') + .set('origin', 'invalid') + .expect(200); + + const cookies = response.headers['set-cookie']; + should(cookies).not.be.undefined(); + + const visitorCookie = cookies.find(cookie => cookie.startsWith('wmc=')); + should(visitorCookie).not.be.undefined(); + visitorCookie.should.match(/domain=localhost/i); + visitorCookie.should.match(/secure/i); + visitorCookie.should.match(/samesite=none/i); + + const [, encoded] = visitorCookie.split(';')[0].split('='); + const decoded = JSON.parse(decodeURIComponent(encoded)); + uuid.validate(decoded.cookie_id).should.be.true(); + }); + }); + + describe('when callback for cookie domain is provided from config', function() { + let origConfig; + before(function() { + origConfig = server.config.visitor; + server.config.visitor.getCookieDomain = () => 'domain-provider-cb'; + }); + + after(function() { + server.config.visitor = origConfig; + }); + + it('uses the callback to set a visitor cookie domain', async () => { + const response = await supertest('localhost:3220') + .get('/test') + .set('origin', 'invalid') + .expect(200); + + const cookies = response.headers['set-cookie']; + should(cookies).not.be.undefined(); + + const visitorCookie = cookies.find(cookie => cookie.startsWith('wmc=')); + should(visitorCookie).not.be.undefined(); + visitorCookie.should.match(/domain=domain-provider-cb/i); + visitorCookie.should.match(/secure/i); + visitorCookie.should.match(/samesite=none/i); + + const [, encoded] = visitorCookie.split(';')[0].split('='); + const decoded = JSON.parse(decodeURIComponent(encoded)); + uuid.validate(decoded.cookie_id).should.be.true(); + }); + + it('precedes the origin header uses the callback to set a visitor cookie', async () => { + const response = await supertest('localhost:3220') + .get('/test') + .set('origin', 'http://localhost:3220') + .expect(200); + + const cookies = response.headers['set-cookie']; + should(cookies).not.be.undefined(); + + const visitorCookie = cookies.find(cookie => cookie.startsWith('wmc=')); + should(visitorCookie).not.be.undefined(); + visitorCookie.should.match(/domain=domain-provider-cb/i); + visitorCookie.should.match(/secure/i); + visitorCookie.should.match(/samesite=none/i); + + const [, encoded] = visitorCookie.split(';')[0].split('='); + const decoded = JSON.parse(decodeURIComponent(encoded)); + uuid.validate(decoded.cookie_id).should.be.true(); + }); + }); + }); +}); diff --git a/test/initializers/koa/add-visitor-id.test.ts b/test/initializers/koa/add-visitor-id.test.ts index bbd67023..bea62a00 100644 --- a/test/initializers/koa/add-visitor-id.test.ts +++ b/test/initializers/koa/add-visitor-id.test.ts @@ -16,7 +16,7 @@ describe('add-visitor-id', function () { beforeEach(function () { getRequestContextStub = sandbox.stub(); ctx = { - origin: 'https://apply.workable.com/foo', + request: { URL: new URL('https://apply.workable.com/foo') }, state: {}, cookies: { get: () => encodeURIComponent(JSON.stringify({ cookie_id: visitor })) From 6e3dbfb557c846b4eebc767fc680b3bfe1500e4a Mon Sep 17 00:00:00 2001 From: Michalis Apostolou Date: Thu, 1 Oct 2026 12:26:56 +0300 Subject: [PATCH 2/2] review fixes --- .../default-middlewares-example.test.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/test/examples/default-middlewares-example.test.ts b/test/examples/default-middlewares-example.test.ts index d0908888..7bbc7e17 100644 --- a/test/examples/default-middlewares-example.test.ts +++ b/test/examples/default-middlewares-example.test.ts @@ -48,7 +48,7 @@ describe('Default middlewares examples', () => { should(response.headers['set-cookie']).be.undefined(); }); - describe('when origin header is invalid', function() { + describe('when the Origin header is malformed', function() { it('/test sets a visitor cookie', async () => { const response = await supertest('localhost:3220') .get('/test') @@ -68,17 +68,26 @@ describe('Default middlewares examples', () => { const decoded = JSON.parse(decodeURIComponent(encoded)); uuid.validate(decoded.cookie_id).should.be.true(); }); + + it('derives the cookie domain from the host, not from Origin', async () => { + const response = await supertest('localhost:3220') + .get('/test') + .set('origin', 'https://evil.example.com') + .expect(200); + const visitorCookie = response.headers['set-cookie'].find(c => c.startsWith('wmc=')); + should(visitorCookie).not.be.undefined(); + visitorCookie.should.match(/domain=localhost/i); + visitorCookie.should.not.match(/evil\.example\.com/i); + }); }); describe('when callback for cookie domain is provided from config', function() { - let origConfig; before(function() { - origConfig = server.config.visitor; server.config.visitor.getCookieDomain = () => 'domain-provider-cb'; }); after(function() { - server.config.visitor = origConfig; + delete server.config.visitor.getCookieDomain; }); it('uses the callback to set a visitor cookie domain', async () => {