mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
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:
1 parent
7d13b2a9fe
commit
25e58a6425
5 files changed
+200
-113
No files matched your search
+37
-103
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user