diff --git a/package.json b/package.json index dda79077..2f0dfa23 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.7.6", + "version": "2.7.6-dev.fd29718", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", diff --git a/src/core/external/collect/compose-collect-element.ts b/src/core/external/collect/compose-collect-element.ts index faa3bec3..be880d30 100644 --- a/src/core/external/collect/compose-collect-element.ts +++ b/src/core/external/collect/compose-collect-element.ts @@ -10,6 +10,7 @@ import SKYFLOW_ERROR_CODE from '../../../utils/constants'; import { ELEMENT_EVENTS_TO_CLIENT, ELEMENT_EVENTS_TO_IFRAME, ElementType } from '../../constants'; import { printLog } from '../../../utils/logs-helper'; import logs from '../../../utils/logs'; +import properties from '../../../properties'; class ComposableElement { #elementName: string; @@ -134,17 +135,19 @@ class ComposableElement { } }); window.addEventListener('message', (event) => { - if (event?.data?.type === `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES_RESPONSE}:${this.#elementName}`) { - if (event?.data?.data?.errorResponse || event?.data?.data?.error) { - printLog(`${event?.data?.data.errorResponse || event?.data?.data.error}`, MessageType.ERROR, this.#context.logLevel); - reject(event?.data?.data); - } else if (event?.data?.data.fileUploadResponse) { - printLog(logs.infoLogs.MULTI_UPLOAD_FILES_SUCCESS, - MessageType.LOG, this.#context.logLevel); - resolve(event?.data?.data); - } else { - printLog(`${event?.data?.data}`, MessageType.ERROR, this.#context.logLevel); - reject(event?.data?.data); + if (event?.origin === properties.IFRAME_SECURE_ORIGIN) { + if (event?.data?.type === `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES_RESPONSE}:${this.#elementName}`) { + if (event?.data?.data?.errorResponse || event?.data?.data?.error) { + printLog(`${event?.data?.data.errorResponse || event?.data?.data.error}`, MessageType.ERROR, this.#context.logLevel); + reject(event?.data?.data); + } else if (event?.data?.data.fileUploadResponse) { + printLog(logs.infoLogs.MULTI_UPLOAD_FILES_SUCCESS, + MessageType.LOG, this.#context.logLevel); + resolve(event?.data?.data); + } else { + printLog(`${event?.data?.data}`, MessageType.ERROR, this.#context.logLevel); + reject(event?.data?.data); + } } } }); diff --git a/src/core/external/reveal/composable-reveal-internal.ts b/src/core/external/reveal/composable-reveal-internal.ts index 8adaa7d9..6afdae9f 100644 --- a/src/core/external/reveal/composable-reveal-internal.ts +++ b/src/core/external/reveal/composable-reveal-internal.ts @@ -338,27 +338,30 @@ class ComposableRevealInternalElement extends SkyflowElement { }, ); window?.addEventListener('message', (event) => { - if (event?.data && event?.data?.type === ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + if (event?.origin === properties.IFRAME_SECURE_ORIGIN) { + if (event?.data + && event?.data?.type === ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + recordData.name) { - if (event?.data?.data?.type === REVEAL_TYPES.RENDER_FILE) { - const revealData = event?.data?.data?.result; - if (revealData?.error || revealData?.errors) { - printLog(parameterizedString( - logs.errorLogs.FAILED_RENDER, - ), MessageType.ERROR, - this.#context.logLevel); - if (Object.prototype.hasOwnProperty.call(recordData, 'altText')) { - this.setAltText(altText, recordData); - } - reject(revealData?.error || revealData?.errors); - } else { - printLog(parameterizedString(logs.infoLogs.RENDER_SUBMIT_SUCCESS, CLASS_NAME), - MessageType.LOG, + if (event?.data?.data?.type === REVEAL_TYPES.RENDER_FILE) { + const revealData = event?.data?.data?.result; + if (revealData?.error || revealData?.errors) { + printLog(parameterizedString( + logs.errorLogs.FAILED_RENDER, + ), MessageType.ERROR, this.#context.logLevel); - printLog(parameterizedString(logs.infoLogs.FILE_RENDERED, - CLASS_NAME, recordData.skyflowID), - MessageType.LOG, this.#context.logLevel); - resolve(revealData); + if (Object.prototype.hasOwnProperty.call(recordData, 'altText')) { + this.setAltText(altText, recordData); + } + reject(revealData?.error || revealData?.errors); + } else { + printLog(parameterizedString(logs.infoLogs.RENDER_SUBMIT_SUCCESS, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + printLog(parameterizedString(logs.infoLogs.FILE_RENDERED, + CLASS_NAME, recordData.skyflowID), + MessageType.LOG, this.#context.logLevel); + resolve(revealData); + } } } } @@ -408,29 +411,31 @@ class ComposableRevealInternalElement extends SkyflowElement { }, ); window.addEventListener('message', (event1) => { - if (event1?.data + if (event1?.origin === properties.IFRAME_SECURE_ORIGIN) { + if (event1?.data && event1?.data?.type === ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + recordData.name) { - if (event1?.data?.data?.type === REVEAL_TYPES.RENDER_FILE) { - const revealData = event1?.data?.data?.result; - if (revealData?.error || revealData?.errors) { - printLog(parameterizedString( - logs.errorLogs.FAILED_RENDER, - ), MessageType.ERROR, - this.#context.logLevel); - if (Object.prototype.hasOwnProperty.call(recordData, 'altText')) { - this.setAltText(altText, recordData); - } - reject(revealData?.error || revealData?.errors); - } else { - // eslint-disable-next-line max-len - printLog(parameterizedString(logs.infoLogs.RENDER_SUBMIT_SUCCESS, CLASS_NAME), - MessageType.LOG, + if (event1?.data?.data?.type === REVEAL_TYPES.RENDER_FILE) { + const revealData = event1?.data?.data?.result; + if (revealData?.error || revealData?.errors) { + printLog(parameterizedString( + logs.errorLogs.FAILED_RENDER, + ), MessageType.ERROR, this.#context.logLevel); - printLog(parameterizedString(logs.infoLogs.FILE_RENDERED, - CLASS_NAME, recordData.skyflowID), - MessageType.LOG, this.#context.logLevel); - resolve(revealData); + if (Object.prototype.hasOwnProperty.call(recordData, 'altText')) { + this.setAltText(altText, recordData); + } + reject(revealData?.error || revealData?.errors); + } else { + // eslint-disable-next-line max-len + printLog(parameterizedString(logs.infoLogs.RENDER_SUBMIT_SUCCESS, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + printLog(parameterizedString(logs.infoLogs.FILE_RENDERED, + CLASS_NAME, recordData.skyflowID), + MessageType.LOG, this.#context.logLevel); + resolve(revealData); + } } } } diff --git a/src/core/internal/reveal/reveal-frame.ts b/src/core/internal/reveal/reveal-frame.ts index fe6a20a4..90b7c8fe 100644 --- a/src/core/internal/reveal/reveal-frame.ts +++ b/src/core/internal/reveal/reveal-frame.ts @@ -300,41 +300,43 @@ class RevealFrame { ); this.updateRevealElementOptions(); window.addEventListener('message', (event) => { - if (event?.data?.name === ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + this.#name) { - if (event?.data?.data?.iframeName === this.#name + if (event?.origin === this.#clientDomain) { + if (event?.data?.name === ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + this.#name) { + if (event?.data?.data?.iframeName === this.#name && event?.data?.data?.type === REVEAL_TYPES.RENDER_FILE) { - this.renderFile(this.#record, event?.data?.clientConfig, - event?.data?.errorMessages)?.then((resolvedResult) => { - const result = formatForRenderClient( - resolvedResult as IRenderResponseType, - this.#record?.column, - ); - window?.parent?.postMessage({ - type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + this.#name, - data: { - type: REVEAL_TYPES.RENDER_FILE, - result, - }, - }, this.#clientDomain); - - window?.postMessage({ - type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK_COMPOSABLE + window?.name, - }, properties?.IFRAME_SECURE_ORIGIN); - })?.catch((error) => { - window?.parent?.postMessage({ - type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + this.#name, - data: { - type: REVEAL_TYPES.RENDER_FILE, - result: { - errors: error, + this.renderFile(this.#record, event?.data?.clientConfig, + event?.data?.errorMessages)?.then((resolvedResult) => { + const result = formatForRenderClient( + resolvedResult as IRenderResponseType, + this.#record?.column, + ); + window?.parent?.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + this.#name, + data: { + type: REVEAL_TYPES.RENDER_FILE, + result, }, - }, - }, this.#clientDomain); + }, this.#clientDomain); + + window?.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK_COMPOSABLE + window?.name, + }, properties?.IFRAME_SECURE_ORIGIN); + })?.catch((error) => { + window?.parent?.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + this.#name, + data: { + type: REVEAL_TYPES.RENDER_FILE, + result: { + errors: error, + }, + }, + }, this.#clientDomain); - window?.postMessage({ - type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK_COMPOSABLE + window?.name, - }, properties?.IFRAME_SECURE_ORIGIN); - }); + window?.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK_COMPOSABLE + window?.name, + }, properties?.IFRAME_SECURE_ORIGIN); + }); + } } } diff --git a/tests/core/external/collect/composable-element.test.ts b/tests/core/external/collect/composable-element.test.ts index f4701eaf..34276467 100644 --- a/tests/core/external/collect/composable-element.test.ts +++ b/tests/core/external/collect/composable-element.test.ts @@ -7,6 +7,7 @@ import EventEmitter from "../../../../src/event-emitter"; import { ContainerType } from "../../../../src/skyflow"; import { ElementState } from "../../../../src/utils/common"; import SKYFLOW_ERROR_CODE from "../../../../src/utils/constants"; +import properties from "../../../../src/properties"; describe("test composable element", () => { const emitter = jest.fn(); @@ -162,6 +163,7 @@ describe("test composable element", () => { // Trigger upload then dispatch error event AFTER listener is attached. const p = testElement4.uploadMultipleFiles(); window.dispatchEvent(new MessageEvent('message', { + origin: properties.IFRAME_SECURE_ORIGIN, data: { type: `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES_RESPONSE}:testce4`, data: { error: 'Error occurred' } @@ -180,6 +182,7 @@ describe("test composable element", () => { // Trigger upload then dispatch error event AFTER listener is attached. const p = testElement4.uploadMultipleFiles(); window.dispatchEvent(new MessageEvent('message', { + origin: properties.IFRAME_SECURE_ORIGIN, data: { type: `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES_RESPONSE}:testce4`, data: { error: 'Error occurred' } @@ -198,6 +201,7 @@ describe("test composable element", () => { // Trigger upload then dispatch error event AFTER listener is attached. const p = testElement4.uploadMultipleFiles(); window.dispatchEvent(new MessageEvent('message', { + origin: properties.IFRAME_SECURE_ORIGIN, data: { type: `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES_RESPONSE}:testce4`, data: 'error occurred' @@ -216,7 +220,7 @@ describe("test composable element", () => { const promise = multiEl.uploadMultipleFiles(); expect(messageHandler).toBeDefined(); // Simulate success - messageHandler({ data: { type: `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES_RESPONSE}:${elementName}`, data: { fileUploadResponse: [{ filename: 'doc.pdf' }] } } }); + messageHandler({ origin: properties.IFRAME_SECURE_ORIGIN, data: { type: `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES_RESPONSE}:${elementName}`, data: { fileUploadResponse: [{ filename: 'doc.pdf' }] } } }); await expect(promise).resolves.toMatchObject({ fileUploadResponse: [{ filename: 'doc.pdf' }] }); addSpy.mockRestore(); }); @@ -228,7 +232,7 @@ describe("test composable element", () => { let messageHandler: any; const addSpy = jest.spyOn(window, 'addEventListener').mockImplementation((evt, handler) => { if (evt === 'message') messageHandler = handler; }); const promise = multiEl.uploadMultipleFiles(); - messageHandler({ data: { type: `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES_RESPONSE}:${elementName}`, data: { errorResponse: 'Upload failed' } } }); + messageHandler({ origin: properties.IFRAME_SECURE_ORIGIN, data: { type: `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES_RESPONSE}:${elementName}`, data: { errorResponse: 'Upload failed' } } }); await expect(promise).rejects.toMatchObject({ errorResponse: 'Upload failed' }); addSpy.mockRestore(); }); @@ -240,8 +244,41 @@ describe("test composable element", () => { let messageHandler: any; const addSpy = jest.spyOn(window, 'addEventListener').mockImplementation((evt, handler) => { if (evt === 'message') messageHandler = handler; }); const promise = multiEl.uploadMultipleFiles(); - messageHandler({ data: { type: `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES_RESPONSE}:${elementName}`, data: { error: 'Validation error' } } }); + messageHandler({ origin: properties.IFRAME_SECURE_ORIGIN, data: { type: `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES_RESPONSE}:${elementName}`, data: { error: 'Validation error' } } }); await expect(promise).rejects.toMatchObject({ error: 'Validation error' }); addSpy.mockRestore(); }); + it('uploadMultipleFiles ignores message from wrong origin', async () => { + const elementName = 'multiWrongOrigin'; + const emitterStub: any = { _emit: jest.fn(), on: jest.fn() }; + const multiEl = new ComposableElement(elementName, emitterStub, iframeName, { type: ElementType.MULTI_FILE_INPUT }); + let messageHandler: any; + const addSpy = jest.spyOn(window, 'addEventListener').mockImplementation((evt, handler) => { + if (evt === 'message') messageHandler = handler; + }); + const promise = multiEl.uploadMultipleFiles(); + // Wrong origin — origin check is false, inner code skipped + messageHandler({ origin: 'https://attacker.com', data: { type: `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES_RESPONSE}:${elementName}`, data: { fileUploadResponse: [{ filename: 'doc.pdf' }] } } }); + // Correct origin — now resolves + messageHandler({ origin: properties.IFRAME_SECURE_ORIGIN, data: { type: `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES_RESPONSE}:${elementName}`, data: { fileUploadResponse: [{ filename: 'doc.pdf' }] } } }); + await expect(promise).resolves.toMatchObject({ fileUploadResponse: [{ filename: 'doc.pdf' }] }); + addSpy.mockRestore(); + }); + + it('uploadMultipleFiles ignores message with wrong event type', async () => { + const elementName = 'multiWrongType'; + const emitterStub: any = { _emit: jest.fn(), on: jest.fn() }; + const multiEl = new ComposableElement(elementName, emitterStub, iframeName, { type: ElementType.MULTI_FILE_INPUT }); + let messageHandler: any; + const addSpy = jest.spyOn(window, 'addEventListener').mockImplementation((evt, handler) => { + if (evt === 'message') messageHandler = handler; + }); + const promise = multiEl.uploadMultipleFiles(); + // Correct origin but wrong type — type check is false, inner code skipped + messageHandler({ origin: properties.IFRAME_SECURE_ORIGIN, data: { type: 'WRONG_EVENT_TYPE', data: { fileUploadResponse: [{ filename: 'doc.pdf' }] } } }); + // Correct type — now resolves + messageHandler({ origin: properties.IFRAME_SECURE_ORIGIN, data: { type: `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES_RESPONSE}:${elementName}`, data: { fileUploadResponse: [{ filename: 'doc.pdf' }] } } }); + await expect(promise).resolves.toMatchObject({ fileUploadResponse: [{ filename: 'doc.pdf' }] }); + addSpy.mockRestore(); + }); }); diff --git a/tests/core/internal/reveal/reveal-frame.test.js b/tests/core/internal/reveal/reveal-frame.test.js index b27981b1..a614eb15 100644 --- a/tests/core/internal/reveal/reveal-frame.test.js +++ b/tests/core/internal/reveal/reveal-frame.test.js @@ -1273,11 +1273,13 @@ describe("Reveal Frame Class", () => { defineUrl('http://localhost/?' + btoa(JSON.stringify(data))); RevealFrame.init(); window.dispatchEvent(new MessageEvent('message', { + origin: 'http://localhost', data: { name: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + elementName, data: { type: REVEAL_TYPES.RENDER_FILE, iframeName: elementName }, clientConfig: { vaultURL: 'http://localhost', vaultID: 'vault123', authToken: 'dummy-token' } - } + }, + origin: 'http://localhost' })); await new Promise(r => setTimeout(r, 0)); @@ -1321,7 +1323,8 @@ describe("Reveal Frame Class", () => { name: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + elementName, data: { type: REVEAL_TYPES.RENDER_FILE, iframeName: elementName }, clientConfig: { vaultURL: 'http://localhost', vaultID: 'vault123', authToken: 'dummy-token' } - } + }, + origin: 'http://localhost' })); await new Promise(r => setTimeout(r, 0)); @@ -1363,7 +1366,8 @@ describe("Reveal Frame Class", () => { name: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + elementName, data: { type: REVEAL_TYPES.RENDER_FILE, iframeName: elementName }, clientConfig: { vaultURL: 'http://localhost', vaultID: 'vault123', authToken: 'dummy-token' } - } + }, + origin: 'http://localhost' })); await new Promise(r => setTimeout(r, 0)); @@ -1379,6 +1383,43 @@ describe("Reveal Frame Class", () => { expect(heightCall).toBeTruthy(); }); + test("render file request ignored when message origin does not match clientDomain", async () => { + setFileURLResolve(); + window.postMessage = jest.fn(); + + const data = { + record: { + skyflowID: '1815-6223-1073-1425', + table: 'pii_fields', + column: 'primary_card_file', + label: 'Card Number', + altText: 'xxxx-xxxx-xxxx-xxxx', + inputStyles: { base: { color: 'red' } }, + labelStyles: { base: { color: 'black' } }, + }, + clientJSON: { metaData: { uuid: '1234' } }, + context: { logLevel: LogLevel.ERROR, env: Env.PROD }, + }; + defineUrl('http://localhost/?' + btoa(JSON.stringify(data))); + RevealFrame.init(); + + // Wrong origin — origin check at line 303 is false, renderFile is never called + window.dispatchEvent(new MessageEvent('message', { + origin: 'https://attacker.com', + data: { + name: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + elementName, + data: { type: REVEAL_TYPES.RENDER_FILE, iframeName: elementName }, + clientConfig: { vaultURL: 'http://localhost', vaultID: 'vault123', authToken: 'dummy-token' } + } + })); + + await new Promise(r => setTimeout(r, 0)); + + const parentCalls = window.parent.postMessage.mock.calls; + const responseCall = parentCalls.find(c => c[0]?.type === (ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + elementName)); + expect(responseCall).toBeUndefined(); + }); + test("responseUpdate with signed token and mask applies both correctly", () => { const uniqueElementName = "reveal:container300:frame300:meta:" + btoa('http://localhost'); const actualToken = "response-update-with-mask"; @@ -2140,7 +2181,8 @@ describe("Reveal Frame Class - Additional Tests", () => { name: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + elementNameComposable, data: { type: REVEAL_TYPES.RENDER_FILE, iframeName: elementNameComposable }, clientConfig: { vaultURL: 'http://localhost', vaultID: 'vault123', authToken: 'dummy-token' } - } + }, + origin: 'http://localhost' })); await new Promise(r => setTimeout(r, 0)); @@ -2196,7 +2238,8 @@ describe("Reveal Frame Class - Additional Tests", () => { name: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + composableElementName, data: { type: REVEAL_TYPES.RENDER_FILE, iframeName: composableElementName }, clientConfig: { vaultURL: 'http://localhost', vaultID: 'vault123', authToken: 'dummy-token' } - } + }, + origin: 'http://localhost' })); // Wait for async operations to complete (Promise resolution and DOM updates) @@ -2270,7 +2313,8 @@ describe("Reveal Frame Class - Additional Tests", () => { name: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + composableElementName, data: { type: REVEAL_TYPES.RENDER_FILE, iframeName: composableElementName }, clientConfig: { vaultURL: 'http://localhost', vaultID: 'vault123', authToken: 'dummy-token' } - } + }, + origin: 'http://localhost' })); // Wait for async operations to complete (Promise resolution and DOM updates) @@ -2338,7 +2382,8 @@ describe("Reveal Frame Class - Additional Tests", () => { name: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + composableElementName, data: { type: REVEAL_TYPES.RENDER_FILE, iframeName: composableElementName }, clientConfig: { vaultURL: 'http://localhost', vaultID: 'vault123', authToken: 'dummy-token' } - } + }, + origin: 'http://localhost' })); // Wait for async operations to complete (Promise resolution and DOM updates) @@ -2395,7 +2440,8 @@ describe("Reveal Frame Class - Additional Tests", () => { name: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + elementNameComposable, data: { type: REVEAL_TYPES.RENDER_FILE, iframeName: elementNameComposable }, clientConfig: { vaultURL: 'http://localhost', vaultID: 'vault123', authToken: 'dummy-token' } - } + }, + origin: 'http://localhost' })); await new Promise(r => setTimeout(r, 0));