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 <github@erik.michelson.eu>
This commit is contained in:
Erik Michelson
2026-07-17 00:51:50 +02:00
committed by Philip Molares
parent be67b09a9e
commit 77a59c3a26
2 changed files with 73 additions and 1 deletions
@@ -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(<RendererIframe markdownContentLines={[]} rendererType={RendererType.DOCUMENT} />)
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))
}
})
})
@@ -186,7 +186,10 @@ export const RendererIframe: React.FC<RendererIframeProps> = ({
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'}