From f3cca21e49729fbf4bab85aa1de64b9dcfebd0ce Mon Sep 17 00:00:00 2001 From: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com> Date: Mon, 27 Apr 2026 07:06:06 -0400 Subject: [PATCH] fix(security): refine dynamic config URL trust policy and document trusted-origin behavior (#5973) * fix(security): refine dynamic config URL trust policy and document trusted-origin behavior * Update documentation for Authorization and Authentication. * Add isSameOrigin to resolveConfigFetchPolicy return value. --- .../default/src/utils/secureConfigFetch.js | 8 +++-- .../src/utils/secureConfigFetch.test.js | 36 ++++++++++++++++++- .../docs/docs/deployment/authorization.md | 9 +++-- 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/extensions/default/src/utils/secureConfigFetch.js b/extensions/default/src/utils/secureConfigFetch.js index 7ddc82c2f..079f994ae 100644 --- a/extensions/default/src/utils/secureConfigFetch.js +++ b/extensions/default/src/utils/secureConfigFetch.js @@ -61,6 +61,7 @@ function resolveConfigFetchPolicy(rawUrl, policy = {}) { const { allowedOrigins = [], userAuthenticationService } = policy; const parsedUrl = resolveConfigUrl(rawUrl); const protocol = parsedUrl.protocol.toLowerCase(); + const isSameOrigin = parsedUrl.origin === window.location.origin; if (!['http:', 'https:'].includes(protocol)) { throw new Error('Only HTTP(S) URLs are allowed for dynamic datasource configuration'); @@ -78,7 +79,7 @@ function resolveConfigFetchPolicy(rawUrl, policy = {}) { userAuthenticationService?.getAuthorizationHeader?.()?.Authorization ); - if (isAuthenticated) { + if (isAuthenticated && !isSameOrigin) { const normalizedAllowedOrigins = normalizeAllowedOrigins(allowedOrigins); if (!normalizedAllowedOrigins.length || !normalizedAllowedOrigins.includes(parsedUrl.origin)) { throw new Error( @@ -91,12 +92,13 @@ function resolveConfigFetchPolicy(rawUrl, policy = {}) { parsedUrl, normalizedUrl: parsedUrl.toString(), isAuthenticated, + isSameOrigin, }; } async function fetchConfigJson(normalizedPolicy) { - const { normalizedUrl, isAuthenticated } = normalizedPolicy; - const response = isAuthenticated + const { normalizedUrl, isAuthenticated, isSameOrigin } = normalizedPolicy; + const response = isAuthenticated || isSameOrigin ? await fetch(normalizedUrl) : await fetch(normalizedUrl, { method: 'GET', diff --git a/extensions/default/src/utils/secureConfigFetch.test.js b/extensions/default/src/utils/secureConfigFetch.test.js index a3b29d7c5..9a7f68aee 100644 --- a/extensions/default/src/utils/secureConfigFetch.test.js +++ b/extensions/default/src/utils/secureConfigFetch.test.js @@ -12,6 +12,7 @@ describe('secureConfigFetch', () => { expect(result.normalizedUrl).toBe('https://untrusted.example.com/config.json'); expect(result.isAuthenticated).toBe(false); + expect(result.isSameOrigin).toBe(false); }); it('blocks non-allowlisted origins in authenticated environments', () => { @@ -35,6 +36,7 @@ describe('secureConfigFetch', () => { expect(result.normalizedUrl).toBe('http://localhost:5000/config.json'); expect(result.isAuthenticated).toBe(true); + expect(result.isSameOrigin).toBe(false); }); it('blocks authenticated fetch when allowlist is missing', () => { @@ -47,6 +49,18 @@ describe('secureConfigFetch', () => { ).toThrow('Blocked remote configuration origin'); }); + it('allows same-origin in authenticated environments without allowlist', () => { + const result = resolveConfigFetchPolicy('/protected/config.json', { + userAuthenticationService: { + getAuthorizationHeader: () => ({ Authorization: 'Bearer token123' }), + }, + }); + + expect(result.normalizedUrl).toBe(`${window.location.origin}/protected/config.json`); + expect(result.isAuthenticated).toBe(true); + expect(result.isSameOrigin).toBe(true); + }); + it('rejects embedded userinfo in config URLs', () => { expect(() => resolveConfigFetchPolicy('https://user:pass@trusted.example.com/config.json', { @@ -71,7 +85,7 @@ describe('secureConfigFetch', () => { global.fetch = originalFetch; }); - it('uses hardened fetch options in unauthenticated environments', async () => { + it('uses hardened fetch options for unauthenticated cross-origin requests', async () => { global.fetch.mockResolvedValue({ status: 200, ok: true, @@ -81,6 +95,7 @@ describe('secureConfigFetch', () => { await fetchConfigJson({ normalizedUrl: 'https://example.com/config.json', isAuthenticated: false, + isSameOrigin: false, }); expect(global.fetch).toHaveBeenCalledWith( @@ -95,6 +110,24 @@ describe('secureConfigFetch', () => { ); }); + it('uses simple fetch for unauthenticated same-origin requests', async () => { + global.fetch.mockResolvedValue({ + status: 200, + ok: true, + json: async () => ({ ok: true }), + }); + + await fetchConfigJson({ + normalizedUrl: `${window.location.origin}/protected/config.json`, + isAuthenticated: false, + isSameOrigin: true, + }); + + expect(global.fetch).toHaveBeenCalledWith( + `${window.location.origin}/protected/config.json` + ); + }); + it('uses simple fetch in authenticated environments', async () => { global.fetch.mockResolvedValue({ status: 200, @@ -105,6 +138,7 @@ describe('secureConfigFetch', () => { await fetchConfigJson({ normalizedUrl: 'https://trusted.example.com/config.json', isAuthenticated: true, + isSameOrigin: false, }); expect(global.fetch).toHaveBeenCalledWith('https://trusted.example.com/config.json'); diff --git a/platform/docs/docs/deployment/authorization.md b/platform/docs/docs/deployment/authorization.md index 8270cf585..ae19fcd83 100644 --- a/platform/docs/docs/deployment/authorization.md +++ b/platform/docs/docs/deployment/authorization.md @@ -85,14 +85,17 @@ Allowed entry format for `dangerouslyAllowedOriginsForAuthenticatedEnvironments` Policy summary: - In unauthenticated environments, any HTTP(S) `?url=` origin is allowed. -- In authenticated environments, `?url=` origins must be present in `dangerouslyAllowedOriginsForAuthenticatedEnvironments`, otherwise loading fails closed. -- In unauthenticated environments, config URLs are fetched with: +- In authenticated environments, same-origin `?url=` values are allowed by default. +- In authenticated environments, cross-origin `?url=` values must be present in `dangerouslyAllowedOriginsForAuthenticatedEnvironments`, otherwise loading fails closed. +- In unauthenticated environments, cross-origin config URLs are fetched with: - `method: 'GET'` - `mode: 'cors'` - `credentials: 'omit'` - `redirect: 'error'` - `referrerPolicy: 'no-referrer'` -- In authenticated environments, allowlisted config URLs are fetched using simple fetch behavior. +- In unauthenticated environments, same-origin config URLs use a plain `fetch()` call (browser default `credentials: 'same-origin'`). +- Same-origin config URLs are fetched using simple fetch behavior (so same-origin session/cookie auth is preserved). +- In authenticated environments, allowlisted cross-origin config URLs are fetched using simple fetch behavior. - Returned datasource configuration payloads are consumed as-is (no additional URL/config scrubbing). Example: