Skip to content
Open
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
15 changes: 15 additions & 0 deletions emain/emain-ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,21 @@ export function initIpcHandlers() {
event.returnValue = getWaveVersion() as AboutModalDetails;
});

electron.ipcMain.handle("show-open-folder-dialog", async () => {
const ww = focusedWaveWindow;
if (ww == null) {
return null;
}
const result = await electron.dialog.showOpenDialog(ww, {
title: "Select Workspace Directory",
properties: ["openDirectory", "createDirectory"],
});
if (result.canceled || result.filePaths.length === 0) {
return null;
}
return result.filePaths[0];
});
Comment on lines +261 to +274
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate the file and examine the exact handler implementation
find . -name "emain-ipc.ts" -type f

Repository: wavetermdev/waveterm

Length of output: 84


🏁 Script executed:

# Get an overview of the repository structure
git ls-files | head -30

Repository: wavetermdev/waveterm

Length of output: 791


🏁 Script executed:

# Check the actual implementation at lines 261-274
if [ -f "emain/emain-ipc.ts" ]; then
  sed -n '255,280p' emain/emain-ipc.ts
fi

Repository: wavetermdev/waveterm

Length of output: 904


🏁 Script executed:

# Search for all ipcMain.handle patterns to understand security patterns used in the codebase
rg "ipcMain\.handle" -A 3 -B 1 --max-count 10

Repository: wavetermdev/waveterm

Length of output: 977


🏁 Script executed:

# Look for WebContents validation patterns and window management
rg "event\.sender|event\.senderFrame|webContents" -B 2 -A 2 | head -50

Repository: wavetermdev/waveterm

Length of output: 3303


🏁 Script executed:

# Search for getWaveWindowByWebContentsId or similar window lookup functions
rg "getWaveWindowByWebContentsId|getWaveTabViewByWebContentsId" -B 2 -A 5

Repository: wavetermdev/waveterm

Length of output: 8316


🏁 Script executed:

# Look for focusedWaveWindow definition and how it's managed
rg "focusedWaveWindow" -B 2 -A 2

Repository: wavetermdev/waveterm

Length of output: 4512


🏁 Script executed:

# Check what windows/webContents can invoke IPC handlers - look at preload/context bridge setup
fd "preload|contextBridge|ipcRenderer" -type f

Repository: wavetermdev/waveterm

Length of output: 234


🏁 Script executed:

# Search for WaveBrowserWindow and related window classes to understand architecture
rg "class.*BrowserWindow|focusedWaveWindow\s*=" -A 3

Repository: wavetermdev/waveterm

Length of output: 896


Validate the IPC handler sender before showing the native dialog.

The handler lacks sender validation, which is inconsistent with security patterns used elsewhere in the same file. Multiple other handlers (e.g., "capture-screenshot", "update-window-controls-overlay") validate the sender using getWaveWindowByWebContentsId(event.sender.id). Since webviews exist in this app and may be able to invoke this channel, any untrusted renderer could trigger native file dialogs without proper authorization. Add the event parameter and validate it against an expected window/webContents:

