Skip to content

Commit

Permalink
fix(core): Do not allow arbitrary path traversal in BinaryDataManager (
Browse files Browse the repository at this point in the history
  • Loading branch information
netroy authored and janober committed Feb 21, 2023
1 parent fb07d77 commit 40b9784
Show file tree
Hide file tree
Showing 4 changed files with 42 additions and 25 deletions.
34 changes: 20 additions & 14 deletions packages/cli/src/Server.ts
Expand Up @@ -33,6 +33,7 @@ import {
LoadNodeParameterOptions,
LoadNodeListSearch,
UserSettings,
FileNotFoundError,
} from 'n8n-core';

import type {
Expand Down Expand Up @@ -1119,21 +1120,26 @@ class Server extends AbstractServer {
// TODO UM: check if this needs permission check for UM
const identifier = req.params.path;
const binaryDataManager = BinaryDataManager.getInstance();
const binaryPath = binaryDataManager.getBinaryPath(identifier);
let { mode, fileName, mimeType } = req.query;
if (!fileName || !mimeType) {
try {
const metadata = await binaryDataManager.getBinaryMetadata(identifier);
fileName = metadata.fileName;
mimeType = metadata.mimeType;
res.setHeader('Content-Length', metadata.fileSize);
} catch {}
}
if (mimeType) res.setHeader('Content-Type', mimeType);
if (mode === 'download') {
res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
try {
const binaryPath = binaryDataManager.getBinaryPath(identifier);
let { mode, fileName, mimeType } = req.query;
if (!fileName || !mimeType) {
try {
const metadata = await binaryDataManager.getBinaryMetadata(identifier);
fileName = metadata.fileName;
mimeType = metadata.mimeType;
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;
}
res.sendFile(binaryPath);
},
);

Expand Down
27 changes: 16 additions & 11 deletions packages/core/src/BinaryDataManager/FileSystem.ts
Expand Up @@ -7,6 +7,7 @@ import type { BinaryMetadata } from 'n8n-workflow';
import { jsonParse } from 'n8n-workflow';

import type { IBinaryDataConfig, IBinaryDataManager } from '../Interfaces';
import { FileNotFoundError } from '../errors';

const PREFIX_METAFILE = 'binarymeta';
const PREFIX_PERSISTED_METAFILE = 'persistedmeta';
Expand Down Expand Up @@ -85,17 +86,17 @@ export class BinaryDataFileSystem implements IBinaryDataManager {
}

getBinaryPath(identifier: string): string {
return path.join(this.storagePath, identifier);
return this.resolveStoragePath(identifier);
}

getMetadataPath(identifier: string): string {
return path.join(this.storagePath, `${identifier}.metadata`);
return this.resolveStoragePath(`${identifier}.metadata`);
}

async markDataForDeletionByExecutionId(executionId: string): Promise<void> {
const tt = new Date(new Date().getTime() + this.binaryDataTTL * 60000);
return fs.writeFile(
path.join(this.getBinaryDataMetaPath(), `${PREFIX_METAFILE}_${executionId}_${tt.valueOf()}`),
this.resolveStoragePath('meta', `${PREFIX_METAFILE}_${executionId}_${tt.valueOf()}`),
'',
);
}
Expand All @@ -116,8 +117,8 @@ export class BinaryDataFileSystem implements IBinaryDataManager {
const timeAtNextHour = currentTime + 3600000 - (currentTime % 3600000);
const timeoutTime = timeAtNextHour + this.persistedBinaryDataTTL * 60000;

const filePath = path.join(
this.getBinaryDataPersistMetaPath(),
const filePath = this.resolveStoragePath(
'persistMeta',
`${PREFIX_PERSISTED_METAFILE}_${executionId}_${timeoutTime}`,
);

Expand Down Expand Up @@ -170,21 +171,18 @@ export class BinaryDataFileSystem implements IBinaryDataManager {
const newBinaryDataId = this.generateFileName(prefix);

return fs
.copyFile(
path.join(this.storagePath, binaryDataId),
path.join(this.storagePath, newBinaryDataId),
)
.copyFile(this.resolveStoragePath(binaryDataId), this.resolveStoragePath(newBinaryDataId))
.then(() => newBinaryDataId);
}

async deleteBinaryDataByExecutionId(executionId: string): Promise<void> {
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(
(allProms, filename) => {
if (regex.test(filename)) {
allProms.push(fs.rm(path.join(this.storagePath, filename)));
allProms.push(fs.rm(this.resolveStoragePath(filename)));
}

return allProms;
Expand Down Expand Up @@ -253,4 +251,11 @@ export class BinaryDataFileSystem implements IBinaryDataManager {
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 changes: 5 additions & 0 deletions packages/core/src/errors.ts
@@ -0,0 +1,5 @@
export class FileNotFoundError extends Error {
constructor(readonly filePath: string) {
super(`File not found: ${filePath}`);
}
}
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Expand Up @@ -15,6 +15,7 @@ export * from './LoadNodeListSearch';
export * from './NodeExecuteFunctions';
export * from './WorkflowExecute';
export { eventEmitter, NodeExecuteFunctions, UserSettings };
export * from './errors';

declare module 'http' {
export interface IncomingMessage {
Expand Down

0 comments on commit 40b9784

Please sign in to comment.