feat(tests): add unit tests for readValidDirectory function to validate path traversal logic

This commit is contained in:
Mauricio Siu
2026-02-17 14:22:20 -06:00
parent 06fd561bb1
commit b58f2b236f
8 changed files with 269 additions and 18 deletions

View File

@@ -3,6 +3,7 @@ import path, { join } from "node:path";
import { paths } from "@dokploy/server/constants";
import type { Application } from "@dokploy/server/services/application";
import { findServerById } from "@dokploy/server/services/server";
import { readValidDirectory } from "@dokploy/server/wss/utils";
import AdmZip from "adm-zip";
import { Client, type SFTPWrapper } from "ssh2";
import {
@@ -62,6 +63,21 @@ export const unzipDrop = async (zipFile: File, application: Application) => {
if (!filePath) continue;
const fullPath = path.join(outputPath, filePath).replace(/\\/g, "/");
if (!readValidDirectory(fullPath, application.serverId)) {
throw new Error(
`Path traversal detected: resolved path escapes output directory: ${filePath}`,
);
}
if (isSymlinkEntry(entry)) {
throw new Error(`Symlink entries are not allowed: ${entry.entryName}`);
}
if (isDangerousNode(entry)) {
throw new Error(
`Dangerous node entries are not allowed: ${entry.entryName}`,
);
}
if (application.serverId) {
if (!entry.isDirectory) {
@@ -132,3 +148,20 @@ const uploadFileToServer = (
});
});
};
function isSymlinkEntry(entry: AdmZip.IZipEntry) {
// upper 16 bits = unix permissions
const unix = (entry.header.attr >> 16) & 0o170000;
return unix === 0o120000;
}
function isDangerousNode(entry: AdmZip.IZipEntry) {
const type = (entry.header.attr >> 16) & 0o170000;
return (
type === 0o120000 || // symlink
type === 0o060000 || // block device
type === 0o020000 || // char device
type === 0o010000 // fifo/pipe
);
}