archetecture security fix
This commit is contained in:
Vendored
+131
@@ -0,0 +1,131 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getStorageRoot = getStorageRoot;
|
||||
exports.getPublicStorageRoot = getPublicStorageRoot;
|
||||
exports.getPrivateStorageRoot = getPrivateStorageRoot;
|
||||
exports.getLegacyStorageRoots = getLegacyStorageRoots;
|
||||
exports.assertStorageConfiguration = assertStorageConfiguration;
|
||||
exports.getProtectedCustomerLicenseImageUrl = getProtectedCustomerLicenseImageUrl;
|
||||
exports.uploadImage = uploadImage;
|
||||
exports.resolveStoredFilePath = resolveStoredFilePath;
|
||||
exports.deleteImage = deleteImage;
|
||||
const fs_1 = __importDefault(require("fs"));
|
||||
const path_1 = __importDefault(require("path"));
|
||||
const crypto_1 = __importDefault(require("crypto"));
|
||||
const APP_PACKAGE_ROOT = path_1.default.resolve(__dirname, '..', '..', '..');
|
||||
const APP_SOURCE_ROOT = path_1.default.join(APP_PACKAGE_ROOT, 'src');
|
||||
const APP_DIST_ROOT = path_1.default.join(APP_PACKAGE_ROOT, 'dist');
|
||||
const DEFAULT_STORAGE_ROOT = path_1.default.join(APP_PACKAGE_ROOT, 'storage');
|
||||
const LEGACY_STORAGE_ROOTS = [
|
||||
path_1.default.join(APP_SOURCE_ROOT, 'lib', 'storage'),
|
||||
];
|
||||
function getStorageRoot() {
|
||||
return process.env.FILE_STORAGE_ROOT
|
||||
? path_1.default.resolve(process.env.FILE_STORAGE_ROOT)
|
||||
: DEFAULT_STORAGE_ROOT;
|
||||
}
|
||||
function getPublicStorageRoot() {
|
||||
return path_1.default.join(getStorageRoot(), 'public');
|
||||
}
|
||||
function getPrivateStorageRoot() {
|
||||
return path_1.default.join(getStorageRoot(), 'private');
|
||||
}
|
||||
function getLegacyStorageRoots() {
|
||||
return LEGACY_STORAGE_ROOTS;
|
||||
}
|
||||
function isWithinPath(targetPath, parentPath) {
|
||||
const relative = path_1.default.relative(parentPath, targetPath);
|
||||
return relative === '' || (!relative.startsWith('..') && !path_1.default.isAbsolute(relative));
|
||||
}
|
||||
function assertStorageConfiguration() {
|
||||
const storageRoot = getStorageRoot();
|
||||
if (process.env.NODE_ENV === 'production' && !process.env.FILE_STORAGE_ROOT) {
|
||||
throw new Error('FILE_STORAGE_ROOT must be set in production so uploads are stored on the mounted volume.');
|
||||
}
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
const forbiddenRoots = [APP_PACKAGE_ROOT, APP_SOURCE_ROOT, APP_DIST_ROOT];
|
||||
const invalidRoot = forbiddenRoots.find((root) => isWithinPath(storageRoot, root));
|
||||
if (invalidRoot) {
|
||||
throw new Error(`FILE_STORAGE_ROOT must point outside the API app tree in production. Received ${storageRoot}, which is inside ${invalidRoot}.`);
|
||||
}
|
||||
}
|
||||
return storageRoot;
|
||||
}
|
||||
function ensureStorageRoot(visibility) {
|
||||
assertStorageConfiguration();
|
||||
const root = visibility === 'public' ? getPublicStorageRoot() : getPrivateStorageRoot();
|
||||
fs_1.default.mkdirSync(root, { recursive: true });
|
||||
return root;
|
||||
}
|
||||
function inferVisibility(folder) {
|
||||
const normalized = folder.replace(/\\/g, '/');
|
||||
if (/\/customers\//.test(`/${normalized}/`))
|
||||
return 'private';
|
||||
if (/\/licenses?\//.test(`/${normalized}/`))
|
||||
return 'private';
|
||||
if (/\/contracts?\//.test(`/${normalized}/`))
|
||||
return 'private';
|
||||
if (/\/documents?\//.test(`/${normalized}/`))
|
||||
return 'private';
|
||||
if (/\/inspections?\//.test(`/${normalized}/`))
|
||||
return 'private';
|
||||
if (/\/internal\//.test(`/${normalized}/`))
|
||||
return 'private';
|
||||
return 'public';
|
||||
}
|
||||
function getApiBase() {
|
||||
return (process.env.API_URL ?? 'http://localhost:4000').replace(/\/$/, '');
|
||||
}
|
||||
function getDashboardBase() {
|
||||
return (process.env.DASHBOARD_URL ??
|
||||
process.env.NEXT_PUBLIC_DASHBOARD_URL ??
|
||||
'http://localhost:3000/dashboard').replace(/\/$/, '');
|
||||
}
|
||||
function getStorageRelativePath(imageUrl) {
|
||||
const marker = '/storage/';
|
||||
const idx = imageUrl.indexOf(marker);
|
||||
if (idx === -1)
|
||||
return null;
|
||||
return imageUrl.slice(idx + marker.length);
|
||||
}
|
||||
function getProtectedCustomerLicenseImageUrl(customerId) {
|
||||
return `${getDashboardBase()}/api/v1/customers/${customerId}/license-image`;
|
||||
}
|
||||
async function uploadImage(buffer, folder, publicId, visibility = inferVisibility(folder)) {
|
||||
const storageRoot = ensureStorageRoot(visibility);
|
||||
const folderPath = path_1.default.join(storageRoot, folder);
|
||||
if (!isWithinPath(folderPath, storageRoot)) {
|
||||
throw new Error('Upload path escapes storage root');
|
||||
}
|
||||
fs_1.default.mkdirSync(folderPath, { recursive: true });
|
||||
const filename = publicId
|
||||
? `${publicId}.jpg`
|
||||
: `${crypto_1.default.randomBytes(16).toString('hex')}.jpg`;
|
||||
fs_1.default.writeFileSync(path_1.default.join(folderPath, filename), buffer);
|
||||
return `${getApiBase()}/storage/${folder}/${filename}`;
|
||||
}
|
||||
function resolveStoredFilePath(imageUrl) {
|
||||
const relative = getStorageRelativePath(imageUrl);
|
||||
if (!relative)
|
||||
return null;
|
||||
const roots = [getPublicStorageRoot(), getPrivateStorageRoot(), ...getLegacyStorageRoots(), assertStorageConfiguration()];
|
||||
for (const root of roots) {
|
||||
const filePath = path_1.default.join(root, relative);
|
||||
if (!isWithinPath(filePath, root))
|
||||
continue;
|
||||
if (fs_1.default.existsSync(filePath)) {
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
async function deleteImage(imageUrl) {
|
||||
const filePath = resolveStoredFilePath(imageUrl);
|
||||
if (filePath && fs_1.default.existsSync(filePath)) {
|
||||
fs_1.default.unlinkSync(filePath);
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=storage.js.map
|
||||
Reference in New Issue
Block a user