Skip to content

Commit 2b397ee

Browse files
authored
Merge branch 'MCSManager:master' into master
2 parents c6e02f7 + 1a7e431 commit 2b397ee

16 files changed

Lines changed: 82 additions & 81 deletions

daemon/src/routers/file_router.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,6 @@ routerApp.on("file/download_from_url", async (ctx, data) => {
188188

189189
const fileManager = getFileManager(data.instanceUuid);
190190
fileManager.checkPath(fileName);
191-
fileManager.assertNotLink(fileName);
192191
const targetPath = fileManager.toAbsolutePath(fileName);
193192

194193
// Start download in background
@@ -225,7 +224,6 @@ routerApp.on("file/download_from_url_stop", (ctx, data) => {
225224
} else if (data.instanceUuid) {
226225
const fileManager = getFileManager(data.instanceUuid);
227226
fileManager.checkPath(data.fileName);
228-
fileManager.assertNotLink(data.fileName);
229227
const targetPath = fileManager.toAbsolutePath(data.fileName);
230228

231229
const result = downloadManager.stop(targetPath);

daemon/src/service/disk_limit_service.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { exec } from "node:child_process";
22
import fs from "node:fs";
3+
import os from "node:os";
34
import { promisify } from "util";
45
import Instance from "../entity/instance/instance";
56
import { $t } from "../i18n";
@@ -92,6 +93,11 @@ class DiskLimitService {
9293
return this.getCheckDefaultValue(instance, maxSpace);
9394
}
9495

96+
// Disk limit is not supported on Windows
97+
if (os.platform() === "win32") {
98+
return this.getCheckDefaultValue(instance, maxSpace);
99+
}
100+
95101
// There was already an initial check on the working directory when saving,
96102
// but we double-check here.
97103
if (!checkFilePath(workspace) || !fs.existsSync(workspace)) {

daemon/src/service/system_file.ts

Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import { compress, decompress } from "../common/compress";
77
import { globalConfiguration } from "../entity/config";
88
import { $t, i18next } from "../i18n";
99
import { normalizedJoin } from "../tools/filepath";
10-
import { isFileSystemLink } from "../tools/path_link_check";
10+
import { resolveRealPath } from "../tools/path_link_check";
1111

1212
const ERROR_MSG_01 = $t("TXT_CODE_system_file.illegalAccess");
13-
const ERROR_MSG_LINK = $t("TXT_CODE_system_file.linkNotAllowed");
13+
const ERROR_PATH_NOT_FOUND = $t("TXT_CODE_96281410");
1414
const MAX_EDIT_SIZE = 1024 * 1024 * 5;
1515

1616
interface IFile {
@@ -43,6 +43,16 @@ export default class FileManager {
4343
return this.topPath === "/" || this.topPath === "\\";
4444
}
4545

46+
private isOutsideWorkspace(absPath: string): boolean {
47+
// fix the /app/ vs /app mismatch bug and keep it secure
48+
if (this.isRootTopRath()) return false;
49+
const realTop = resolveRealPath(this.topPath);
50+
const realPath = resolveRealPath(absPath);
51+
if (!realTop || !realPath) return true; // If the path cannot be resolved, treat it as outside for safety
52+
const relative = path.relative(realTop, realPath);
53+
return relative === ".." || relative.startsWith(".." + path.sep) || path.isAbsolute(relative);
54+
}
55+
4656
toAbsolutePath(fileName: string = "") {
4757
const topAbsolutePath = this.topPath;
4858

@@ -62,12 +72,7 @@ export default class FileManager {
6272
finalPath = path.normalize(path.join(this.topPath, this.cwd, fileName));
6373
}
6474

65-
// fix the /app/ vs /app mismatch bug and keep it secure
66-
const relative = path.relative(topAbsolutePath, finalPath);
67-
const isOutside =
68-
relative === ".." || relative.startsWith(".." + path.sep) || path.isAbsolute(relative);
69-
if (isOutside && topAbsolutePath !== "/" && topAbsolutePath !== "\\")
70-
throw new Error(ERROR_MSG_01);
75+
if (this.isOutsideWorkspace(finalPath)) throw new Error(ERROR_MSG_01);
7176
return finalPath;
7277
}
7378

@@ -83,7 +88,7 @@ export default class FileManager {
8388
? topAbsolutePath.slice(0, -1)
8489
: topAbsolutePath;
8590

86-
this.assertNotLink(destPath);
91+
this.assertInsideWorkspace(destPath);
8792

8893
if (destPath.startsWith(topPath)) {
8994
const parts = destPath.split(path.sep);
@@ -94,17 +99,15 @@ export default class FileManager {
9499
return false;
95100
}
96101

97-
assertNotLink(fileNameOrPath: string) {
102+
assertInsideWorkspace(fileNameOrPath: string) {
98103
const absPath = this.toAbsolutePath(fileNameOrPath);
99-
if (fs.existsSync(absPath) && isFileSystemLink(absPath)) {
100-
throw new Error(ERROR_MSG_LINK);
101-
}
104+
if (this.isOutsideWorkspace(absPath)) throw new Error(ERROR_MSG_01);
102105
}
103106

104107
check(destPath: string) {
105108
if (this.isRootTopRath()) return true;
106-
if (!this.checkPath(destPath) || !fs.existsSync(this.toAbsolutePath(destPath))) return false;
107-
this.assertNotLink(destPath);
109+
if (!this.checkPath(destPath)) return false;
110+
if (!fs.existsSync(this.toAbsolutePath(destPath))) throw new Error(ERROR_PATH_NOT_FOUND);
108111
return true;
109112
}
110113

@@ -116,21 +119,30 @@ export default class FileManager {
116119
async list(page: 0, pageSize = 40, searchFileName?: string) {
117120
if (pageSize > 100 || pageSize <= 0 || page < 0) throw new Error("Beyond the value limit");
118121

119-
this.assertNotLink(".");
122+
this.assertInsideWorkspace(".");
120123

121124
// Use withFileTypes option to get file type directly, reducing stat calls
122125
const dirents = await fs.readdir(this.toAbsolutePath(), { withFileTypes: true });
123126

124127
// Filter search results and create basic file info with type
125-
let filteredItems = dirents
126-
.filter(
127-
(dirent) =>
128-
!searchFileName || dirent.name.toLowerCase().includes(searchFileName.toLowerCase())
129-
)
130-
.map((dirent) => ({
131-
name: dirent.name,
132-
type: dirent.isFile() ? 1 : 0
133-
}));
128+
let filteredItems = await Promise.all(
129+
dirents
130+
.filter(
131+
(dirent) =>
132+
!searchFileName || dirent.name.toLowerCase().includes(searchFileName.toLowerCase())
133+
)
134+
.map(async (dirent) => {
135+
let type = dirent.isFile() ? 1 : 0;
136+
if (type === 0 && !dirent.isDirectory()) {
137+
// Symbolic links may return false for both isFile() and isDirectory()
138+
// see #2124
139+
try {
140+
type = (await fs.stat(this.toAbsolutePath(dirent.name))).isFile() ? 1 : 0;
141+
} catch {}
142+
}
143+
return { name: dirent.name, type };
144+
})
145+
);
134146

135147
const total = filteredItems.length;
136148

@@ -263,12 +275,12 @@ export default class FileManager {
263275
const filesPath = [];
264276
let totalSize = 0;
265277
for (const iterator of files) {
266-
if (this.check(iterator)) {
267-
filesPath.push(this.toAbsolutePath(iterator));
268-
try {
278+
try {
279+
if (this.check(iterator)) {
280+
filesPath.push(this.toAbsolutePath(iterator));
269281
totalSize += fs.statSync(this.toAbsolutePath(iterator))?.size;
270-
} catch (error: any) {}
271-
}
282+
}
283+
} catch (error: any) {}
272284
}
273285
if (totalSize > MAX_TOTAL_FIELS_SIZE)
274286
throw new Error($t("TXT_CODE_system_file.unzipLimit", { max: MAX_ZIP_GB }));
@@ -307,7 +319,6 @@ export default class FileManager {
307319
}
308320

309321
private zipFileCheck(path: string) {
310-
if (isFileSystemLink(path)) throw new Error(ERROR_MSG_LINK);
311322
const fileInfo = fs.statSync(path);
312323
const MAX_ZIP_GB = globalConfiguration.config.maxZipFileSize;
313324
if (fileInfo.size > 1024 * 1024 * 1024 * MAX_ZIP_GB)
Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,30 @@
11
import fs from "fs-extra";
2+
import path from "path";
23

34
/**
4-
* Returns true when the path itself is a symbolic link (does not follow the link).
5+
* Resolves the real absolute path by walking backwards from the target to
6+
* find the deepest existing ancestor, then calling realpathSync on it and
7+
* appending the non-existent tail segments.
8+
*
9+
* Returns null if realpathSync fails on the existing ancestor.
510
*/
6-
export function isSymbolicLinkPath(absolutePath: string): boolean {
7-
try {
8-
return fs.lstatSync(absolutePath).isSymbolicLink();
9-
} catch {
10-
return false;
11-
}
12-
}
11+
export function resolveRealPath(absolutePath: string): string | null {
12+
let dir = path.resolve(absolutePath);
13+
const root = path.parse(dir).root;
14+
const missed: string[] = [];
1315

14-
/**
15-
* Returns true when the path is a hard link to an existing file.
16-
* Directories are excluded because nlink is not meaningful for them across platforms.
17-
*/
18-
export function isHardLinkPath(absolutePath: string): boolean {
19-
try {
20-
const stat = fs.statSync(absolutePath);
21-
return stat.isFile() && stat.nlink > 1;
22-
} catch {
23-
return false;
16+
while (true) {
17+
try {
18+
fs.lstatSync(dir);
19+
try {
20+
return path.join(fs.realpathSync(dir), ...missed);
21+
} catch {
22+
return null;
23+
}
24+
} catch {
25+
if (dir === root) return null;
26+
missed.unshift(path.basename(dir));
27+
dir = path.dirname(dir);
28+
}
2429
}
2530
}
26-
27-
/**
28-
* Returns true when the path is a symbolic link or a hard link.
29-
*/
30-
export function isFileSystemLink(absolutePath: string): boolean {
31-
return isSymbolicLinkPath(absolutePath) || isHardLinkPath(absolutePath);
32-
}

languages/de_DE.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3445,6 +3445,5 @@
34453445
"TXT_CODE_6542489a": "Beispiel: /dev/sda:10MB",
34463446
"TXT_CODE_9d2e6d5e": "Schreibrate-Limit für Geräte (BPS)",
34473447
"TXT_CODE_2b2ff3ff": "Begrenzt die Schreibrate bestimmter Blockgeräte für den Container. Format: /dev/sda:5MB. Mehrere Geräte durch Kommas trennen.",
3448-
"TXT_CODE_f0dc9eca": "Beispiel: /dev/sda:5MB",
3449-
"TXT_CODE_system_file.linkNotAllowed": "Symbolische Verknüpfungen und feste Verknüpfungen sind für diesen Vorgang nicht zulässig"
3448+
"TXT_CODE_f0dc9eca": "Beispiel: /dev/sda:5MB"
34503449
}

languages/en_US.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3299,7 +3299,6 @@
32993299
"TXT_CODE_systemUser.userCount": "Number of local users: {{n}}",
33003300
"TXT_CODE_system_file.execLimit": "Exceeded maximum file editing limit",
33013301
"TXT_CODE_system_file.illegalAccess": "Illegal access path",
3302-
"TXT_CODE_system_file.linkNotAllowed": "Symbolic links and hard links are not allowed for this operation",
33033302
"TXT_CODE_system_file.unzipLimit": "File decompression only supports file up to {{max}}GB. To change this limit, please go to the data/Config/global.json file.",
33043303
"TXT_CODE_system_instance.autoStart": "Automatic start command for instance {{name}} ({{uuid}}) has been issued.",
33053304
"TXT_CODE_system_instance.autoStartErr": "Error occurred while automatically starting instance {{name}} ({{uuid}}): {{reason}}",
@@ -3447,4 +3446,4 @@
34473446
"TXT_CODE_9d2e6d5e": "Device Write Rate Limit (BPS)",
34483447
"TXT_CODE_2b2ff3ff": "Limit the write rate of specific block devices for the container. Format: /dev/sda:5MB. Separate multiple devices with commas.",
34493448
"TXT_CODE_f0dc9eca": "Example: /dev/sda:5MB"
3450-
}
3449+
}

languages/es_ES.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3445,6 +3445,5 @@
34453445
"TXT_CODE_6542489a": "Ejemplo: /dev/sda:10MB",
34463446
"TXT_CODE_9d2e6d5e": "Límite de velocidad de escritura del dispositivo (BPS)",
34473447
"TXT_CODE_2b2ff3ff": "Limitar la velocidad de escritura de dispositivos de bloque específicos del contenedor. Formato: /dev/sda:5MB. Separe varios dispositivos con comas.",
3448-
"TXT_CODE_f0dc9eca": "Ejemplo: /dev/sda:5MB",
3449-
"TXT_CODE_system_file.linkNotAllowed": "No se permiten enlaces simbólicos ni enlaces duros para esta operación"
3448+
"TXT_CODE_f0dc9eca": "Ejemplo: /dev/sda:5MB"
34503449
}

languages/fr_FR.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3445,6 +3445,5 @@
34453445
"TXT_CODE_6542489a": "Exemple : /dev/sda:10Mo",
34463446
"TXT_CODE_9d2e6d5e": "Limite d'écriture du périphérique (BPS)",
34473447
"TXT_CODE_2b2ff3ff": "Limite le débit d'écriture de périphériques bloc spécifiques pour le conteneur. Format : /dev/sda:5Mo. Séparez plusieurs périphériques par des virgules.",
3448-
"TXT_CODE_f0dc9eca": "Exemple : /dev/sda:5Mo",
3449-
"TXT_CODE_system_file.linkNotAllowed": "Les liens symboliques et les liens physiques ne sont pas autorisés pour cette opération"
3448+
"TXT_CODE_f0dc9eca": "Exemple : /dev/sda:5Mo"
34503449
}

languages/ja_JP.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3445,6 +3445,5 @@
34453445
"TXT_CODE_6542489a": "例: /dev/sda:10MB",
34463446
"TXT_CODE_9d2e6d5e": "デバイス書き込みレート制限(BPS)",
34473447
"TXT_CODE_2b2ff3ff": "コンテナの特定ブロックデバイスの書き込みレートを制限します。形式: /dev/sda:5MB。複数デバイスはカンマで区切ります。",
3448-
"TXT_CODE_f0dc9eca": "例: /dev/sda:5MB",
3449-
"TXT_CODE_system_file.linkNotAllowed": "この操作では、シンボリックリンクとハードリンクは許可されていません。"
3448+
"TXT_CODE_f0dc9eca": "例: /dev/sda:5MB"
34503449
}

languages/ko_KR.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3445,6 +3445,5 @@
34453445
"TXT_CODE_6542489a": "예시: /dev/sda:10MB",
34463446
"TXT_CODE_9d2e6d5e": "장치 쓰기 속도 제한 (BPS)",
34473447
"TXT_CODE_2b2ff3ff": "컨테이너의 특정 블록 장치 쓰기 속도를 제한합니다. 형식: /dev/sda:5MB. 여러 장치는 쉼표로 구분하세요.",
3448-
"TXT_CODE_f0dc9eca": "예시: /dev/sda:5MB",
3449-
"TXT_CODE_system_file.linkNotAllowed": "이 작업에서는 심볼릭 링크와 하드 링크가 허용되지 않습니다"
3448+
"TXT_CODE_f0dc9eca": "예시: /dev/sda:5MB"
34503449
}

0 commit comments

Comments
 (0)