🛂(frontend) secure download button

Blocknote download button opens the file in a new
tab, which could be not secure because of XSS attacks.
We replace the download button with a new one that
downloads the file instead of opening it in a new tab.
Some files are flags as unsafe (SVG / js / exe),
for these files we add a confirmation modal before
downloading the file to prevent the user from
downloading a file that could be harmful.
In the future, we could add other security layers
from this model, to analyze the file before
downloading it by example.
This commit is contained in:
Anthony LC
2025-02-28 11:38:12 +01:00
committed by Anthony LC
parent ebd49f05a8
commit d099d58f77
7 changed files with 288 additions and 13 deletions

View File

@@ -0,0 +1,13 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="100"
height="100"
viewBox="0 0 100 100"
>
<circle cx="50" cy="30" r="20" fill="#3498db" />
<polygon
points="50,10 55,20 65,20 58,30 60,40 50,35 40,40 42,30 35,20 45,20"
fill="#f1c40f"
/>
<text x="50" y="70" text-anchor="middle" fill="white">Hello svg</text>
</svg>

After

Width:  |  Height:  |  Size: 336 B

View File

@@ -1,8 +1,7 @@
/* eslint-disable playwright/no-conditional-expect */
/* eslint-disable playwright/no-conditional-in-test */
import path from 'path';
import { expect, test } from '@playwright/test';
import cs from 'convert-stream';
import {
createDoc,
@@ -415,6 +414,8 @@ test.describe('Doc Editor', () => {
const editor = page.locator('.ProseMirror');
await editor.getByText('Hello').dblclick();
/* eslint-disable playwright/no-conditional-expect */
/* eslint-disable playwright/no-conditional-in-test */
if (!ai_transform && !ai_translate) {
await expect(page.getByRole('button', { name: 'AI' })).toBeHidden();
return;
@@ -441,6 +442,45 @@ test.describe('Doc Editor', () => {
page.getByRole('menuitem', { name: 'Language' }),
).toBeHidden();
}
/* eslint-enable playwright/no-conditional-expect */
/* eslint-enable playwright/no-conditional-in-test */
});
});
test('it downloads unsafe files', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1);
const fileChooserPromise = page.waitForEvent('filechooser');
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`svg`);
});
await verifyDocName(page, randomDoc);
await page.locator('.ProseMirror.bn-editor').click();
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
await page.keyboard.press('Enter');
await page.locator('.bn-block-outer').last().fill('/');
await page.getByText('Resizable image with caption').click();
await page.getByText('Upload image').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(path.join(__dirname, 'assets/test.svg'));
await page.locator('.bn-block-content[data-name="test.svg"]').click();
await page.getByRole('button', { name: 'Download image' }).click();
await expect(
page.getByText('This file is flagged as unsafe.'),
).toBeVisible();
await page.getByRole('button', { name: 'Download' }).click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toContain(`-unsafe.svg`);
const svgBuffer = await cs.toBuffer(await download.createReadStream());
expect(svgBuffer.toString()).toContain('Hello svg');
});
});