diff --git a/assets/js/exelearning-editor.js b/assets/js/exelearning-editor.js index 0c22a05..acfea0a 100644 --- a/assets/js/exelearning-editor.js +++ b/assets/js/exelearning-editor.js @@ -322,7 +322,13 @@ const iframeWindow = this.iframe[0]?.contentWindow; if ( ! data || ! data.type || ! iframeWindow || event.source !== iframeWindow ) { - if ( data?.source === 'wp-exe-editor' && data.type === 'request-save' ) { + // The standalone editor page relays Ctrl+S through its own window. + if ( + data?.source === 'wp-exe-editor' && + data.type === 'request-save' && + event.source === window && + event.origin === window.location.origin + ) { this.requestSave(); } return; diff --git a/assets/js/wp-exe-bridge.js b/assets/js/wp-exe-bridge.js index 50c107f..1882105 100644 --- a/assets/js/wp-exe-bridge.js +++ b/assets/js/wp-exe-bridge.js @@ -273,6 +273,10 @@ if ( ! message.type || message.source === 'wp-exe-editor' ) { return; } + // Only the embedding WordPress page may drive save/export. + if ( event.source !== window.parent || ( '*' !== targetOrigin && event.origin !== targetOrigin ) ) { + return; + } try { switch ( message.type ) { diff --git a/tests/js/exelearning_editor.test.js b/tests/js/exelearning_editor.test.js index fccd191..9d69ccc 100644 --- a/tests/js/exelearning_editor.test.js +++ b/tests/js/exelearning_editor.test.js @@ -592,6 +592,17 @@ describe( 'exelearning-editor: the conversation with the editor', () => { expect( messagesOfType( posted, 'WP_REQUEST_SAVE' ) ).toHaveLength( 1 ); } ); + it( 'ignores a save request relayed from another window or origin', async () => { + const editor = await loadEditorOn( MODAL_MARKUP ); + const posted = stubEditorWindow( editor ); + const data = { source: 'wp-exe-editor', type: 'request-save' }; + + await editor.handleMessage( { data, source: {}, origin: window.location.origin } ); + await editor.handleMessage( { data, source: window, origin: 'https://evil.test' } ); + + expect( messagesOfType( posted, 'WP_REQUEST_SAVE' ) ).toHaveLength( 0 ); + } ); + it( 'asks the editor for the file when the save button is pressed', async () => { const editor = await loadEditorOn( MODAL_MARKUP ); const posted = stubEditorWindow( editor ); diff --git a/tests/js/wp_exe_bridge.test.js b/tests/js/wp_exe_bridge.test.js index c32babb..fd180e2 100644 --- a/tests/js/wp_exe_bridge.test.js +++ b/tests/js/wp_exe_bridge.test.js @@ -135,8 +135,10 @@ async function settle() { } /** Deliver a protocol message from the parent and let the bridge answer it. */ -async function send( data ) { - window.dispatchEvent( new window.MessageEvent( 'message', { data } ) ); +async function send( data, from = window.parent, origin = '' ) { + const event = new window.MessageEvent( 'message', { data, origin } ); + Object.defineProperty( event, 'source', { value: from } ); + window.dispatchEvent( event ); await settle(); } @@ -250,6 +252,33 @@ describe( 'wp-exe-bridge: announcing itself to the parent', () => { } ); } ); +describe( 'wp-exe-bridge: who may drive the protocol', () => { + it( 'ignores protocol commands from a window other than the parent', async () => { + embedIn( { postMessage: ( message ) => posted.push( message ) } ); + installEditorWithDocument(); + await loadBridge(); + posted.length = 0; + + await send( { type: 'GET_PROJECT_INFO', requestId: 'x' }, window ); + + expect( messageOf( 'PROJECT_INFO' ) ).toBeUndefined(); + } ); + + it( 'ignores protocol commands from another origin once the parent origin is known', async () => { + embedIn( { postMessage: ( message ) => posted.push( message ) } ); + window.__EXE_EMBEDDING_CONFIG__ = { parentOrigin: 'https://wp.example' }; + installEditorWithDocument(); + await loadBridge(); + posted.length = 0; + + await send( { type: 'GET_PROJECT_INFO', requestId: 'x' }, window.parent, 'https://evil.test' ); + expect( messageOf( 'PROJECT_INFO' ) ).toBeUndefined(); + + await send( { type: 'GET_PROJECT_INFO', requestId: 'y' }, window.parent, 'https://wp.example' ); + expect( messageOf( 'PROJECT_INFO' ) ).toBeDefined(); + } ); +} ); + describe( 'wp-exe-bridge: the target origin', () => { it( 'posts to the embedding parent origin when one was configured', async () => { const origins = [];