-
Notifications
You must be signed in to change notification settings - Fork 51.7k
fix(core): Only resolve the filepath once #22767
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 8 commits
c3f6350
5a7bf76
77d6244
ea4923a
671881a
ef47707
52be596
5dc9144
2e752b4
7140e57
1fdf078
b8df2f6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,10 @@ | ||
| import { isContainedWithin, safeJoinPath } from '@n8n/backend-common'; | ||
| import { SecurityConfig } from '@n8n/config'; | ||
| import { Container } from '@n8n/di'; | ||
| import type { FileSystemHelperFunctions, INode } from 'n8n-workflow'; | ||
| import { NodeOperationError } from 'n8n-workflow'; | ||
| import { createReadStream } from 'node:fs'; | ||
| import type { FileSystemHelperFunctions, INode, ResolvedFilePath } from 'n8n-workflow'; | ||
| import type { PathLike } from 'node:fs'; | ||
| import { constants, createReadStream } from 'node:fs'; | ||
| import { | ||
| access as fsAccess, | ||
| writeFile as fsWriteFile, | ||
|
|
@@ -35,18 +36,19 @@ const getAllowedPaths = () => { | |
| return allowedPaths; | ||
| }; | ||
|
|
||
| export async function isFilePathBlocked(filePath: string): Promise<boolean> { | ||
| const allowedPaths = getAllowedPaths(); | ||
| let resolvedFilePath = ''; | ||
| const resolvePath = async (path: PathLike): Promise<ResolvedFilePath> => { | ||
| try { | ||
| resolvedFilePath = await fsRealpath(filePath); | ||
| return (await fsRealpath(path)) as ResolvedFilePath; // apply brand, since we know it's resolved now | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Branded types seem like a smart way to enforce this 👍 |
||
| } catch (error: unknown) { | ||
| if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { | ||
| resolvedFilePath = resolve(filePath); | ||
| } else { | ||
| throw error; | ||
| return resolve(path.toString()) as ResolvedFilePath; // apply brand, since we know it's resolved now | ||
| } | ||
| throw error; | ||
| } | ||
| }; | ||
|
|
||
| function isFilePathBlocked(resolvedFilePath: ResolvedFilePath): boolean { | ||
| const allowedPaths = getAllowedPaths(); | ||
| const blockFileAccessToN8nFiles = process.env[BLOCK_FILE_ACCESS_TO_N8N_FILES] !== 'false'; | ||
|
|
||
| const restrictedPaths = blockFileAccessToN8nFiles ? getN8nRestrictedPaths() : []; | ||
|
|
@@ -64,8 +66,8 @@ export async function isFilePathBlocked(filePath: string): Promise<boolean> { | |
| } | ||
|
|
||
| export const getFileSystemHelperFunctions = (node: INode): FileSystemHelperFunctions => ({ | ||
| async createReadStream(filePath) { | ||
| if (await isFilePathBlocked(filePath.toString())) { | ||
| async createReadStream(resolvedFilePath) { | ||
| if (isFilePathBlocked(resolvedFilePath)) { | ||
| const allowedPaths = getAllowedPaths(); | ||
| const message = allowedPaths.length ? ` Allowed paths: ${allowedPaths.join(', ')}` : ''; | ||
| throw new NodeOperationError(node, `Access to the file is not allowed.${message}`, { | ||
|
|
@@ -74,34 +76,59 @@ export const getFileSystemHelperFunctions = (node: INode): FileSystemHelperFunct | |
| } | ||
|
|
||
| try { | ||
| await fsAccess(filePath); | ||
| await fsAccess(resolvedFilePath); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just a note: the NodeJS docs say: |
||
| } catch (error) { | ||
| // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access | ||
| throw error.code === 'ENOENT' | ||
| ? // eslint-disable-next-line @typescript-eslint/no-unsafe-argument | ||
| new NodeOperationError(node, error, { | ||
| message: `The file "${String(filePath)}" could not be accessed.`, | ||
| message: `The file "${String(resolvedFilePath)}" could not be accessed.`, | ||
| level: 'warning', | ||
| }) | ||
| : error; | ||
| } | ||
|
|
||
| return createReadStream(filePath); | ||
| // Use O_NOFOLLOW to prevent createReadStream from following symlinks. We require that the path | ||
| // already be resolved beforehand. | ||
| const stream = createReadStream(resolvedFilePath, { | ||
| flags: (constants.O_RDONLY | constants.O_NOFOLLOW) as unknown as string, | ||
| }); | ||
|
|
||
| return await new Promise<ReturnType<typeof createReadStream>>((resolve, reject) => { | ||
| stream.once('error', (error) => { | ||
| if ((error as NodeJS.ErrnoException).code === 'ELOOP') { | ||
| reject( | ||
| new NodeOperationError( | ||
| node, | ||
| `The file "${String(resolvedFilePath)}" could not be opened.`, | ||
| { level: 'warning' }, | ||
|
||
| ), | ||
| ); | ||
| } else { | ||
| reject(error); | ||
| } | ||
| }); | ||
| stream.once('open', () => resolve(stream)); | ||
| }); | ||
| }, | ||
|
|
||
| getStoragePath() { | ||
| return safeJoinPath(Container.get(InstanceSettings).n8nFolder, `storage/${node.type}`); | ||
| }, | ||
|
|
||
| async writeContentToFile(filePath, content, flag) { | ||
| if (await isFilePathBlocked(filePath as string)) { | ||
| throw new NodeOperationError(node, `The file "${String(filePath)}" is not writable.`, { | ||
| level: 'warning', | ||
| }); | ||
| async writeContentToFile(resolvedFilePath, content, flag) { | ||
| if (isFilePathBlocked(resolvedFilePath)) { | ||
| throw new NodeOperationError( | ||
| node, | ||
| `The file "${String(resolvedFilePath)}" is not writable.`, | ||
| { | ||
| level: 'warning', | ||
| }, | ||
| ); | ||
| } | ||
| return await fsWriteFile(filePath, content, { encoding: 'binary', flag }); | ||
| return await fsWriteFile(resolvedFilePath, content, { encoding: 'binary', flag }); | ||
|
||
| }, | ||
|
|
||
| resolvePath, | ||
| isFilePathBlocked, | ||
| }); | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Rest of the functions in this file have been defined with
function. Maybe we could keep it consistent?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.