Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions electron/ipc/handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import {
} from "../services/runtimeService.js";
import logger from "../services/logger.js";
import { exportConfigToFile, importConfigFromFile } from "../services/configBackupService.js";
import { allowExternalMediaPath } from "../services/mediaAllowlist.js";

const appLauncher = new AutoLaunch({
name: "SelfHost Helper",
Expand Down Expand Up @@ -533,6 +534,7 @@ export const registerHandlers = () => {
if (canceled) {
return null;
} else {
allowExternalMediaPath(filePaths[0]);
return filePaths[0];
}
});
Expand Down
52 changes: 27 additions & 25 deletions electron/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import path from "path";
import fs from "fs";
import { fileURLToPath, pathToFileURL } from "url";
import { registerHandlers, validateExternalUrl } from "./ipc/handlers.js";
import { initializeDatabase } from "./services/database.js";
import { getProjects, initializeDatabase } from "./services/database.js";
import { initTray } from "./tray/tray.js";
import { stopAllProjects } from "./services/projectsManager.js";
import logger from "./services/logger.js";
import settingsService from "./services/settingsService.js";
import { getSessionExternalMediaBases } from "./services/mediaAllowlist.js";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
Expand All @@ -32,7 +33,7 @@ const configuredExternalMediaBases = (process.env[EXTERNAL_MEDIA_DIRS_ENV_KEY] |
.map((entry) => entry.trim())
.filter(Boolean)
.map((entry) => path.resolve(entry));
let hasLoggedPermissiveExternalMediaMode = false;
let hasLoggedMissingExternalMediaAllowlist = false;

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

Expand Down Expand Up @@ -60,6 +61,19 @@ const resolveAndValidatePath = (candidatePath, allowedBases) => {
return isAllowed ? resolvedPath : null;
};

const getProjectIconMediaBases = async () => {
try {
const projects = await getProjects();
return projects
.map((project) => project?.icon)
.filter((iconPath) => typeof iconPath === "string" && path.isAbsolute(iconPath))
.map((iconPath) => path.dirname(path.resolve(iconPath)));
} catch (error) {
logger.warn("[Media Protocol] Failed to load project icon allowlist:", error);
return [];
}
};

async function createWindow() {
mainWindow = new BrowserWindow({
width: 1280,
Expand Down Expand Up @@ -272,29 +286,17 @@ if (!gotTheLock) {
return new Response("Forbidden", { status: 403 });
}

if (configuredExternalMediaBases.length > 0) {
allowedBases.push(...configuredExternalMediaBases);
} else {
// Backward-compatible permissive mode: allow readable files on the resolved drive root.
const driveRoot = path.parse(path.resolve(filePath)).root;
if (driveRoot) {
allowedBases.push(driveRoot);
}
if (
process.platform === "win32" &&
hostname &&
hostname.length === 1 &&
/^[a-zA-Z]$/.test(hostname)
) {
allowedBases.push(path.resolve(`${hostname}:\\`));
}

if (!hasLoggedPermissiveExternalMediaMode) {
logger.warn(
`[Media Protocol] External media requests are using permissive drive-root mode. Set ${EXTERNAL_MEDIA_DIRS_ENV_KEY} to a ${path.delimiter}-separated list of allowed directories to restrict access.`
);
hasLoggedPermissiveExternalMediaMode = true;
}
allowedBases.push(
...configuredExternalMediaBases,
...getSessionExternalMediaBases(),
...(await getProjectIconMediaBases())
);

if (allowedBases.length === 0 && !hasLoggedMissingExternalMediaAllowlist) {
logger.warn(
`[Media Protocol] External media requests require an explicit allowlist. Set ${EXTERNAL_MEDIA_DIRS_ENV_KEY} or select media through the app before requesting external media paths.`
);
hasLoggedMissingExternalMediaAllowlist = true;
}
}

Expand Down
10 changes: 10 additions & 0 deletions electron/services/mediaAllowlist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import path from "path";

const sessionExternalMediaBases = new Set();

export const allowExternalMediaPath = (filePath) => {
if (!filePath || typeof filePath !== "string") return;
sessionExternalMediaBases.add(path.dirname(path.resolve(filePath)));
};

export const getSessionExternalMediaBases = () => [...sessionExternalMediaBases];
Loading