CVE-2023-32325: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: PostHog-js is a library to interface with the PostHog analytics tool. Versions prior to 1.57.2 have the potential for cross-site scripting. Problem has been patched in 1.57.2. Users are advised to upgrade. Users unable to upgrade should ensure that their Content Security Policy is in place.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 6.1
Published: 2023-05-27T00:15Z
Repository: https://github.com/PostHog/posthog-js
Language: TypeScript
Files changed: package.json, src/__tests__/extensions/toolbar.js, src/extensions/toolbar.ts, src/types.ts
Commit date: 2023-05-17 14:34:34 +0200
Commit message: fix: Remove API and JS urls (#630)

* remove api url

* 1.57.1-alpha.0

* 1.57.2-alpha.0

---------

Co-authored-by: Michael Matloka <dev@twixes.com>
Diff: diff --git a/package.json b/package.json
index 65559d4..62c8794 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,8 @@
 {
     "name": "posthog-js",
-    "version": "1.57.1",
+    "version": "1.57.2-alpha.0",
     "description": "Posthog-js allows you to automatically capture usage and send events to PostHog.",
     "repository": "https://github.com/PostHog/posthog-js",
     "author": "hey@posthog.com",
     "license": "MIT",
     "scripts": {
diff --git a/src/__tests__/extensions/toolbar.js b/src/__tests__/extensions/toolbar.js
index aecc605..a6bff50 100644
--- a/src/__tests__/extensions/toolbar.js
+++ b/src/__tests__/extensions/toolbar.js
@@ -76,11 +76,10 @@ describe('Toolbar', () => {
             userFlags: {
                 flag_1: 0,
                 flag_2: 1,
             },
             userId: 12345,
-            apiURL: given.config.api_host,
             ...given.toolbarParamsOverrides,
         }))
 
         beforeEach(() => {
             jest.spyOn(given.toolbar, 'loadToolbar').mockImplementation(() => {})
@@ -90,18 +89,24 @@ describe('Toolbar', () => {
             given('toolbarParamsOverrides', () => ({
                 action: 'ph_authorize',
             }))
 
             given.subject()
-            expect(given.toolbar.loadToolbar).toHaveBeenCalledWith({ ...given.toolbarParams, source: 'url' })
+            expect(given.toolbar.loadToolbar).toHaveBeenCalledWith({
+                ...given.toolbarParams,
+                source: 'url',
+            })
         })
 
         it('should initialize the toolbar when there are editor params in the session', () => {
             given('storedEditorParams', () => JSON.stringify(toolbarParams))
 
             given.subject()
-            expect(given.toolbar.loadToolbar).toHaveBeenCalledWith({ ...given.toolbarParams, source: 'url' })
+            expect(given.toolbar.loadToolbar).toHaveBeenCalledWith({
+                ...given.toolbarParams,
+                source: 'url',
+            })
         })
 
         it('should NOT initialize the toolbar when the activation query param does not exist', () => {
             given('hash', () => '')
 
@@ -150,22 +155,26 @@ describe('Toolbar', () => {
         given('toolbarParams', () => ({
             accessToken: 'accessToken',
             token: 'public_token',
             expiresAt: 'expiresAt',
             apiKey: 'apiKey',
-            apiURL: 'http://localhost:8000',
-            jsURL: 'http://localhost:8000',
         }))
 
         it('should persist for next time', () => {
             expect(given.subject()).toBe(true)
-            expect(JSON.parse(window.localStorage.getItem('_postHogToolbarParams'))).toEqual(given.toolbarParams)
+            expect(JSON.parse(window.localStorage.getItem('_postHogToolbarParams'))).toEqual({
+                ...given.toolbarParams,
+                apiURL: 'http://api.example.com',
+            })
         })
 
         it('should load if not previously loaded', () => {
             expect(given.subject()).toBe(true)
-            expect(window.ph_load_toolbar).toHaveBeenCalledWith(given.toolbarParams, given.lib)
+            expect(window.ph_load_toolbar).toHaveBeenCalledWith(
+                { ...given.toolbarParams, apiURL: 'http://api.example.com' },
+                given.lib
+            )
         })
 
         it('should NOT load if previously loaded', () => {
             expect(given.subject()).toBe(true)
             expect(given.subject()).toBe(false)
@@ -182,11 +191,10 @@ describe('Toolbar', () => {
         it('should load if not previously loaded', () => {
             expect(given.subject()).toBe(true)
             expect(window.ph_load_toolbar).toHaveBeenCalledWith(
                 {
                     ...given.toolbarParams,
-                    jsURL: 'http://api.example.com',
                     apiURL: 'http://api.example.com',
                     token: 'test_token',
                 },
                 given.lib
             )
diff --git a/src/extensions/toolbar.ts b/src/extensions/toolbar.ts
index 17351c7..5cc071b 100644
--- a/src/extensions/toolbar.ts
+++ b/src/extensions/toolbar.ts
@@ -19,11 +19,10 @@ export class Toolbar {
             toolbarParams['toolbarVersion'] &&
             toolbarParams['toolbarVersion'].indexOf('toolbar') === 0
         ) {
             this.loadToolbar({
                 ...toolbarParams,
-                apiURL: this.instance.get_config('api_host'),
             })
         }
     }
 
     /**
@@ -78,14 +77,10 @@ export class Toolbar {
                 // delete "add-action" or other intent from toolbarParams, otherwise we'll have the same intent
                 // every time we open the page (e.g. you just visiting your own site an hour later)
                 delete toolbarParams.userIntent
             }
 
-            if (!toolbarParams.apiURL) {
-                toolbarParams.apiURL = this.instance.get_config('api_host')
-            }
-
             if (toolbarParams['token'] && this.instance.get_config('token') === toolbarParams['token']) {
                 this.loadToolbar(toolbarParams)
                 return true
             } else {
                 return false
@@ -100,27 +95,25 @@ export class Toolbar {
             return false
         }
         // only load the toolbar once, even if there are multiple instances of PostHogLib
         ;(window as any)['_postHogToolbarLoaded'] = true
 
-        // the toolbar does not use the `jsURL` as that route is cached for 24 hours.
         // By design array.js, recorder.js, and toolbar.js are served from Django with no or limited caching, not from our CDN
         // Django respects the query params for caching, returning a 304 if appropriate
-        const host = params?.['apiURL'] || this.instance.get_config('api_host')
+        const host = this.instance.get_config('api_host')
         const timestampToNearestThirtySeconds = Math.floor(Date.now() / 30000) * 30000
         const toolbarUrl = `${host}${
             host.endsWith('/') ? '' : '/'
         }static/toolbar.js?_ts=${timestampToNearestThirtySeconds}`
         const disableToolbarMetrics =
             !POSTHOG_MANAGED_HOSTS.includes(this.instance.get_config('api_host')) &&
             this.instance.get_config('advanced_disable_toolbar_metrics')
 
         const toolbarParams = {
-            apiURL: host, // defaults to api_host from the instance config if nothing else set
-            jsURL: host, // defaults to api_host from the instance config if nothing else set
             token: this.instance.get_config('token'),
             ...params,
+            apiURL: host, // defaults to api_host from the instance config if nothing else set
             ...(disableToolbarMetrics ? { instrument: false } : {}),
         }
 
         const { source: _discard, ...paramsToPersist } = toolbarParams // eslint-disable-line
         window.localStorage.setItem('_postHogToolbarParams', JSON.stringify(paramsToPersist))
diff --git a/src/types.ts b/src/types.ts
index a010c71..526b525 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -267,12 +267,10 @@ export type ToolbarUserIntent = 'add-action' | 'edit-action'
 export type ToolbarSource = 'url' | 'localstorage'
 export type ToolbarVersion = 'toolbar'
 
 /* sync with posthog */
 export interface ToolbarParams {
-    apiURL?: string
-    jsURL?: string
     token?: string /** public posthog-js token */
     temporaryToken?: string /** private temporary user token */
     actionId?: number
     userIntent?: ToolbarUserIntent
     source?: ToolbarSource

Vulnerable code:     "version": "1.57.1",
            apiURL: given.config.api_host,
            expect(given.toolbar.loadToolbar).toHaveBeenCalledWith({ ...given.toolbarParams, source: 'url' })
            expect(given.toolbar.loadToolbar).toHaveBeenCalledWith({ ...given.toolbarParams, source: 'url' })
            apiURL: 'http://localhost:8000',
            jsURL: 'http://localhost:8000',
            expect(JSON.parse(window.localStorage.getItem('_postHogToolbarParams'))).toEqual(given.toolbarParams)
            expect(window.ph_load_toolbar).toHaveBeenCalledWith(given.toolbarParams, given.lib)
                    jsURL: 'http://api.example.com',
                apiURL: this.instance.get_config('api_host'),
            if (!toolbarParams.apiURL) {
                toolbarParams.apiURL = this.instance.get_config('api_host')
            }

        // the toolbar does not use the `jsURL` as that route is cached for 24 hours.
        const host = params?.['apiURL'] || this.instance.get_config('api_host')
            apiURL: host, // defaults to api_host from the instance config if nothing else set
            jsURL: host, // defaults to api_host from the instance config if nothing else set
    apiURL?: string
    jsURL?: string
Fixed code:     "version": "1.57.2-alpha.0",
            expect(given.toolbar.loadToolbar).toHaveBeenCalledWith({
                ...given.toolbarParams,
                source: 'url',
            })
            expect(given.toolbar.loadToolbar).toHaveBeenCalledWith({
                ...given.toolbarParams,
                source: 'url',
            })
            expect(JSON.parse(window.localStorage.getItem('_postHogToolbarParams'))).toEqual({
                ...given.toolbarParams,
                apiURL: 'http://api.example.com',
            })
            expect(window.ph_load_toolbar).toHaveBeenCalledWith(
                { ...given.toolbarParams, apiURL: 'http://api.example.com' },
                given.lib
            )
        const host = this.instance.get_config('api_host')
            apiURL: host, // defaults to api_host from the instance config if nothing else set