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
6 changes: 6 additions & 0 deletions src/channels/telegram/monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ export async function startTelegramMonitor(opts: TelegramMonitorOptions): Promis

// handle callback queries (approvals + questions)
bot.on('callback_query:data', async (ctx) => {
const callbackSenderId = String(ctx.from?.id || '');
if (opts.allowFrom && opts.allowFrom.length > 0 && !opts.allowFrom.includes(callbackSenderId)) {
await ctx.answerCallbackQuery('Unauthorized');
return;
}

const data = ctx.callbackQuery.data;
const sep = data.indexOf(':');
if (sep < 0) return;
Expand Down
18 changes: 14 additions & 4 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,19 +278,29 @@ export function isPathAllowed(
try {
resolved = realpathSync(targetPath);
} catch {
resolved = resolve(targetPath);
// For non-existent targets, resolve the parent via realpath to prevent
// symlink-parent bypasses, then append the final segment.
const parent = dirname(resolve(targetPath));
try {
resolved = join(realpathSync(parent), resolve(targetPath).slice(parent.length));
} catch {
resolved = resolve(targetPath);
}
}

const pathMatch = (dir: string, target: string) =>
target === dir || target.startsWith(dir + '/');

// denied: always_denied + global + channel-specific (all merged)
const globalDenied = config.gateway?.deniedPaths || ALWAYS_DENIED;
const globalDenied = [...ALWAYS_DENIED, ...(config.gateway?.deniedPaths || [])];
const channelDenied = channelOverride?.deniedPaths || [];
const denied = [...globalDenied, ...channelDenied].map(p => resolve(p.replace(/^~/, home)));
if (denied.some(d => resolved.startsWith(d))) return false;
if (denied.some(d => pathMatch(d, resolved))) return false;

// allowed: channel-specific overrides global if set
const allowedRaw = channelOverride?.allowedPaths?.length
? channelOverride.allowedPaths
: (config.gateway?.allowedPaths || [home, '/tmp']);
const allowed = allowedRaw.map(p => resolve(p.replace(/^~/, home)));
return allowed.some(a => resolved.startsWith(a));
return allowed.some(a => pathMatch(a, resolved));
}
6 changes: 2 additions & 4 deletions src/gateway/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { WebSocketServer, WebSocket } from 'ws';
import { createServer } from 'node:http';
import { readdirSync, statSync, readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, renameSync, chmodSync, unlinkSync, watch, type FSWatcher } from 'node:fs';
import { writeFile } from 'node:fs/promises';
import { execSync } from 'node:child_process';
import { execSync, execFileSync } from 'node:child_process';
import { resolve as pathResolve, join, dirname } from 'node:path';
import { homedir } from 'node:os';
import { createConnection } from 'node:net';
Expand Down Expand Up @@ -63,9 +63,7 @@ import {

function macNotify(title: string, body: string) {
try {
const t = title.replace(/"/g, '\\"');
const b = body.replace(/"/g, '\\"');
execSync(`osascript -e 'display notification "${b}" with title "${t}"'`, { stdio: 'ignore' });
execFileSync('osascript', ['-e', `display notification "${body}" with title "${title}"`], { stdio: 'ignore' });
} catch { /* ignore */ }
}

Expand Down
4 changes: 2 additions & 2 deletions src/providers/claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -552,8 +552,8 @@ export class ClaudeProvider implements Provider {
return { authenticated: false, error: 'Invalid auth code.' };
}

if (returnedState && returnedState !== this._pkceState) {
console.warn('[claude] OAuth state mismatch, proceeding anyway');
if (returnedState !== this._pkceState) {
return { authenticated: false, error: 'OAuth state mismatch — possible CSRF. Please retry login.' };
}

try {
Expand Down
4 changes: 2 additions & 2 deletions src/skills/loader.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
import { join, basename, dirname } from 'node:path';
import { execSync } from 'node:child_process';
import { execFileSync } from 'node:child_process';
import matter from 'gray-matter';
import type { Config } from '../config.js';

Expand Down Expand Up @@ -28,7 +28,7 @@ export type SkillEligibility = {

function checkBinaryExists(bin: string): boolean {
try {
execSync(`which ${bin}`, { stdio: 'ignore' });
execFileSync('which', [bin], { stdio: 'ignore' });
return true;
} catch {
return false;
Expand Down