fix(core): Do not allow arbitrary path traversal in BinaryDataManager (#5523)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2023-02-21 11:21:17 +01:00 committed by GitHub
parent f0f8d59fee
commit eef2574067
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 42 additions and 25 deletions

View file

@ -33,6 +33,7 @@ import {
LoadNodeParameterOptions, LoadNodeParameterOptions,
LoadNodeListSearch, LoadNodeListSearch,
UserSettings, UserSettings,
FileNotFoundError,
} from 'n8n-core'; } from 'n8n-core';
import type { import type {
@ -1149,21 +1150,26 @@ class Server extends AbstractServer {
// TODO UM: check if this needs permission check for UM // TODO UM: check if this needs permission check for UM
const identifier = req.params.path; const identifier = req.params.path;
const binaryDataManager = BinaryDataManager.getInstance(); const binaryDataManager = BinaryDataManager.getInstance();
const binaryPath = binaryDataManager.getBinaryPath(identifier); try {
let { mode, fileName, mimeType } = req.query; const binaryPath = binaryDataManager.getBinaryPath(identifier);
if (!fileName || !mimeType) { let { mode, fileName, mimeType } = req.query;
try { if (!fileName || !mimeType) {
const metadata = await binaryDataManager.getBinaryMetadata(identifier); try {
fileName = metadata.fileName; const metadata = await binaryDataManager.getBinaryMetadata(identifier);
mimeType = metadata.mimeType; fileName = metadata.fileName;
res.setHeader('Content-Length', metadata.fileSize); mimeType = metadata.mimeType;
} catch {} res.setHeader('Content-Length', metadata.fileSize);
} catch {}
}
if (mimeType) res.setHeader('Content-Type', mimeType);
if (mode === 'download') {
res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
}
res.sendFile(binaryPath);
} catch (error) {
if (error instanceof FileNotFoundError) res.writeHead(404).end();
else throw error;
} }
if (mimeType) res.setHeader('Content-Type', mimeType);
if (mode === 'download') {
res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
}
res.sendFile(binaryPath);
}, },
); );

View file

@ -7,6 +7,7 @@ import type { BinaryMetadata } from 'n8n-workflow';
import { jsonParse } from 'n8n-workflow'; import { jsonParse } from 'n8n-workflow';
import type { IBinaryDataConfig, IBinaryDataManager } from '../Interfaces'; import type { IBinaryDataConfig, IBinaryDataManager } from '../Interfaces';
import { FileNotFoundError } from '../errors';
const PREFIX_METAFILE = 'binarymeta'; const PREFIX_METAFILE = 'binarymeta';
const PREFIX_PERSISTED_METAFILE = 'persistedmeta'; const PREFIX_PERSISTED_METAFILE = 'persistedmeta';
@ -85,17 +86,17 @@ export class BinaryDataFileSystem implements IBinaryDataManager {
} }
getBinaryPath(identifier: string): string { getBinaryPath(identifier: string): string {
return path.join(this.storagePath, identifier); return this.resolveStoragePath(identifier);
} }
getMetadataPath(identifier: string): string { getMetadataPath(identifier: string): string {
return path.join(this.storagePath, `${identifier}.metadata`); return this.resolveStoragePath(`${identifier}.metadata`);
} }
async markDataForDeletionByExecutionId(executionId: string): Promise<void> { async markDataForDeletionByExecutionId(executionId: string): Promise<void> {
const tt = new Date(new Date().getTime() + this.binaryDataTTL * 60000); const tt = new Date(new Date().getTime() + this.binaryDataTTL * 60000);
return fs.writeFile( return fs.writeFile(
path.join(this.getBinaryDataMetaPath(), `${PREFIX_METAFILE}_${executionId}_${tt.valueOf()}`), this.resolveStoragePath('meta', `${PREFIX_METAFILE}_${executionId}_${tt.valueOf()}`),
'', '',
); );
} }
@ -116,8 +117,8 @@ export class BinaryDataFileSystem implements IBinaryDataManager {
const timeAtNextHour = currentTime + 3600000 - (currentTime % 3600000); const timeAtNextHour = currentTime + 3600000 - (currentTime % 3600000);
const timeoutTime = timeAtNextHour + this.persistedBinaryDataTTL * 60000; const timeoutTime = timeAtNextHour + this.persistedBinaryDataTTL * 60000;
const filePath = path.join( const filePath = this.resolveStoragePath(
this.getBinaryDataPersistMetaPath(), 'persistMeta',
`${PREFIX_PERSISTED_METAFILE}_${executionId}_${timeoutTime}`, `${PREFIX_PERSISTED_METAFILE}_${executionId}_${timeoutTime}`,
); );
@ -170,21 +171,18 @@ export class BinaryDataFileSystem implements IBinaryDataManager {
const newBinaryDataId = this.generateFileName(prefix); const newBinaryDataId = this.generateFileName(prefix);
return fs return fs
.copyFile( .copyFile(this.resolveStoragePath(binaryDataId), this.resolveStoragePath(newBinaryDataId))
path.join(this.storagePath, binaryDataId),
path.join(this.storagePath, newBinaryDataId),
)
.then(() => newBinaryDataId); .then(() => newBinaryDataId);
} }
async deleteBinaryDataByExecutionId(executionId: string): Promise<void> { async deleteBinaryDataByExecutionId(executionId: string): Promise<void> {
const regex = new RegExp(`${executionId}_*`); const regex = new RegExp(`${executionId}_*`);
const filenames = await fs.readdir(path.join(this.storagePath)); const filenames = await fs.readdir(this.storagePath);
const proms = filenames.reduce( const proms = filenames.reduce(
(allProms, filename) => { (allProms, filename) => {
if (regex.test(filename)) { if (regex.test(filename)) {
allProms.push(fs.rm(path.join(this.storagePath, filename))); allProms.push(fs.rm(this.resolveStoragePath(filename)));
} }
return allProms; return allProms;
@ -253,4 +251,11 @@ export class BinaryDataFileSystem implements IBinaryDataManager {
throw new Error(`Error finding file: ${filePath}`); throw new Error(`Error finding file: ${filePath}`);
} }
} }
private resolveStoragePath(...args: string[]) {
const returnPath = path.join(this.storagePath, ...args);
if (path.relative(this.storagePath, returnPath).startsWith('..'))
throw new FileNotFoundError('Invalid path detected');
return returnPath;
}
} }

View file

@ -0,0 +1,5 @@
export class FileNotFoundError extends Error {
constructor(readonly filePath: string) {
super(`File not found: ${filePath}`);
}
}

View file

@ -15,6 +15,7 @@ export * from './LoadNodeListSearch';
export * from './NodeExecuteFunctions'; export * from './NodeExecuteFunctions';
export * from './WorkflowExecute'; export * from './WorkflowExecute';
export { eventEmitter, NodeExecuteFunctions, UserSettings }; export { eventEmitter, NodeExecuteFunctions, UserSettings };
export * from './errors';
declare module 'http' { declare module 'http' {
export interface IncomingMessage { export interface IncomingMessage {