From f92edae0e64ba82d45cb66a40b058ff880b60a73 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Mon, 10 Aug 2026 03:27:59 -0700 Subject: [PATCH 1/3] fix: select CSP by mounted application Fixes #7456 --- .changeset/calm-clouds-load.md | 5 ++ .../middleware/koa-security-headers.test.ts | 49 ++++++++++++++++++- .../src/middleware/koa-security-headers.ts | 6 ++- 3 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 .changeset/calm-clouds-load.md diff --git a/.changeset/calm-clouds-load.md b/.changeset/calm-clouds-load.md new file mode 100644 index 000000000000..586e7867eb72 --- /dev/null +++ b/.changeset/calm-clouds-load.md @@ -0,0 +1,5 @@ +--- +"@logto/core": patch +--- + +fix self-hosted Console entry points when requests are served by the Experience fallback diff --git a/packages/core/src/middleware/koa-security-headers.test.ts b/packages/core/src/middleware/koa-security-headers.test.ts index afd05a855f64..d2533ae0ac75 100644 --- a/packages/core/src/middleware/koa-security-headers.test.ts +++ b/packages/core/src/middleware/koa-security-headers.test.ts @@ -8,11 +8,12 @@ import createMockContext from '#src/test-utils/jest-koa-mocks/create-mock-contex const { jest } = import.meta; const { mockEsmWithActual } = createMockUtils(jest); +const mockEnvSetValues = new GlobalValues(); await mockEsmWithActual('#src/env-set/index.js', () => ({ EnvSet: { get values() { - return new GlobalValues(); + return mockEnvSetValues; }, }, AdminApps: { Console: 'console', Welcome: 'welcome' }, @@ -188,3 +189,49 @@ describe('koaSecurityHeaders() middleware — experience CSP', () => { expect(queries.signInExperiences.findDefaultSignInExperience).not.toHaveBeenCalled(); }); }); + +describe('koaSecurityHeaders() middleware — production admin CSP selection', () => { + const { isProduction } = mockEnvSetValues; + + beforeEach(() => { + Object.assign(mockEnvSetValues, { isProduction: true }); + }); + + afterEach(() => { + Object.assign(mockEnvSetValues, { isProduction }); + }); + + it.each([ + { path: '/console', mountedApp: 'console' }, + { path: '/welcome', mountedApp: 'welcome' }, + ])('uses the Console CSP for mounted $path routes', async ({ path, mountedApp }) => { + const run = koaSecurityHeaders([mountedApp], 'default'); + const ctx = createMockContext({ method: 'GET', url: path }); + + await run(ctx, koaNoop); + + const scriptSource = getCspDirective(ctx, 'script-src'); + + expect(scriptSource).toContain("'self'"); + expect(scriptSource).toContain('https://cdn.jsdelivr.net/'); + expect(scriptSource).toContain('blob:'); + expect(scriptSource).not.toContain("'unsafe-inline'"); + }); + + it.each([ + { path: '/console' }, + { path: '/welcome' }, + ])('uses the Experience CSP for unmounted $path routes', async ({ path }) => { + const run = koaSecurityHeaders([], 'default'); + const ctx = createMockContext({ method: 'GET', url: path }); + + await run(ctx, koaNoop); + + const scriptSource = getCspDirective(ctx, 'script-src'); + + expect(scriptSource).toContain("'self'"); + expect(scriptSource).toContain("'unsafe-inline'"); + expect(scriptSource).not.toContain('https://cdn.jsdelivr.net/'); + expect(scriptSource).not.toContain('blob:'); + }); +}); diff --git a/packages/core/src/middleware/koa-security-headers.ts b/packages/core/src/middleware/koa-security-headers.ts index 35de53803ea8..6ee5de2bfe15 100644 --- a/packages/core/src/middleware/koa-security-headers.ts +++ b/packages/core/src/middleware/koa-security-headers.ts @@ -298,8 +298,10 @@ export default function koaSecurityHeaders( // Admin Console if ( - requestPath.startsWith(`/${AdminApps.Console}`) || - requestPath.startsWith(`/${AdminApps.Welcome}`) + (mountedApps.includes(AdminApps.Console) && + requestPath.startsWith(`/${AdminApps.Console}`)) || + (mountedApps.includes(AdminApps.Welcome) && + requestPath.startsWith(`/${AdminApps.Welcome}`)) ) { await helmetPromise(consoleSecurityHeaderSettings, req, res); From c4380d5bb2aa238ee7c569a7f393dd1543319a37 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Fri, 21 Aug 2026 05:42:47 -0700 Subject: [PATCH 2/3] style: satisfy main-lint in koa-security-headers tests --- .../middleware/koa-security-headers.test.ts | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/core/src/middleware/koa-security-headers.test.ts b/packages/core/src/middleware/koa-security-headers.test.ts index d2533ae0ac75..5297d7f36650 100644 --- a/packages/core/src/middleware/koa-security-headers.test.ts +++ b/packages/core/src/middleware/koa-security-headers.test.ts @@ -194,10 +194,12 @@ describe('koaSecurityHeaders() middleware — production admin CSP selection', ( const { isProduction } = mockEnvSetValues; beforeEach(() => { + // eslint-disable-next-line @silverhand/fp/no-mutating-assign -- Toggle production mode for CSP selection tests. Object.assign(mockEnvSetValues, { isProduction: true }); }); afterEach(() => { + // eslint-disable-next-line @silverhand/fp/no-mutating-assign -- Restore the shared environment after each test. Object.assign(mockEnvSetValues, { isProduction }); }); @@ -218,20 +220,20 @@ describe('koaSecurityHeaders() middleware — production admin CSP selection', ( expect(scriptSource).not.toContain("'unsafe-inline'"); }); - it.each([ - { path: '/console' }, - { path: '/welcome' }, - ])('uses the Experience CSP for unmounted $path routes', async ({ path }) => { - const run = koaSecurityHeaders([], 'default'); - const ctx = createMockContext({ method: 'GET', url: path }); + it.each([{ path: '/console' }, { path: '/welcome' }])( + 'uses the Experience CSP for unmounted $path routes', + async ({ path }) => { + const run = koaSecurityHeaders([], 'default'); + const ctx = createMockContext({ method: 'GET', url: path }); - await run(ctx, koaNoop); + await run(ctx, koaNoop); - const scriptSource = getCspDirective(ctx, 'script-src'); + const scriptSource = getCspDirective(ctx, 'script-src'); - expect(scriptSource).toContain("'self'"); - expect(scriptSource).toContain("'unsafe-inline'"); - expect(scriptSource).not.toContain('https://cdn.jsdelivr.net/'); - expect(scriptSource).not.toContain('blob:'); - }); + expect(scriptSource).toContain("'self'"); + expect(scriptSource).toContain("'unsafe-inline'"); + expect(scriptSource).not.toContain('https://cdn.jsdelivr.net/'); + expect(scriptSource).not.toContain('blob:'); + } + ); }); From 9c78061cba2340ed49e2f730357af12ec12e649f Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Fri, 21 Aug 2026 06:21:59 -0700 Subject: [PATCH 3/3] style: satisfy prettier in koa-security-headers --- packages/core/src/middleware/koa-security-headers.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/src/middleware/koa-security-headers.ts b/packages/core/src/middleware/koa-security-headers.ts index 6ee5de2bfe15..cd691b6e3bc9 100644 --- a/packages/core/src/middleware/koa-security-headers.ts +++ b/packages/core/src/middleware/koa-security-headers.ts @@ -300,8 +300,7 @@ export default function koaSecurityHeaders( if ( (mountedApps.includes(AdminApps.Console) && requestPath.startsWith(`/${AdminApps.Console}`)) || - (mountedApps.includes(AdminApps.Welcome) && - requestPath.startsWith(`/${AdminApps.Welcome}`)) + (mountedApps.includes(AdminApps.Welcome) && requestPath.startsWith(`/${AdminApps.Welcome}`)) ) { await helmetPromise(consoleSecurityHeaderSettings, req, res);