diff --git a/packages/nocodb/src/plugins/storage/Local.ts b/packages/nocodb/src/plugins/storage/Local.ts index f0dbebf2a4..7d49e54f70 100644 --- a/packages/nocodb/src/plugins/storage/Local.ts +++ b/packages/nocodb/src/plugins/storage/Local.ts @@ -3,6 +3,7 @@ import path from 'path'; import { promisify } from 'util'; import mkdirp from 'mkdirp'; import axios from 'axios'; +import { NcError } from '../../helpers/catchError'; import { getToolDir } from '../../utils/nc-config'; import type { IStorageAdapterV2, XcFile } from 'nc-plugin'; import type { Readable } from 'stream'; @@ -11,7 +12,7 @@ export default class Local implements IStorageAdapterV2 { constructor() {} public async fileCreate(key: string, file: XcFile): Promise { - const destPath = path.join(getToolDir(), ...key.split('/')); + const destPath = this.validateAndNormalisePath(key); try { await mkdirp(path.dirname(destPath)); const data = await promisify(fs.readFile)(file.path); @@ -24,7 +25,7 @@ export default class Local implements IStorageAdapterV2 { } async fileCreateByUrl(key: string, url: string): Promise { - const destPath = path.join(getToolDir(), ...key.split('/')); + const destPath = this.validateAndNormalisePath(key); return new Promise((resolve, reject) => { axios .get(url, { @@ -71,7 +72,7 @@ export default class Local implements IStorageAdapterV2 { stream: Readable, ): Promise { return new Promise((resolve, reject) => { - const destPath = path.join(getToolDir(), ...key.split('/')); + const destPath = this.validateAndNormalisePath(key); try { mkdirp(path.dirname(destPath)).then(() => { const writableStream = fs.createWriteStream(destPath); @@ -86,12 +87,12 @@ export default class Local implements IStorageAdapterV2 { } public async fileReadByStream(key: string): Promise { - const srcPath = path.join(getToolDir(), ...key.split('/')); + const srcPath = this.validateAndNormalisePath(key); return fs.createReadStream(srcPath, { encoding: 'utf8' }); } public async getDirectoryList(key: string): Promise { - const destDir = path.join(getToolDir(), ...key.split('/')); + const destDir = this.validateAndNormalisePath(key); return fs.promises.readdir(destDir); } @@ -103,7 +104,7 @@ export default class Local implements IStorageAdapterV2 { public async fileRead(filePath: string): Promise { try { const fileData = await fs.promises.readFile( - path.join(getToolDir(), ...filePath.split('/')), + this.validateAndNormalisePath(filePath, true), ); return fileData; } catch (e) { @@ -118,4 +119,29 @@ export default class Local implements IStorageAdapterV2 { test(): Promise { return Promise.resolve(false); } + + // method for validate/normalise the path for avoid path traversal attack + protected validateAndNormalisePath( + fileOrFolderPath: string, + throw404 = false, + ): string { + // Get the absolute path to the base directory + const absoluteBasePath = path.resolve(getToolDir(), 'nc'); + + // Get the absolute path to the file + const absolutePath = path.resolve( + path.join(getToolDir(), ...fileOrFolderPath.split('/')), + ); + + // Check if the resolved path is within the intended directory + if (!absolutePath.startsWith(absoluteBasePath)) { + if (throw404) { + NcError.notFound(); + } else { + NcError.badRequest('Invalid path'); + } + } + + return absolutePath; + } }