fix(desktop): harden external editor and session cookie IPC (#1627)

* fix(desktop): stop the renderer choosing the external editor binary or reading the session cookie

open-external-editor now launches only the editor saved from a file dialog
and refuses to hand executable file types to the OS opener. The unused
get-session-cookie and wait-session-cookie channels, which returned the
httpOnly jwt, are removed.

* fix: validate setting keys before editor authorization
This commit is contained in:
ZacharyZcR authored and GitHub committed 2026-10-14 08:22:15 +08:00
1 parent 7d13b2a9fe
commit 25e58a6425
5 files changed
+200 -113

No files matched your search

+37 -103
View File
@@ -36,6 +36,10 @@ const { quitApp } = require("./app-quit.cjs");
const { selectLinuxPasswordStore } = require("./linux-password-store.cjs");
const { resolveLocalShell, repairSpawnHelpers } = require("./local-shell.cjs");
const { registerLocalFileHandlers } = require("./local-files.cjs");
const {
EXTERNAL_EDITOR_SETTING,
isLaunchableFileName,
} = require("./external-editor.cjs");
const { isExternalLoginUrl, isCallbackPort } = require("./external-login.cjs");
const localTerminalSessions = new Map();
@@ -701,6 +705,9 @@ let tray = null;
let isQuitting = false;
const tempFiles = new Map();
const externalEditorSessions = new Map();
// Paths the user picked in an open dialog this session. Only these may be
// saved as the external editor, so the renderer cannot name an executable.
const dialogChosenPaths = new Set();
const isDev = process.env.NODE_ENV === "development" || !app.isPackaged;
const appRoot = isDev ? process.cwd() : path.join(__dirname, "..");
@@ -3163,18 +3170,16 @@ ipcMain.handle("get-c2s-tunnel-preset-default-name", () => {
return `[${date}] ${computerName} (${platform} ${release})`;
});
function readSetting(key) {
const settingsPath = path.join(app.getPath("userData"), "settings.json");
if (!fs.existsSync(settingsPath)) return null;
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
return settings[key] !== undefined ? settings[key] : null;
}
ipcMain.handle("get-setting", (event, key) => {
try {
const userDataPath = app.getPath("userData");
const settingsPath = path.join(userDataPath, "settings.json");
if (!fs.existsSync(settingsPath)) {
return null;
}
const settingsData = fs.readFileSync(settingsPath, "utf8");
const settings = JSON.parse(settingsData);
return settings[key] !== undefined ? settings[key] : null;
return readSetting(key);
} catch (error) {
console.error("Error reading setting:", error);
return null;
@@ -3182,6 +3187,16 @@ ipcMain.handle("get-setting", (event, key) => {
});
ipcMain.handle("set-setting", (event, key, value) => {
if (typeof key !== "string") {
return { success: false, error: "Invalid setting key" };
}
if (
key === EXTERNAL_EDITOR_SETTING &&
value &&
!dialogChosenPaths.has(value)
) {
return { success: false, error: "Choose the editor in the file dialog" };
}
try {
const userDataPath = app.getPath("userData");
const settingsPath = path.join(userDataPath, "settings.json");
@@ -3205,94 +3220,6 @@ ipcMain.handle("set-setting", (event, key, value) => {
}
});
ipcMain.handle("get-session-cookie", async (_event, name, targetUrl) => {
try {
const ses = mainWindow?.webContents?.session;
if (!ses) return null;
const cookies = await ses.cookies.get({
name,
...(targetUrl ? { url: targetUrl } : {}),
});
const cookie = cookies.find((candidate) =>
cookieMatchesUrl(candidate, targetUrl),
);
return (
cookie?.value ||
getRememberedElectronAuthCookie(name, targetUrl)?.value ||
null
);
} catch (error) {
console.error("Failed to get session cookie:", error);
return getRememberedElectronAuthCookie(name, targetUrl)?.value || null;
}
});
function cookieMatchesUrl(cookie, targetUrl) {
if (!targetUrl) return true;
try {
const targetHost = new URL(targetUrl).hostname;
const cookieDomain = (cookie.domain || "").replace(/^\./, "");
return (
cookieDomain === targetHost ||
targetHost.endsWith(`.${cookieDomain}`) ||
(!cookieDomain && targetHost === "localhost")
);
} catch {
return true;
}
}
ipcMain.handle(
"wait-session-cookie",
async (_event, name, targetUrl, previousValue, timeoutMs = 5000) => {
const ses = mainWindow?.webContents?.session;
if (!ses) return { success: false, error: "No Electron session" };
const existingCookies = await ses.cookies.get({
name,
...(targetUrl ? { url: targetUrl } : {}),
});
const existingCookie = existingCookies.find((cookie) =>
cookieMatchesUrl(cookie, targetUrl),
);
if (existingCookie?.value && existingCookie.value !== previousValue) {
return { success: true, value: existingCookie.value };
}
const rememberedCookie = getRememberedElectronAuthCookie(name, targetUrl);
if (rememberedCookie?.value && rememberedCookie.value !== previousValue) {
return { success: true, value: rememberedCookie.value };
}
return new Promise((resolve) => {
const timeout = setTimeout(() => {
ses.cookies.off("changed", onCookieChanged);
resolve({ success: false, error: "Timed out waiting for cookie" });
}, timeoutMs);
function onCookieChanged(_event, cookie, _cause, removed) {
if (
removed ||
cookie.name !== name ||
!cookie.value ||
cookie.value === previousValue ||
!cookieMatchesUrl(cookie, targetUrl)
) {
return;
}
clearTimeout(timeout);
ses.cookies.off("changed", onCookieChanged);
resolve({ success: true, value: cookie.value });
}
ses.cookies.on("changed", onCookieChanged);
});
},
);
ipcMain.handle("clear-session-cookies", async () => {
try {
clearPersistedElectronAuthCookies();
@@ -3420,7 +3347,9 @@ ipcMain.handle("show-save-dialog", async (_event, options) => {
});
ipcMain.handle("show-open-dialog", async (_event, options) => {
return dialog.showOpenDialog(mainWindow, options || {});
const result = await dialog.showOpenDialog(mainWindow, options || {});
for (const filePath of result.filePaths) dialogChosenPaths.add(filePath);
return result;
});
function toLocalFileType(stat) {
@@ -3743,6 +3672,15 @@ ipcMain.handle("cleanup-temp-file", (_event, tempId) => {
ipcMain.handle("open-external-editor", async (_event, fileData) => {
try {
// The editor comes from the saved setting, never from the caller.
const editorPath = readSetting(EXTERNAL_EDITOR_SETTING) || null;
if (!editorPath && isLaunchableFileName(fileData?.fileName)) {
return {
success: false,
error: "Choose an external editor to open this file type",
};
}
const result = createManagedTempFile(
fileData?.fileName,
fileData?.content,
@@ -3764,10 +3702,6 @@ ipcMain.handle("open-external-editor", async (_event, fileData) => {
lastMtimeMs: stat.mtimeMs,
});
const editorPath =
typeof fileData?.editorPath === "string" && fileData.editorPath.trim()
? fileData.editorPath.trim()
: null;
const openError = await openPathWithEditor(result.path, editorPath);
if (openError) {
closeExternalEditorSession(editId);