Suggested hardening pattern
-    electron.ipcMain.handle("show-open-folder-dialog", async () => {
-        const ww = focusedWaveWindow;
+    electron.ipcMain.handle("show-open-folder-dialog", async (event) => {
+        const ww = getWaveWindowByWebContentsId(event.sender.id) ?? focusedWaveWindow;
         if (ww == null) {
             return null;
         }
         const result = await electron.dialog.showOpenDialog(ww, {
             title: "Select Workspace Directory",
             properties: ["openDirectory", "createDirectory"],
         });
         if (result.canceled || result.filePaths.length === 0) {
             return null;
         }
         return result.filePaths[0];
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
electron.ipcMain.handle("show-open-folder-dialog", async () => {
const ww = focusedWaveWindow;
if (ww == null) {
return null;
}
const result = await electron.dialog.showOpenDialog(ww, {
title: "Select Workspace Directory",
properties: ["openDirectory", "createDirectory"],
});
if (result.canceled || result.filePaths.length === 0) {
return null;
}
return result.filePaths[0];
});
electron.ipcMain.handle("show-open-folder-dialog", async (event) => {
const ww = getWaveWindowByWebContentsId(event.sender.id) ?? focusedWaveWindow;
if (ww == null) {
return null;
}
const result = await electron.dialog.showOpenDialog(ww, {
title: "Select Workspace Directory",
properties: ["openDirectory", "createDirectory"],
});
if (result.canceled || result.filePaths.length === 0) {
return null;
}
return result.filePaths[0];
});
🤖 Prompt for AI Agents
In @emain/emain-ipc.ts around lines 261 - 274, The IPC handler for
"show-open-folder-dialog" must validate the sender like other handlers: change
the handler signature to include the event parameter (e.g., async (event) => {
... }), call getWaveWindowByWebContentsId(event.sender.id) to retrieve and
verify the window (instead of using focusedWaveWindow), return null if it’s not
found, and then call electron.dialog.showOpenDialog with that verified window;
ensure you still check result.canceled and result.filePaths before returning the
selected path.


electron.ipcMain.on("get-zoom-factor", (event) => {
event.returnValue = event.sender.getZoomFactor();
});
Expand Down
1 change: 1 addition & 0 deletions emain/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ contextBridge.exposeInMainWorld("api", {
openBuilder: (appId?: string) => ipcRenderer.send("open-builder", appId),
setBuilderWindowAppId: (appId: string) => ipcRenderer.send("set-builder-window-appid", appId),
doRefresh: () => ipcRenderer.send("do-refresh"),
showOpenFolderDialog: () => ipcRenderer.invoke("show-open-folder-dialog"),
});

// Custom event for "new-window"
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/store/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ class WorkspaceServiceType {
}

// @returns object updates
UpdateWorkspace(workspaceId: string, name: string, icon: string, color: string, applyDefaults: boolean): Promise<void> {
UpdateWorkspace(workspaceId: string, name: string, icon: string, color: string, directory: string, applyDefaults: boolean): Promise<void> {
return WOS.callBackendService("workspace", "UpdateWorkspace", Array.from(arguments))
}
}
Expand Down
27 changes: 27 additions & 0 deletions frontend/app/tab/workspaceeditor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,33 @@
}
}

.directory-selector {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid var(--modal-border-color);

.directory-label {
display: block;
font-size: 12px;
color: var(--secondary-text-color);
margin-bottom: 5px;
}

.directory-input-row {
display: flex;
gap: 8px;
align-items: center;

.directory-input {
flex: 1;
}

.browse-btn {
flex-shrink: 0;
}
}
}

.delete-ws-btn-wrapper {
display: flex;
align-items: center;
Expand Down
73 changes: 52 additions & 21 deletions frontend/app/tab/workspaceeditor.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getApi } from "@/app/store/global";
import { fireAndForget, makeIconClass } from "@/util/util";
import clsx from "clsx";
import { memo, useEffect, useRef, useState } from "react";
Expand All @@ -13,19 +14,20 @@ interface ColorSelectorProps {
className?: string;
}

const ColorSelector = memo(({ colors, selectedColor, onSelect, className }: ColorSelectorProps) => {
const handleColorClick = (color: string) => {
onSelect(color);
};

const ColorSelector = memo(function ColorSelector({
colors,
selectedColor,
onSelect,
className,
}: ColorSelectorProps) {
return (
<div className={clsx("color-selector", className)}>
{colors.map((color) => (
<div
key={color}
className={clsx("color-circle", { selected: selectedColor === color })}
style={{ backgroundColor: color }}
onClick={() => handleColorClick(color)}
onClick={() => onSelect(color)}
/>
))}
</div>
Expand All @@ -39,11 +41,12 @@ interface IconSelectorProps {
className?: string;
}

const IconSelector = memo(({ icons, selectedIcon, onSelect, className }: IconSelectorProps) => {
const handleIconClick = (icon: string) => {
onSelect(icon);
};

const IconSelector = memo(function IconSelector({
icons,
selectedIcon,
onSelect,
className,
}: IconSelectorProps) {
return (
<div className={clsx("icon-selector", className)}>
{icons.map((icon) => {
Expand All @@ -52,7 +55,7 @@ const IconSelector = memo(({ icons, selectedIcon, onSelect, className }: IconSel
<i
key={icon}
className={clsx(iconClass, "icon-item", { selected: selectedIcon === icon })}
onClick={() => handleIconClick(icon)}
onClick={() => onSelect(icon)}
/>
);
})}
Expand All @@ -64,33 +67,37 @@ interface WorkspaceEditorProps {
title: string;
icon: string;
color: string;
directory: string;
focusInput: boolean;
onTitleChange: (newTitle: string) => void;
onColorChange: (newColor: string) => void;
onIconChange: (newIcon: string) => void;
onDirectoryChange: (newDirectory: string) => void;
onDeleteWorkspace: () => void;
}
const WorkspaceEditorComponent = ({
export const WorkspaceEditor = memo(function WorkspaceEditor({
title,
icon,
color,
directory,
focusInput,
onTitleChange,
onColorChange,
onIconChange,
onDirectoryChange,
onDeleteWorkspace,
}: WorkspaceEditorProps) => {
}: WorkspaceEditorProps) {
const inputRef = useRef<HTMLInputElement>(null);

const [colors, setColors] = useState<string[]>([]);
const [icons, setIcons] = useState<string[]>([]);

useEffect(() => {
fireAndForget(async () => {
const colors = await WorkspaceService.GetColors();
const icons = await WorkspaceService.GetIcons();
setColors(colors);
setIcons(icons);
const fetchedColors = await WorkspaceService.GetColors();
const fetchedIcons = await WorkspaceService.GetIcons();
setColors(fetchedColors);
setIcons(fetchedIcons);
});
}, []);

Expand All @@ -113,13 +120,37 @@ const WorkspaceEditorComponent = ({
/>
<ColorSelector selectedColor={color} colors={colors} onSelect={onColorChange} />
<IconSelector selectedIcon={icon} icons={icons} onSelect={onIconChange} />
<div className="directory-selector">
<label className="directory-label">Directory</label>
<div className="directory-input-row">
<Input
value={directory}
onChange={onDirectoryChange}
placeholder="~/projects/myworkspace"
className="directory-input"
/>
<Button
className="ghost browse-btn"
onClick={async () => {
try {
const path = await getApi().showOpenFolderDialog();
if (path) {
onDirectoryChange(path);
}
} catch (e) {
console.error("error opening folder dialog:", e);
}
}}
>
Browse
</Button>
</div>
</div>
<div className="delete-ws-btn-wrapper">
<Button className="ghost red text-[12px] bold" onClick={onDeleteWorkspace}>
Delete workspace
</Button>
</div>
</div>
);
};

export const WorkspaceEditor = memo(WorkspaceEditorComponent) as typeof WorkspaceEditorComponent;
});
Loading