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.
This commit is contained in:
Joe Boccanfuso authored and GitHub committed 2026-04-27 07:06:06 -04:00
1 parent 5ba8339037
commit f3cca21e49
3 files changed
+46 -7

No files matched your search

@@ -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',
@@ -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');