fix(electrobun): complete all 16 Codex #3 findings

CRITICAL:
- Message persistence race: snapshot batchEnd before async save
- Double-start guard: startingProjects Set prevents concurrent launches
- Symlink path traversal: fs.realpathSync() in path-guard.ts
- Relay false success: connect() returns { ok, machineId, error }

HIGH:
- Session restore skips if active session exists
- Remote remove: new RPC, cleans backend map
- Task board poll token: stale responses discarded after drag-drop
- Health concurrent tools: toolsInFlight counter (was boolean)
- bttask transactions: delete wraps comments+task, addComment validates
- PTY buffer cleared on reconnect
- PTY large paste: chunked String.fromCharCode (8KB chunks)
- Sidecar max line: 10MB limit prevents unbounded memory
- btmsg authorization: group validation, channel membership checks

MEDIUM:
- Session retention: max 5 per project, purgeSession/untrackProject
- Relay IPv6: URL parser replaces string split
- PTY schema: fixed misleading base64 comment
This commit is contained in:
Hibryda 2026-03-22 02:52:04 +01:00
parent c145e37316
commit 0f75cb8e32
12 changed files with 190 additions and 42 deletions

View file

@ -5,6 +5,7 @@
*/
import path from "path";
import fs from "fs";
import { settingsDb } from "../settings-db.ts";
import { homedir } from "os";
import { join } from "path";
@ -29,14 +30,36 @@ function getAllowedRoots(): string[] {
/**
* Validate that a file path is within an allowed boundary.
* Returns the resolved path if valid, or null if outside boundaries.
* Fix #3 (Codex audit): Uses realpathSync to resolve symlinks, preventing
* symlink-based traversal attacks (CWE-59).
* Returns the resolved real path if valid, or null if outside boundaries.
*/
export function validatePath(filePath: string): string | null {
const resolved = path.resolve(filePath);
let resolved: string;
try {
// Resolve symlinks to their actual target to prevent symlink traversal
resolved = fs.realpathSync(path.resolve(filePath));
} catch {
// If the file doesn't exist yet, resolve without symlink resolution
// but only allow if the parent directory resolves within boundaries
const parent = path.dirname(path.resolve(filePath));
try {
const realParent = fs.realpathSync(parent);
resolved = path.join(realParent, path.basename(filePath));
} catch {
return null; // Parent doesn't exist either — reject
}
}
const roots = getAllowedRoots();
for (const root of roots) {
const resolvedRoot = path.resolve(root);
let resolvedRoot: string;
try {
resolvedRoot = fs.realpathSync(path.resolve(root));
} catch {
resolvedRoot = path.resolve(root);
}
if (resolved === resolvedRoot || resolved.startsWith(resolvedRoot + path.sep)) {
return resolved;
}

View file

@ -6,10 +6,11 @@ import type { RelayClient } from "../relay-client.ts";
export function createRemoteHandlers(relayClient: RelayClient) {
return {
// Fix #4 (Codex audit): relay-client.connect() now returns { ok, machineId, error }
"remote.connect": async ({ url, token, label }: { url: string; token: string; label?: string }) => {
try {
const machineId = await relayClient.connect(url, token, label);
return { ok: true, machineId };
const result = await relayClient.connect(url, token, label);
return result;
} catch (err) {
const error = err instanceof Error ? err.message : String(err);
console.error("[remote.connect]", err);
@ -28,6 +29,18 @@ export function createRemoteHandlers(relayClient: RelayClient) {
}
},
// Fix #6 (Codex audit): Add remote.remove RPC that disconnects AND deletes
"remote.remove": ({ machineId }: { machineId: string }) => {
try {
relayClient.removeMachine(machineId);
return { ok: true };
} catch (err) {
const error = err instanceof Error ? err.message : String(err);
console.error("[remote.remove]", err);
return { ok: false, error };
}
},
"remote.list": () => {
try {
return { machines: relayClient.listMachines() };