From 590e3009da2ef01b90edb059c7a612683fd02000 Mon Sep 17 00:00:00 2001 From: Jaakko Husso Date: Tue, 30 Sep 2025 16:34:41 +0300 Subject: [PATCH] feat: Use safer joins in scan-community-package (no-changelog) (#20202) --- .../scanner/scanner.mjs | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/@n8n/scan-community-package/scanner/scanner.mjs b/packages/@n8n/scan-community-package/scanner/scanner.mjs index 976697a4f28..d41d682ee08 100644 --- a/packages/@n8n/scan-community-package/scanner/scanner.mjs +++ b/packages/@n8n/scan-community-package/scanner/scanner.mjs @@ -16,6 +16,38 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); const TEMP_DIR = tmp.dirSync({ unsafeCleanup: true }).name; const registry = 'https://registry.npmjs.org/'; +/** + * Checks if the given childPath is contained within the parentPath. Resolves + * the paths before comparing them, so that relative paths are also supported. + */ +export function isContainedWithin(parentPath, childPath) { + parentPath = path.resolve(parentPath); + childPath = path.resolve(childPath); + + if (parentPath === childPath) { + return true; + } + + return childPath.startsWith(parentPath + path.sep); +} + +/** + * Joins the given paths to the parentPath, ensuring that the resulting path + * is still contained within the parentPath. If not, it throws an error to + * prevent path traversal vulnerabilities. + * + * @throws {UnexpectedError} If the resulting path is not contained within the parentPath. + */ +export function safeJoinPath(parentPath, ...paths) { + const candidate = path.join(parentPath, ...paths); + + if (!isContainedWithin(parentPath, candidate)) { + throw new Error( + `Path traversal detected, refusing to join paths: ${parentPath} and ${JSON.stringify(paths)}`, + ); + } +} + export const resolvePackage = (packageSpec) => { // Validate input to prevent command injection if (!/^[a-zA-Z0-9@/_.-]+$/.test(packageSpec)) { @@ -57,7 +89,7 @@ const downloadAndExtractPackage = async (packageName, version) => { } // Unpack the tarball - const packageDir = path.join(TEMP_DIR, `${packageName}-${version}`); + const packageDir = safeJoinPath(TEMP_DIR, `${packageName}-${version}`); fs.mkdirSync(packageDir, { recursive: true }); const tarResult = spawnSync( 'tar', @@ -70,7 +102,7 @@ const downloadAndExtractPackage = async (packageName, version) => { if (tarResult.status !== 0) { throw new Error(`tar extraction failed: ${tarResult.stderr?.toString()}`); } - fs.unlinkSync(path.join(TEMP_DIR, tarballName)); + fs.unlinkSync(safeJoinPath(TEMP_DIR, tarballName)); return packageDir; } catch (error) {