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.
This commit is contained in:
@@ -171,8 +171,50 @@ function assertZipEntriesWithin(entries, extractRoot) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a stored `/uploads/<kind>/<file>` URL to the file it names inside
|
||||
* that upload directory, or null when the value is not one of ours.
|
||||
*
|
||||
* Only the basename is trusted: the URL comes from an admin-writable
|
||||
* setting, and `path.join(storage, url)` after a `startsWith('/uploads/…')`
|
||||
* check still collapses `..` segments, so it could name any file the process
|
||||
* can delete. Restricting to a flat leaf inside the fixed directory is the
|
||||
* whole control -- the upload routes only ever write flat filenames there.
|
||||
*
|
||||
* @param {string} url stored value, e.g. "/uploads/logos/logo-1.png"
|
||||
* @param {string} kind "logos" | "favicons"
|
||||
* @param {string} storageRoot the root the writer used (callers differ)
|
||||
*/
|
||||
function uploadedAssetPath(url, kind, storageRoot) {
|
||||
if (!url || typeof url !== 'string') return null;
|
||||
const prefix = `/uploads/${kind}/`;
|
||||
if (!url.startsWith(prefix)) return null;
|
||||
const leaf = url.slice(prefix.length);
|
||||
if (!leaf || leaf === '.' || leaf === '..' || path.basename(leaf) !== leaf) return null;
|
||||
return path.join(storageRoot, 'uploads', kind, leaf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve business_profile.logo_path to the file the PDF-logo upload route
|
||||
* wrote, or null. logo_path is a free-text field on the profile PUT (an
|
||||
* admin may point it at a file managed elsewhere), so it must never be
|
||||
* unlinked as given: a `/pdf-logo-\d+\./` marker test plus path.join let
|
||||
* `pdf-logo-1./../../../<anything>` -- or any absolute path containing the
|
||||
* marker -- delete arbitrary files. Only a flat `pdf-logo-<n>.<ext>` leaf
|
||||
* inside uploads/logos is ever named.
|
||||
*/
|
||||
function uploadedPdfLogoPath(logoPath, storageRoot) {
|
||||
if (!logoPath || typeof logoPath !== 'string') return null;
|
||||
const normalized = logoPath.replace(/^\/+/, '');
|
||||
const match = /^uploads\/logos\/(pdf-logo-\d+\.[A-Za-z0-9]+)$/.exec(normalized);
|
||||
if (!match) return null;
|
||||
return path.join(storageRoot, 'uploads', 'logos', match[1]);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
assertPathInside,
|
||||
assertContractPdfPath,
|
||||
assertZipEntriesWithin,
|
||||
uploadedAssetPath,
|
||||
uploadedPdfLogoPath,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user