fix symlink in downloads

This commit is contained in:
Emrik Östling committed 2026-09-09 23:43:55 +02:00
1 parent bc5a163fb5
commit d112ef9a1b
7 files changed
+166 -26

No files matched your search

+3
View File
@@ -25,3 +25,6 @@ export const UNAUTHENTICATED_USER_SHARING =
process.env.UNAUTHENTICATED_USER_SHARING?.toLowerCase() === "true" || false;
export const TIMEZONE = process.env.TZ || undefined;
export const uploadsDir = "./data/uploads/";
export const outputDir = "./data/output/";
+1 -3
View File
@@ -4,7 +4,7 @@ import { Elysia } from "elysia";
import { rmSync } from "node:fs";
import db from "./db/db";
import { Jobs } from "./db/types";
import { AUTO_DELETE_EVERY_N_HOURS, WEBROOT } from "./helpers/env";
import { AUTO_DELETE_EVERY_N_HOURS, outputDir, uploadsDir, WEBROOT } from "./helpers/env";
import "./helpers/printVersions";
import { getLandlockRunnerPath, isSandboxAvailable } from "./helpers/sandbox";
import { chooseConverter } from "./pages/chooseConverter";
@@ -20,8 +20,6 @@ import { root } from "./pages/root";
import { upload } from "./pages/upload";
import { user } from "./pages/user";
export const uploadsDir = "./data/uploads/";
export const outputDir = "./data/output/";
// Fix for Elysia issue with Bun, (see https://github.com/oven-sh/bun/issues/12161)
process.getBuiltinModule = require;
+24 -7
View File
@@ -226,28 +226,45 @@ int main(int argc, char *argv[]) {
return 127;
}
// Build supported filesystem access rights
// Handled filesystem access rights in ruleset
__u64 handled_fs_access = LANDLOCK_ACCESS_FS_EXECUTE |
LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR |
LANDLOCK_ACCESS_FS_WRITE_FILE |
LANDLOCK_ACCESS_FS_REMOVE_DIR |
LANDLOCK_ACCESS_FS_REMOVE_FILE |
LANDLOCK_ACCESS_FS_MAKE_CHAR |
LANDLOCK_ACCESS_FS_MAKE_DIR |
LANDLOCK_ACCESS_FS_MAKE_REG |
LANDLOCK_ACCESS_FS_MAKE_SOCK |
LANDLOCK_ACCESS_FS_MAKE_FIFO |
LANDLOCK_ACCESS_FS_MAKE_BLOCK |
LANDLOCK_ACCESS_FS_MAKE_SYM;
if (abi >= 2) handled_fs_access |= LANDLOCK_ACCESS_FS_REFER;
if (abi >= 3) handled_fs_access |= LANDLOCK_ACCESS_FS_TRUNCATE;
if (abi >= 5) handled_fs_access |= LANDLOCK_ACCESS_FS_IOCTL_DEV;
// Build supported filesystem access rights for read-only paths
__u64 fs_ro_access = LANDLOCK_ACCESS_FS_EXECUTE |
LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR;
if (abi >= 2) fs_ro_access |= LANDLOCK_ACCESS_FS_REFER;
// Build supported filesystem access rights for read-write paths
// NOTE: Intentionally exclude LANDLOCK_ACCESS_FS_MAKE_SYM, MAKE_CHAR, MAKE_BLOCK, MAKE_FIFO
// to prevent creation of symlinks (CWE-59 sandbox escape) and device nodes.
__u64 fs_rw_access = fs_ro_access |
LANDLOCK_ACCESS_FS_WRITE_FILE |
LANDLOCK_ACCESS_FS_REMOVE_DIR |
LANDLOCK_ACCESS_FS_REMOVE_FILE |
LANDLOCK_ACCESS_FS_MAKE_CHAR |
LANDLOCK_ACCESS_FS_MAKE_DIR |
LANDLOCK_ACCESS_FS_MAKE_REG |
LANDLOCK_ACCESS_FS_MAKE_SOCK |
LANDLOCK_ACCESS_FS_MAKE_FIFO |
LANDLOCK_ACCESS_FS_MAKE_BLOCK |
LANDLOCK_ACCESS_FS_MAKE_SYM;
LANDLOCK_ACCESS_FS_MAKE_SOCK;
if (abi >= 3) fs_rw_access |= LANDLOCK_ACCESS_FS_TRUNCATE;
if (abi >= 5) fs_rw_access |= LANDLOCK_ACCESS_FS_IOCTL_DEV;
struct landlock_ruleset_attr attr = {
.handled_access_fs = fs_rw_access,
.handled_access_fs = handled_fs_access,
};
if (no_net && abi >= 4) {
+32 -9
View File
@@ -1,10 +1,10 @@
import path from "node:path";
import { Elysia } from "elysia";
import { lstat, realpath } from "node:fs/promises";
import path from "node:path";
import sanitize from "sanitize-filename";
import * as tar from "tar";
import { outputDir } from "..";
import db from "../db/db";
import { WEBROOT } from "../helpers/env";
import { outputDir, WEBROOT } from "../helpers/env";
import { userService } from "./user";
export const download = new Elysia()
@@ -24,13 +24,28 @@ export const download = new Elysia()
const jobId = decodeURIComponent(params.jobId);
const fileName = sanitize(decodeURIComponent(params.fileName));
const filePath = `${outputDir}${userId}/${jobId}/${fileName}`;
const file = Bun.file(filePath);
if (!(await file.exists())) {
const jobOutputDir = path.join(outputDir, userId, jobId);
const filePath = path.join(jobOutputDir, fileName);
try {
const stat = await lstat(filePath);
if (stat.isSymbolicLink()) {
set.status = 403;
return { message: "Access denied." };
}
const resolvedBase = await realpath(jobOutputDir);
const resolvedFile = await realpath(filePath);
if (!resolvedFile.startsWith(resolvedBase + path.sep)) {
set.status = 403;
return { message: "Access denied." };
}
} catch {
set.status = 404;
return { message: "Converted file not found." };
}
const file = Bun.file(filePath);
return file;
},
{
@@ -50,15 +65,23 @@ export const download = new Elysia()
}
const jobId = decodeURIComponent(params.jobId);
const outputPath = `${outputDir}${userId}/${jobId}`;
const outputPath = path.join(outputDir, userId, jobId);
const outputTar = path.join(outputPath, `converted_files_${jobId}.tar`);
await tar.create(
{
file: outputTar,
cwd: outputPath,
filter: (path) => {
return !path.match(".*\\.tar");
filter: (entryPath, stat) => {
const isSymlink =
("isSymbolicLink" in stat &&
typeof stat.isSymbolicLink === "function" &&
stat.isSymbolicLink()) ||
("type" in stat && (stat as { type?: string }).type === "SymbolicLink");
if (entryPath.endsWith(".tar") || isSymlink) {
return false;
}
return true;
},
},
["."],