Skip to content
Merged
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
61 changes: 51 additions & 10 deletions electron/main.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,80 @@
const { app, BrowserWindow } = require('electron');
const { app, BrowserWindow, protocol, net } = require('electron');
const path = require('path');
const url = require('url');

// Use Electron's built-in app.isPackaged instead of electron-is-dev
// app.isPackaged is true when running from a packaged app, false during development
const isDev = !app.isPackaged;

// Register custom protocol scheme before app is ready.
// This allows serving local files with proper URL resolution so that
// absolute asset paths (e.g. /entry-xxx.js) resolve correctly instead of
// hitting the filesystem root when using file:// protocol.
protocol.registerSchemesAsPrivileged([
{
scheme: 'app',
privileges: {
standard: true,
secure: true,
supportFetchAPI: true,
corsEnabled: true,
stream: true,
},
},
]);

function createWindow() {
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
title: 'Resgrid Dispatch',
icon: path.join(__dirname, '../assets/icon.png'),
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: isDev,
contextIsolation: !isDev,
webSecurity: !isDev
webSecurity: !isDev,
},
});

// Prevent the HTML <title> tag from overriding the window title
mainWindow.on('page-title-updated', (event) => {
event.preventDefault();
});

// In development, load the local Expo web server
// In production, load the built index.html
// In production, load via custom app:// protocol that serves from dist/
if (isDev) {
console.log('Loading dev URL: http://localhost:8081');
mainWindow.loadURL('http://localhost:8081');
} else {
const indexPath = path.join(__dirname, '../dist/index.html');
console.log('Loading file:', indexPath);
mainWindow.loadFile(indexPath);
}

if (isDev) {
mainWindow.webContents.openDevTools();
} else {
console.log('Loading app://./index.html');
mainWindow.loadURL('app://./index.html');
}
}

app.whenReady().then(() => {
// Register the custom app:// protocol handler for production builds.
// This serves all files from the dist/ directory so that absolute asset
// paths in the bundled HTML/JS/CSS resolve correctly.
if (!isDev) {
const distPath = path.join(__dirname, '../dist');
protocol.handle('app', (request) => {
const requestUrl = new URL(request.url);
let filePath = decodeURIComponent(requestUrl.pathname);

// Normalize the path: remove leading slashes/dots
filePath = filePath.replace(/^\/+/, '');
if (!filePath || filePath === '.' || filePath === './') {
filePath = 'index.html';
}

const fullPath = path.join(distPath, filePath);
return net.fetch(url.pathToFileURL(fullPath).toString());
});
Comment on lines +61 to +75
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Path traversal vulnerability — the protocol handler does not confine requests to dist/.

filePath is derived from the request URL's pathname and only has leading slashes stripped. A request such as app://./../../etc/passwd will resolve to a path outside distPath because path.join happily resolves .. segments. Since the scheme has supportFetchAPI: true, any JS running in the renderer (including via an XSS vector) can fetch() arbitrary local files.

Add a check that the resolved path stays within the dist/ directory:

🔒 Proposed fix to prevent path traversal
         const distPath = path.join(__dirname, '../dist');
         protocol.handle('app', (request) => {
             const requestUrl = new URL(request.url);
             let filePath = decodeURIComponent(requestUrl.pathname);

             // Normalize the path: remove leading slashes/dots
             filePath = filePath.replace(/^\/+/, '');
             if (!filePath || filePath === '.' || filePath === './') {
                 filePath = 'index.html';
             }

-            const fullPath = path.join(distPath, filePath);
+            const fullPath = path.resolve(distPath, filePath);
+
+            // Prevent path traversal outside dist/
+            if (!fullPath.startsWith(distPath + path.sep) && fullPath !== distPath) {
+                return new Response('Forbidden', { status: 403 });
+            }
+
             return net.fetch(url.pathToFileURL(fullPath).toString());
         });
🤖 Prompt for AI Agents
In `@electron/main.js` around lines 61 - 75, The protocol handler for
protocol.handle('app') currently builds filePath from requestUrl.pathname and
joins it with distPath allowing path traversal; fix by resolving the candidate
path using path.resolve(distPath, filePath) (or similar) into a variable like
fullPath and verify that fullPath starts with the resolved distPath (e.g.,
path.resolve(distPath) as a prefix) before calling net.fetch; if the check
fails, return an error response (or a 404) instead of reading the file. Ensure
you update the logic around filePath, fullPath, distPath and protocol.handle to
perform the resolve-and-validate step to confine requests to dist/.

}

createWindow();

app.on('activate', () => {
Expand Down
Loading