diff --git a/packages/core/src/__tests__/internal/fetchSettings.test.ts b/packages/core/src/__tests__/internal/fetchSettings.test.ts index 4599b55bf..ddf6e7e9a 100644 --- a/packages/core/src/__tests__/internal/fetchSettings.test.ts +++ b/packages/core/src/__tests__/internal/fetchSettings.test.ts @@ -293,6 +293,7 @@ describe('internal #getSettings', () => { ...clientArgs.config, useSegmentEndpoints: true, cdnProxy: cdnProxy, + allowInsecureProxy: true, }; const anotherClient = new SegmentClient({ ...clientArgs, @@ -319,6 +320,7 @@ describe('internal #getSettings', () => { ...clientArgs.config, useSegmentEndpoints: true, cdnProxy: cdnProxy, + allowInsecureProxy: true, }; const anotherClient = new SegmentClient({ ...clientArgs, @@ -346,6 +348,7 @@ describe('internal #getSettings', () => { ...clientArgs.config, useSegmentEndpoints: true, cdnProxy: cdnProxy, + allowInsecureProxy: true, }; const anotherClient = new SegmentClient({ ...clientArgs, diff --git a/packages/core/src/__tests__/util.test.ts b/packages/core/src/__tests__/util.test.ts index 9125f4426..83eeefab2 100644 --- a/packages/core/src/__tests__/util.test.ts +++ b/packages/core/src/__tests__/util.test.ts @@ -197,4 +197,16 @@ describe('getURL function', () => { 'Invalid URL has been passed' ); }); + + it('should throw when an explicit http:// host is passed without opt-in', () => { + expect(() => getURL('http://proxy.example.com', '/path')).toThrow( + 'Insecure HTTP proxy URL rejected' + ); + }); + + it('should allow http:// host when allowInsecure is true', () => { + expect(getURL('http://proxy.example.com', '/path', true)).toBe( + 'http://proxy.example.com/path' + ); + }); }); diff --git a/packages/core/src/analytics.ts b/packages/core/src/analytics.ts index 890955a3c..9f15e81dc 100644 --- a/packages/core/src/analytics.ts +++ b/packages/core/src/analytics.ts @@ -380,7 +380,11 @@ export class SegmentClient { settingsEndpoint = `/${this.config.writeKey}/settings`; } try { - return getURL(settingsPrefix, settingsEndpoint); + return getURL( + settingsPrefix, + settingsEndpoint, + this.config.allowInsecureProxy + ); } catch (error) { console.error( 'Error in getEndpointForSettings:', diff --git a/packages/core/src/plugins/SegmentDestination.ts b/packages/core/src/plugins/SegmentDestination.ts index 67aa74b71..ba77d8d94 100644 --- a/packages/core/src/plugins/SegmentDestination.ts +++ b/packages/core/src/plugins/SegmentDestination.ts @@ -414,7 +414,7 @@ export class SegmentDestination extends DestinationPlugin { baseURL = this.apiHost ?? defaultApiHost; } try { - return getURL(baseURL, endpoint); + return getURL(baseURL, endpoint, config?.allowInsecureProxy); } catch (error) { console.error('Error in getEndpoint:', `fallback to ${defaultApiHost}`); return defaultApiHost; diff --git a/packages/core/src/plugins/__tests__/SegmentDestination.test.ts b/packages/core/src/plugins/__tests__/SegmentDestination.test.ts index 23f4d410c..bdb29d5c9 100644 --- a/packages/core/src/plugins/__tests__/SegmentDestination.test.ts +++ b/packages/core/src/plugins/__tests__/SegmentDestination.test.ts @@ -556,6 +556,7 @@ describe('SegmentDestination', () => { ...clientArgs.config, useSegmentEndpoints: true, proxy: proxy, + allowInsecureProxy: true, }; plugin.analytics = new SegmentClient({ ...clientArgs, @@ -578,6 +579,7 @@ describe('SegmentDestination', () => { ...clientArgs.config, useSegmentEndpoints: true, proxy: proxy, + allowInsecureProxy: true, }; plugin.analytics = new SegmentClient({ ...clientArgs, @@ -601,6 +603,7 @@ describe('SegmentDestination', () => { ...clientArgs.config, useSegmentEndpoints: true, proxy: proxy, + allowInsecureProxy: true, }; plugin.analytics = new SegmentClient({ ...clientArgs, diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 12e58a43c..4cbd6b5ba 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -152,6 +152,13 @@ export type Config = { storePersistorSaveDelay?: number; proxy?: string; cdnProxy?: string; + /** + * Allow http:// proxy/cdnProxy URLs. By default the SDK rejects cleartext + * HTTP because the write key and all event PII travel in the request body. + * Only set this if your proxy terminates TLS internally and you understand + * the risk of on-path exposure. + */ + allowInsecureProxy?: boolean; useSegmentEndpoints?: boolean; // Use if you want to use Segment endpoints errorHandler?: (error: SegmentError) => void; /** Client-side httpConfig overrides (highest precedence over defaults and CDN). */ diff --git a/packages/core/src/util.ts b/packages/core/src/util.ts index 91237ff2b..f6440ff85 100644 --- a/packages/core/src/util.ts +++ b/packages/core/src/util.ts @@ -258,10 +258,20 @@ export const createPromise = ( }; }; -export function getURL(host: string, path: string) { +export function getURL(host: string, path: string, allowInsecure = false) { if (!host.startsWith('https://') && !host.startsWith('http://')) { host = 'https://' + host; } + if (host.startsWith('http://')) { + if (!allowInsecure) { + throw new Error( + 'Insecure HTTP proxy URL rejected. The write key and event PII travel in the request body. Use an https:// URL, or set allowInsecureProxy: true to suppress this error.' + ); + } + console.warn( + '[Segment] Warning: proxy/cdnProxy is using http:// — the write key and all event PII will be sent in cleartext.' + ); + } const s = `${host}${path}`; if (!validateURL(s)) { console.error('Invalid URL has been passed');