mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 04:34:06 -08:00
fix(core): Do not allow arbitrary path traversal in BinaryDataManager (#5523)
This commit is contained in:
parent
f0f8d59fee
commit
eef2574067
|
@ -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);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
5
packages/core/src/errors.ts
Normal file
5
packages/core/src/errors.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export class FileNotFoundError extends Error {
|
||||||
|
constructor(readonly filePath: string) {
|
||||||
|
super(`File not found: ${filePath}`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue