Skip to content

Commit 40b9784

Browse files
netroyjanober
authored andcommitted
fix(core): Do not allow arbitrary path traversal in BinaryDataManager (#5523)
1 parent fb07d77 commit 40b9784

File tree

4 files changed

+42
-25
lines changed

4 files changed

+42
-25
lines changed

packages/cli/src/Server.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
LoadNodeParameterOptions,
3434
LoadNodeListSearch,
3535
UserSettings,
36+
FileNotFoundError,
3637
} from 'n8n-core';
3738

3839
import type {
@@ -1119,21 +1120,26 @@ class Server extends AbstractServer {
11191120
// TODO UM: check if this needs permission check for UM
11201121
const identifier = req.params.path;
11211122
const binaryDataManager = BinaryDataManager.getInstance();
1122-
const binaryPath = binaryDataManager.getBinaryPath(identifier);
1123-
let { mode, fileName, mimeType } = req.query;
1124-
if (!fileName || !mimeType) {
1125-
try {
1126-
const metadata = await binaryDataManager.getBinaryMetadata(identifier);
1127-
fileName = metadata.fileName;
1128-
mimeType = metadata.mimeType;
1129-
res.setHeader('Content-Length', metadata.fileSize);
1130-
} catch {}
1131-
}
1132-
if (mimeType) res.setHeader('Content-Type', mimeType);
1133-
if (mode === 'download') {
1134-
res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
1123+
try {
1124+
const binaryPath = binaryDataManager.getBinaryPath(identifier);
1125+
let { mode, fileName, mimeType } = req.query;
1126+
if (!fileName || !mimeType) {
1127+
try {
1128+
const metadata = await binaryDataManager.getBinaryMetadata(identifier);
1129+
fileName = metadata.fileName;
1130+
mimeType = metadata.mimeType;
1131+
res.setHeader('Content-Length', metadata.fileSize);
1132+
} catch {}
1133+
}
1134+
if (mimeType) res.setHeader('Content-Type', mimeType);
1135+
if (mode === 'download') {
1136+
res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
1137+
}
1138+
res.sendFile(binaryPath);
1139+
} catch (error) {
1140+
if (error instanceof FileNotFoundError) res.writeHead(404).end();
1141+
else throw error;
11351142
}
1136-
res.sendFile(binaryPath);
11371143
},
11381144
);
11391145

packages/core/src/BinaryDataManager/FileSystem.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { BinaryMetadata } from 'n8n-workflow';
77
import { jsonParse } from 'n8n-workflow';
88

99
import type { IBinaryDataConfig, IBinaryDataManager } from '../Interfaces';
10+
import { FileNotFoundError } from '../errors';
1011

1112
const PREFIX_METAFILE = 'binarymeta';
1213
const PREFIX_PERSISTED_METAFILE = 'persistedmeta';
@@ -85,17 +86,17 @@ export class BinaryDataFileSystem implements IBinaryDataManager {
8586
}
8687

8788
getBinaryPath(identifier: string): string {
88-
return path.join(this.storagePath, identifier);
89+
return this.resolveStoragePath(identifier);
8990
}
9091

9192
getMetadataPath(identifier: string): string {
92-
return path.join(this.storagePath, `${identifier}.metadata`);
93+
return this.resolveStoragePath(`${identifier}.metadata`);
9394
}
9495

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

119-
const filePath = path.join(
120-
this.getBinaryDataPersistMetaPath(),
120+
const filePath = this.resolveStoragePath(
121+
'persistMeta',
121122
`${PREFIX_PERSISTED_METAFILE}_${executionId}_${timeoutTime}`,
122123
);
123124

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

172173
return fs
173-
.copyFile(
174-
path.join(this.storagePath, binaryDataId),
175-
path.join(this.storagePath, newBinaryDataId),
176-
)
174+
.copyFile(this.resolveStoragePath(binaryDataId), this.resolveStoragePath(newBinaryDataId))
177175
.then(() => newBinaryDataId);
178176
}
179177

180178
async deleteBinaryDataByExecutionId(executionId: string): Promise<void> {
181179
const regex = new RegExp(`${executionId}_*`);
182-
const filenames = await fs.readdir(path.join(this.storagePath));
180+
const filenames = await fs.readdir(this.storagePath);
183181

184182
const proms = filenames.reduce(
185183
(allProms, filename) => {
186184
if (regex.test(filename)) {
187-
allProms.push(fs.rm(path.join(this.storagePath, filename)));
185+
allProms.push(fs.rm(this.resolveStoragePath(filename)));
188186
}
189187

190188
return allProms;
@@ -253,4 +251,11 @@ export class BinaryDataFileSystem implements IBinaryDataManager {
253251
throw new Error(`Error finding file: ${filePath}`);
254252
}
255253
}
254+
255+
private resolveStoragePath(...args: string[]) {
256+
const returnPath = path.join(this.storagePath, ...args);
257+
if (path.relative(this.storagePath, returnPath).startsWith('..'))
258+
throw new FileNotFoundError('Invalid path detected');
259+
return returnPath;
260+
}
256261
}

packages/core/src/errors.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export class FileNotFoundError extends Error {
2+
constructor(readonly filePath: string) {
3+
super(`File not found: ${filePath}`);
4+
}
5+
}

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export * from './LoadNodeListSearch';
1515
export * from './NodeExecuteFunctions';
1616
export * from './WorkflowExecute';
1717
export { eventEmitter, NodeExecuteFunctions, UserSettings };
18+
export * from './errors';
1819

1920
declare module 'http' {
2021
export interface IncomingMessage {

0 commit comments

Comments
 (0)