use custom protocol instead of file:// for better security

This commit is contained in:
Vendicated
2025-10-19 02:57:38 +02:00
parent 9f0af48355
commit fa23c630cb
11 changed files with 99 additions and 35 deletions

View File

@@ -5,10 +5,9 @@
*/
import { app, BrowserWindow } from "electron";
import { join } from "path";
import { VIEW_DIR } from "shared/paths";
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
import { loadView } from "./vesktopStatic";
export async function createAboutWindow() {
const height = 750;
@@ -27,9 +26,7 @@ export async function createAboutWindow() {
APP_VERSION: app.getVersion()
});
about.loadFile(join(VIEW_DIR, "about.html"), {
search: data.toString()
});
loadView(about, "about.html", data);
return about;
}

View File

@@ -9,13 +9,13 @@ import { BrowserWindow } from "electron/main";
import { copyFileSync, mkdirSync, readdirSync } from "fs";
import { join } from "path";
import { SplashProps } from "shared/browserWinProperties";
import { VIEW_DIR } from "shared/paths";
import { autoStart } from "./autoStart";
import { DATA_DIR } from "./constants";
import { createWindows } from "./mainWindow";
import { Settings, State } from "./settings";
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
import { loadView } from "./vesktopStatic";
interface Data {
discordBranch: "stable" | "canary" | "ptb";
@@ -37,7 +37,7 @@ export function createFirstLaunchTour() {
makeLinksOpenExternally(win);
win.loadFile(join(VIEW_DIR, "first-launch.html"));
loadView(win, "first-launch.html");
win.webContents.addListener("console-message", (_e, _l, msg) => {
if (msg === "cancel") return app.exit();

View File

@@ -7,6 +7,7 @@
import "./updater";
import "./ipc";
import "./userAssets";
import "./vesktopProtocol";
import { app, BrowserWindow, nativeTheme } from "electron";

View File

@@ -7,9 +7,9 @@
import { BrowserWindow } from "electron";
import { join } from "path";
import { SplashProps } from "shared/browserWinProperties";
import { VIEW_DIR } from "shared/paths";
import { Settings } from "./settings";
import { loadView } from "./vesktopStatic";
let splash: BrowserWindow | undefined;
@@ -22,7 +22,7 @@ export function createSplashWindow(startMinimized = false) {
}
});
splash.loadFile(join(VIEW_DIR, "splash.html"));
loadView(splash, "splash.html");
const { splashBackground, splashColor, splashTheming } = Settings.store;

View File

@@ -8,12 +8,12 @@ import { app, BrowserWindow, ipcMain } from "electron";
import { autoUpdater, UpdateInfo } from "electron-updater";
import { join } from "path";
import { IpcEvents, UpdaterIpcEvents } from "shared/IpcEvents";
import { VIEW_DIR } from "shared/paths";
import { Millis } from "shared/utils/millis";
import { State } from "./settings";
import { handle } from "./utils/ipcWrappers";
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
import { loadView } from "./vesktopStatic";
let updaterWindow: BrowserWindow | null = null;
@@ -77,5 +77,5 @@ function openUpdater(update: UpdateInfo) {
updaterWindow = null;
});
updaterWindow.loadFile(join(VIEW_DIR, "updater.html"));
loadView(updaterWindow, "updater.html");
}

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { app, dialog, net, protocol } from "electron";
import { app, dialog, net } from "electron";
import { copyFile, mkdir, rm } from "fs/promises";
import { join } from "path";
import { IpcEvents } from "shared/IpcEvents";
@@ -41,30 +41,21 @@ export async function resolveAssetPath(asset: UserAssetType) {
return join(STATIC_DIR, DEFAULT_ASSETS[asset]);
}
app.whenReady().then(() => {
protocol.handle("vesktop", async req => {
if (!req.url.startsWith("vesktop://assets/")) {
return new Response(null, { status: 404 });
}
export async function handleVesktopAssetsProtocol(path: string, req: Request) {
const asset = path.replace(/\?v=\d+$/, "").replace(/\/+$/, "");
const asset = decodeURI(req.url)
.slice("vesktop://assets/".length)
.replace(/\?v=\d+$/, "")
.replace(/\/+$/, "");
// @ts-expect-error dumb types
if (!CUSTOMIZABLE_ASSETS.includes(asset)) {
return new Response(null, { status: 404 });
}
// @ts-expect-error dumb types
if (!CUSTOMIZABLE_ASSETS.includes(asset)) {
return new Response(null, { status: 404 });
}
try {
const res = await net.fetch(pathToFileURL(join(UserAssetFolder, asset)).href);
if (res.ok) return res;
} catch {}
try {
const res = await net.fetch(pathToFileURL(join(UserAssetFolder, asset)).href);
if (res.ok) return res;
} catch {}
return net.fetch(pathToFileURL(join(STATIC_DIR, DEFAULT_ASSETS[asset])).href);
});
});
return net.fetch(pathToFileURL(join(STATIC_DIR, DEFAULT_ASSETS[asset])).href);
}
handle(IpcEvents.CHOOSE_USER_ASSET, async (_event, asset: UserAssetType, value?: null) => {
if (!CUSTOMIZABLE_ASSETS.includes(asset)) {

View File

@@ -18,7 +18,7 @@ export function validateSender(frame: WebFrameMain | null, event: string) {
throw new Error(`ipc[${event}]: Invalid URL ${frame.url}`);
}
if (protocol === "file:") return;
if (protocol === "file:" || protocol === "vesktop:") return;
if (!DISCORD_HOSTNAMES.includes(hostname)) {
throw new Error(`ipc[${event}]: Disallowed hostname ${hostname}`);

View File

@@ -0,0 +1,16 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { resolve, sep } from "path";
export function isPathInDirectory(filePath: string, directory: string) {
const resolvedPath = resolve(filePath);
const resolvedDirectory = resolve(directory);
const normalizedDirectory = resolvedDirectory.endsWith(sep) ? resolvedDirectory : resolvedDirectory + sep;
return resolvedPath.startsWith(normalizedDirectory) || resolvedPath === resolvedDirectory;
}

View File

@@ -0,0 +1,27 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { app, protocol } from "electron";
import { handleVesktopAssetsProtocol } from "./userAssets";
import { handleVesktopStaticProtocol } from "./vesktopStatic";
app.whenReady().then(() => {
protocol.handle("vesktop", async req => {
const url = decodeURI(req.url).slice("vesktop://".length);
const [channel, ...pathParts] = url.split("/");
const path = pathParts.join("/");
if (channel === "assets") {
return handleVesktopAssetsProtocol(path, req);
}
if (channel === "static") {
return handleVesktopStaticProtocol(path, req);
}
return new Response(null, { status: 404 });
});
});

33
src/main/vesktopStatic.ts Normal file
View File

@@ -0,0 +1,33 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { BrowserWindow, net } from "electron";
import { join } from "path";
import { pathToFileURL } from "url";
import { isPathInDirectory } from "./utils/isPathInDirectory";
const STATIC_DIR = join(__dirname, "..", "..", "static");
export async function handleVesktopStaticProtocol(path: string, req: Request) {
const staticPath = new URL(path, "vesktop://").pathname;
const fullPath = join(STATIC_DIR, staticPath);
if (!isPathInDirectory(fullPath, STATIC_DIR)) {
return new Response(null, { status: 404 });
}
return net.fetch(pathToFileURL(fullPath).href);
}
export function loadView(browserWindow: BrowserWindow, view: string, params?: URLSearchParams) {
const url = new URL(`vesktop://static/views/${view}`);
if (params) {
url.search = params.toString();
}
return browserWindow.loadURL(url.toString());
}

View File

@@ -7,5 +7,4 @@
import { join } from "path";
export const STATIC_DIR = /* @__PURE__ */ join(__dirname, "..", "..", "static");
export const VIEW_DIR = /* @__PURE__ */ join(STATIC_DIR, "views");
export const BADGE_DIR = /* @__PURE__ */ join(STATIC_DIR, "badges");