Files
2026-07-13 13:08:41 +08:00

70 lines
1.9 KiB
TypeScript

import fs from "fs";
import os from "os";
import path from "path";
export class LocalFileAccessError extends Error {
constructor(
message: string,
public readonly statusCode: 400 | 403 | 404,
) {
super(message);
this.name = "LocalFileAccessError";
}
}
function resolveBaseDir(baseDir: string): string {
const resolvedBaseDir = path.resolve(baseDir);
try {
return fs.realpathSync(resolvedBaseDir);
} catch {
return resolvedBaseDir;
}
}
function allowedReadableFileBaseDirs(): string[] {
const appDataDirectory =
process.env.APP_DATA_DIRECTORY?.trim() || "/app/user_data";
const tempDirectory =
process.env.TEMP_DIRECTORY?.trim() || path.join(os.tmpdir(), "presenton");
return [appDataDirectory, tempDirectory].map(resolveBaseDir);
}
function assertPathAllowed(candidatePath: string, baseDirs: string[]): void {
for (const baseDir of baseDirs) {
if (candidatePath === baseDir || candidatePath.startsWith(`${baseDir}${path.sep}`)) {
return;
}
}
throw new LocalFileAccessError("Access denied: File path not allowed", 403);
}
export function resolveReadableLocalFile(filePath: unknown): string {
if (typeof filePath !== "string" || filePath.trim().length === 0) {
throw new LocalFileAccessError("Invalid file path", 400);
}
const requestedPath = path.resolve(filePath);
const allowedBaseDirs = allowedReadableFileBaseDirs();
assertPathAllowed(requestedPath, allowedBaseDirs);
let resolvedPath: string;
try {
resolvedPath = fs.realpathSync(requestedPath);
} catch {
throw new LocalFileAccessError("File not found", 404);
}
assertPathAllowed(resolvedPath, allowedBaseDirs);
return resolvedPath;
}
export function readReadableLocalFile(filePath: unknown): string {
const resolvedPath = resolveReadableLocalFile(filePath);
// codeql[js/path-injection]
return fs.readFileSync(resolvedPath, "utf-8");
}