Merge branch 'canary' into patches-impl

This commit is contained in:
Mauricio Siu
2026-02-18 02:09:57 -06:00
19 changed files with 626 additions and 65 deletions

View File

@@ -33,6 +33,7 @@ export * from "./services/port";
export * from "./services/postgres";
export * from "./services/preview-deployment";
export * from "./services/project";
export * from "./services/proprietary/license-key";
export * from "./services/proprietary/sso";
export * from "./services/redirect";
export * from "./services/redis";

View File

@@ -0,0 +1,24 @@
import { db } from "@dokploy/server/db";
import { user } from "@dokploy/server/db/schema";
import { eq } from "drizzle-orm";
import { getOrganizationOwnerId } from "./sso";
export const hasValidLicense = async (organizationId: string) => {
const ownerId = await getOrganizationOwnerId(organizationId);
if (!ownerId) {
return false;
}
const currentUser = await db.query.user.findFirst({
where: eq(user.id, ownerId),
columns: {
enableEnterpriseFeatures: true,
isValidEnterpriseLicense: true,
},
});
return !!(
currentUser?.enableEnterpriseFeatures &&
currentUser?.isValidEnterpriseLicense
);
};

View File

@@ -1,4 +1,6 @@
import { db } from "@dokploy/server/db";
import { organization } from "@dokploy/server/db/schema";
import { eq } from "drizzle-orm";
export const getSSOProviders = async () => {
const providers = await db.query.ssoProvider.findMany({
@@ -33,3 +35,12 @@ export const normalizeTrustedOrigin = (value: string): string => {
// e.g. "https://example.com/" -> "https://example.com"
return value.trim().replace(/\/+$/, "");
};
export const getOrganizationOwnerId = async (organizationId: string) => {
const org = await db.query.organization.findFirst({
where: eq(organization.id, organizationId),
columns: { ownerId: true },
});
if (!org) return null;
return org.ownerId;
};

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,17 @@ 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 (isDangerousNode(entry)) {
throw new Error(
`Dangerous node entries are not allowed: ${entry.entryName}`,
);
}
if (application.serverId) {
if (!entry.isDirectory) {
@@ -132,3 +144,14 @@ const uploadFileToServer = (
});
});
};
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
);
}

View File

@@ -1,4 +1,6 @@
import os from "node:os";
import path from "node:path";
import { paths } from "@dokploy/server/constants";
import { publicIpv4, publicIpv6 } from "public-ip";
export const getShell = () => {
@@ -33,3 +35,18 @@ export const getPublicIpWithFallback = async () => {
}
return ip;
};
export const readValidDirectory = (
directory: string,
serverId?: string | null,
) => {
const { BASE_PATH } = paths(!!serverId);
const resolvedBase = path.resolve(BASE_PATH);
const resolvedDir = path.resolve(directory);
return (
resolvedDir === resolvedBase ||
resolvedDir.startsWith(resolvedBase + path.sep)
);
};