Files
picpeak/backend/__tests__/utils/safePathUploadedAssets.test.js
T
Paul Nothaft 882101b586 fix(security): contain logo, favicon and PDF-logo unlinks to their upload directories
Settings > Branding persisted logo_url / favicon_url verbatim and on clear
unlinked path.join(storage, url) behind a startsWith('/uploads/logos/')
check, which '..' segments pass. The business-profile PDF logo did the same
behind a /pdf-logo-\d+\./ marker test, and used absolute values as given.
Either let a settings.edit or settings.banking holder delete any file the
process can reach.

Both now resolve through helpers in utils/safePath that only ever name a
flat leaf inside the fixed directory. The /favicon.ico streamer is narrowed
the same way: it contained to the whole uploads/ root, which also holds
signed contracts and transfer files.

(cherry picked from commit 3e46530072)
2026-09-03 12:12:12 +02:00

60 lines
2.1 KiB
JavaScript

/**
* Containment for the two admin-writable "delete the old file" paths.
*
* Settings → Branding persists logo_url / favicon_url verbatim and, on
* clear, unlinked `path.join(storage, url)` after a mere prefix check.
* Business profile did the same for logo_path behind a `/pdf-logo-\d+\./`
* marker. Both let an admin delete any file the process can reach. The
* helpers below only ever name a flat leaf inside the fixed directory.
*/
const path = require('path');
const { uploadedAssetPath, uploadedPdfLogoPath } = require('../../src/utils/safePath');
const root = '/srv/picpeak/storage';
describe('uploadedAssetPath', () => {
it('resolves a flat leaf inside the named upload directory', () => {
expect(uploadedAssetPath('/uploads/logos/logo-1.png', 'logos', root))
.toBe(path.join(root, 'uploads', 'logos', 'logo-1.png'));
expect(uploadedAssetPath('/uploads/favicons/fav.ico', 'favicons', root))
.toBe(path.join(root, 'uploads', 'favicons', 'fav.ico'));
});
it.each([
'/uploads/logos/../../../data/picpeak.db',
'/uploads/logos/..',
'/uploads/logos/',
'/uploads/logos/sub/dir.png',
'/uploads/favicons/x.ico', // wrong kind
'uploads/logos/logo.png', // not /-rooted
'https://example.com/uploads/logos/logo.png',
'',
null,
42,
])('refuses %p', (value) => {
expect(uploadedAssetPath(value, 'logos', root)).toBeNull();
});
});
describe('uploadedPdfLogoPath', () => {
it('resolves the file the upload route writes', () => {
expect(uploadedPdfLogoPath('/uploads/logos/pdf-logo-1700000000000.png', root))
.toBe(path.join(root, 'uploads', 'logos', 'pdf-logo-1700000000000.png'));
expect(uploadedPdfLogoPath('uploads/logos/pdf-logo-1.svg', root))
.toBe(path.join(root, 'uploads', 'logos', 'pdf-logo-1.svg'));
});
it.each([
'pdf-logo-1./../../../../etc/target',
'/uploads/logos/pdf-logo-1./../../secret',
'/etc/pdf-logo-1.x',
'/uploads/logos/pdf-logo-1.png/../other',
'/uploads/logos/other-logo.png',
'/uploads/contracts/signed/pdf-logo-1.pdf',
'',
null,
])('refuses %p', (value) => {
expect(uploadedPdfLogoPath(value, root)).toBeNull();
});
});