From 734f0a292d31b3672811a3093f731f64ae04fd23 Mon Sep 17 00:00:00 2001 From: Zimeng Xiong Date: Fri, 6 Feb 2026 22:28:36 -0800 Subject: [PATCH] fix graphQL --- backend/src/index.ts | 1 + backend/src/routes/importExport.ts | 78 ++++++++++++++++++++++++++++-- 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index aeaf023..9de37b5 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -907,6 +907,7 @@ registerImportExportRoutes({ requireAuth, asyncHandler, upload, + uploadDir, config, backendRoot, getBackendVersion, diff --git a/backend/src/routes/importExport.ts b/backend/src/routes/importExport.ts index e85cc6c..9debcfd 100644 --- a/backend/src/routes/importExport.ts +++ b/backend/src/routes/importExport.ts @@ -55,6 +55,7 @@ type RegisterImportExportDeps = { fn: (req: express.Request, res: express.Response, next: express.NextFunction) => Promise ) => express.RequestHandler; upload: any; + uploadDir: string; config: { nodeEnv: string }; backendRoot: string; getBackendVersion: () => string; @@ -154,6 +155,42 @@ const parseOptionalJson = (raw: unknown, fallback: T): T => { return fallback; }; +const isPathInsideDirectory = (candidatePath: string, rootDir: string): boolean => { + const relativePath = path.relative(rootDir, candidatePath); + return ( + relativePath === "" || + (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)) + ); +}; + +const resolveSafeUploadedFilePath = async ( + filePath: unknown, + uploadRoot: string +): Promise => { + if (typeof filePath !== "string" || filePath.trim().length === 0) { + throw new ImportValidationError("Invalid upload path"); + } + + const absoluteUploadRoot = path.resolve(uploadRoot); + const absoluteFilePath = path.resolve(filePath); + + let canonicalUploadRoot = absoluteUploadRoot; + let canonicalFilePath = absoluteFilePath; + + try { + canonicalUploadRoot = await fsPromises.realpath(absoluteUploadRoot); + canonicalFilePath = await fsPromises.realpath(absoluteFilePath); + } catch { + throw new ImportValidationError("Invalid upload path"); + } + + if (!isPathInsideDirectory(canonicalFilePath, canonicalUploadRoot)) { + throw new ImportValidationError("Invalid upload path"); + } + + return canonicalFilePath; +}; + const openReadonlySqliteDb = (filePath: string): any => { try { // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -192,6 +229,7 @@ export const registerImportExportRoutes = (deps: RegisterImportExportDeps) => { requireAuth, asyncHandler, upload, + uploadDir, config, backendRoot, getBackendVersion, @@ -337,7 +375,15 @@ Drawings: ${drawings.length} if (!req.user) return res.status(401).json({ error: "Unauthorized" }); if (!req.file) return res.status(400).json({ error: "No file uploaded" }); - const stagedPath = req.file.path; + let stagedPath: string; + try { + stagedPath = await resolveSafeUploadedFilePath(req.file.path, uploadDir); + } catch (error) { + if (error instanceof ImportValidationError) { + return res.status(error.status).json({ error: "Invalid upload", message: error.message }); + } + throw error; + } try { const buffer = await fsPromises.readFile(stagedPath); const zip = await JSZip.loadAsync(buffer); @@ -417,7 +463,15 @@ Drawings: ${drawings.length} if (!req.user) return res.status(401).json({ error: "Unauthorized" }); if (!req.file) return res.status(400).json({ error: "No file uploaded" }); - const stagedPath = req.file.path; + let stagedPath: string; + try { + stagedPath = await resolveSafeUploadedFilePath(req.file.path, uploadDir); + } catch (error) { + if (error instanceof ImportValidationError) { + return res.status(error.status).json({ error: "Invalid upload", message: error.message }); + } + throw error; + } try { const buffer = await fsPromises.readFile(stagedPath); const zip = await JSZip.loadAsync(buffer); @@ -666,7 +720,15 @@ Drawings: ${drawings.length} if (!req.user) return res.status(401).json({ error: "Unauthorized" }); if (!req.file) return res.status(400).json({ error: "No file uploaded" }); - const stagedPath = req.file.path; + let stagedPath: string; + try { + stagedPath = await resolveSafeUploadedFilePath(req.file.path, uploadDir); + } catch (error) { + if (error instanceof ImportValidationError) { + return res.status(error.status).json({ error: "Invalid upload", message: error.message }); + } + throw error; + } try { const isValid = await verifyDatabaseIntegrityAsync(stagedPath); if (!isValid) return res.status(400).json({ error: "Invalid database format" }); @@ -744,7 +806,15 @@ Drawings: ${drawings.length} if (!req.user) return res.status(401).json({ error: "Unauthorized" }); if (!req.file) return res.status(400).json({ error: "No file uploaded" }); - const stagedPath = req.file.path; + let stagedPath: string; + try { + stagedPath = await resolveSafeUploadedFilePath(req.file.path, uploadDir); + } catch (error) { + if (error instanceof ImportValidationError) { + return res.status(error.status).json({ error: "Invalid upload", message: error.message }); + } + throw error; + } try { const isValid = await verifyDatabaseIntegrityAsync(stagedPath); if (!isValid) return res.status(400).json({ error: "Invalid database format" });