From 77a59c3a2607a7150abb933b3756a677939c84b4 Mon Sep 17 00:00:00 2001 From: Erik Michelson Date: Fri, 17 Jul 2026 00:51:50 +0200 Subject: [PATCH] fix(renderer): allow opening browser popups from sandbox for pdfs Apparently, opening a link from a sandboxed iframe enabled the sandbox mode for the opened new tab as well. For most pages, this is not a problem since these pages don't depend on anything special. However when clicking a link to a PDF file, Chromium- based browsers don't open the tab, since the built-in PDF viewer is not compatible with the sandboxing. The attribute `allow-popups-to-escape-sandbox` allows pages opened from the sandbox into a new tab to be not sandboxed. Since we don't have opener access there, this is no risk to HedgeDoc and can safely be enabled and therefore fix PDF links. Signed-off-by: Erik Michelson --- .../renderer-iframe/renderer-iframe.spec.tsx | 69 +++++++++++++++++++ .../renderer-iframe/renderer-iframe.tsx | 5 +- 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/common/renderer-iframe/renderer-iframe.spec.tsx diff --git a/frontend/src/components/common/renderer-iframe/renderer-iframe.spec.tsx b/frontend/src/components/common/renderer-iframe/renderer-iframe.spec.tsx new file mode 100644 index 000000000..bacfe5357 --- /dev/null +++ b/frontend/src/components/common/renderer-iframe/renderer-iframe.spec.tsx @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2026 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { RendererType } from '../../render-page/window-post-message-communicator/rendering-message' +import { render, screen } from '@testing-library/react' +import { RendererIframe } from './renderer-iframe' + +jest.mock('../../editor-page/render-context/editor-to-renderer-communicator-context-provider', () => ({ + useEditorToRendererCommunicator: () => ({ + enableCommunication: jest.fn(), + getUuid: () => 'test-uuid', + sendMessageToOtherSide: jest.fn(), + setMessageTarget: jest.fn(), + unsetMessageTarget: jest.fn() + }) +})) + +jest.mock('../../markdown-renderer/hooks/use-extension-event-emitter', () => ({ + useExtensionEventEmitter: () => undefined +})) + +jest.mock('../../render-page/window-post-message-communicator/hooks/use-editor-receive-handler', () => ({ + useEditorReceiveHandler: jest.fn() +})) + +jest.mock('./hooks/use-effect-on-render-type-change', () => ({ + useEffectOnRenderTypeChange: jest.fn() +})) + +jest.mock('./hooks/use-force-render-page-url-on-iframe-load-callback', () => ({ + useForceRenderPageUrlOnIframeLoadCallback: () => jest.fn() +})) + +jest.mock('./hooks/use-send-additional-configuration-to-renderer', () => ({ + useSendAdditionalConfigurationToRenderer: jest.fn() +})) + +jest.mock('./hooks/use-send-fragment-to-renderer', () => ({ + useSendFragmentToRenderer: jest.fn() +})) + +jest.mock('./hooks/use-send-markdown-to-renderer', () => ({ + useSendMarkdownToRenderer: jest.fn() +})) + +jest.mock('./hooks/use-send-scroll-state', () => ({ + useSendScrollState: jest.fn() +})) + +describe('RendererIframe', () => { + it('includes security sandboxing attributes for the iframe', () => { + render() + + const expectedArguments = [ + 'allow-downloads', + 'allow-same-origin', + 'allow-scripts', + 'allow-popups', + 'allow-popups-to-escape-sandbox', + 'allow-modals' + ] + const renderer = screen.getByTitle('render') + for (const argument of expectedArguments) { + expect(renderer).toHaveAttribute('sandbox', expect.stringContaining(argument)) + } + }) +}) diff --git a/frontend/src/components/common/renderer-iframe/renderer-iframe.tsx b/frontend/src/components/common/renderer-iframe/renderer-iframe.tsx index ccaec9f8e..054f68b6f 100644 --- a/frontend/src/components/common/renderer-iframe/renderer-iframe.tsx +++ b/frontend/src/components/common/renderer-iframe/renderer-iframe.tsx @@ -186,7 +186,10 @@ export const RendererIframe: React.FC = ({ title='render' {...(isTestMode ? {} - : { sandbox: 'allow-downloads allow-same-origin allow-scripts allow-popups allow-modals' })} + : { + sandbox: + 'allow-downloads allow-same-origin allow-scripts allow-popups allow-popups-to-escape-sandbox allow-modals' + })} allowFullScreen={true} ref={frameReference} referrerPolicy={'no-referrer'}