@goodandready/dsh-goal 0.2.8 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/goal-engine.js +1 -0
- package/lib/routes.js +87 -9
- package/package.json +1 -1
package/lib/goal-engine.js
CHANGED
|
@@ -225,6 +225,7 @@ export class GoalEngine {
|
|
|
225
225
|
title: cleanTitle,
|
|
226
226
|
description: options.description?.trim() || '',
|
|
227
227
|
lang: options.lang || detectLanguage(cleanTitle),
|
|
228
|
+
cwd: options.cwd ? String(options.cwd).trim() : null,
|
|
228
229
|
state: GoalState.RUNNING,
|
|
229
230
|
startedAt: now,
|
|
230
231
|
pausedAt: null,
|
package/lib/routes.js
CHANGED
|
@@ -3,16 +3,82 @@ import path from 'node:path';
|
|
|
3
3
|
import { MilestoneStatus, detectLanguage, sessionIdOf } from './goal-engine-constants.js';
|
|
4
4
|
import { saveGoalArtifact, rollbackToCheckpoint } from './engine-reports.js';
|
|
5
5
|
|
|
6
|
-
function
|
|
7
|
-
if (!
|
|
8
|
-
|
|
6
|
+
export function isLoopback(value) {
|
|
7
|
+
if (!value) return false;
|
|
8
|
+
const address = String(value).toLowerCase().replace(/^\[|\]$/g, '');
|
|
9
|
+
return Boolean(
|
|
10
|
+
address === 'localhost' ||
|
|
11
|
+
address === 'localhost.' ||
|
|
12
|
+
address === '::1' ||
|
|
13
|
+
address.startsWith('127.') ||
|
|
14
|
+
address.startsWith('::ffff:127.')
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isTrustedCaller(req) {
|
|
19
|
+
if (!req) return false;
|
|
20
|
+
const remote = req.socket?.remoteAddress || req.connection?.remoteAddress || '';
|
|
21
|
+
if (isLoopback(remote)) return true;
|
|
22
|
+
|
|
23
|
+
const secSite = String(req.headers?.['sec-fetch-site'] || '').toLowerCase();
|
|
24
|
+
if (secSite === 'same-origin' || secSite === 'same-site' || secSite === 'none') return true;
|
|
25
|
+
if (secSite === 'cross-site') return false;
|
|
26
|
+
|
|
27
|
+
const host = String(req.headers?.host || '').toLowerCase();
|
|
28
|
+
const origin = String(req.headers?.origin || '').trim().toLowerCase();
|
|
29
|
+
if (origin) {
|
|
30
|
+
try {
|
|
31
|
+
const u = new URL(origin);
|
|
32
|
+
if (u.host === host) return true;
|
|
33
|
+
} catch (err) { /* invalid url */ }
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const referer = String(req.headers?.referer || '').trim().toLowerCase();
|
|
38
|
+
if (referer) {
|
|
39
|
+
try {
|
|
40
|
+
const u = new URL(referer);
|
|
41
|
+
if (u.host === host) return true;
|
|
42
|
+
} catch (err) { /* invalid url */ }
|
|
43
|
+
return false;
|
|
9
44
|
}
|
|
45
|
+
|
|
46
|
+
// Fallback for tests or local CLI without socket where host header targets loopback
|
|
47
|
+
if (!remote && host) {
|
|
48
|
+
const hostName = host.split(':')[0];
|
|
49
|
+
if (isLoopback(hostName)) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function isPathInsideOrEqual(parent, child) {
|
|
58
|
+
if (!parent || !child) return false;
|
|
59
|
+
const rel = path.relative(path.resolve(parent), path.resolve(child));
|
|
60
|
+
return !rel || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getSafeCwd(requestedCwd, allowedRoots = []) {
|
|
64
|
+
const defaultRoot = allowedRoots[0] || process.cwd();
|
|
65
|
+
if (!requestedCwd || typeof requestedCwd !== 'string' || !requestedCwd.trim()) {
|
|
66
|
+
return defaultRoot;
|
|
67
|
+
}
|
|
68
|
+
|
|
10
69
|
try {
|
|
11
70
|
const resolved = path.resolve(requestedCwd.trim());
|
|
12
71
|
const st = fs.statSync(resolved);
|
|
13
72
|
if (!st.isDirectory()) {
|
|
14
73
|
return null;
|
|
15
74
|
}
|
|
75
|
+
|
|
76
|
+
const roots = allowedRoots.length > 0 ? allowedRoots : [process.cwd()];
|
|
77
|
+
const isAllowed = roots.some((root) => isPathInsideOrEqual(root, resolved));
|
|
78
|
+
if (!isAllowed) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
16
82
|
return resolved;
|
|
17
83
|
} catch (err) {
|
|
18
84
|
return null;
|
|
@@ -144,8 +210,8 @@ export function registerRoutes(ctx, {
|
|
|
144
210
|
return res.end(JSON.stringify({ error: 'Forbidden: cross-site requests are rejected' }));
|
|
145
211
|
}
|
|
146
212
|
|
|
147
|
-
const origin = req.headers
|
|
148
|
-
const host = req.headers
|
|
213
|
+
const origin = req.headers?.origin;
|
|
214
|
+
const host = req.headers?.host;
|
|
149
215
|
if (origin && host) {
|
|
150
216
|
try {
|
|
151
217
|
const originHost = new URL(origin).host;
|
|
@@ -159,6 +225,11 @@ export function registerRoutes(ctx, {
|
|
|
159
225
|
}
|
|
160
226
|
}
|
|
161
227
|
|
|
228
|
+
if (!isTrustedCaller(req)) {
|
|
229
|
+
res.statusCode = 403;
|
|
230
|
+
return res.end(JSON.stringify({ error: 'Forbidden: untrusted caller origin' }));
|
|
231
|
+
}
|
|
232
|
+
|
|
162
233
|
let body = '';
|
|
163
234
|
let bodySize = 0;
|
|
164
235
|
const MAX_PAYLOAD_BYTES = 256 * 1024;
|
|
@@ -196,6 +267,7 @@ export function registerRoutes(ctx, {
|
|
|
196
267
|
description: typeof description === 'string' ? description.trim() : '',
|
|
197
268
|
maxIterations: conf.maxIterations || 25,
|
|
198
269
|
lang: detectedLang,
|
|
270
|
+
cwd: data.cwd,
|
|
199
271
|
}, sid);
|
|
200
272
|
|
|
201
273
|
const startPrompt = detectedLang === 'zh'
|
|
@@ -309,8 +381,11 @@ export function registerRoutes(ctx, {
|
|
|
309
381
|
res.statusCode = 400;
|
|
310
382
|
return res.end(JSON.stringify({ error: 'commit hash is required for rollback' }));
|
|
311
383
|
}
|
|
312
|
-
const
|
|
313
|
-
|
|
384
|
+
const sessionGoal = engine.getGoal(sid);
|
|
385
|
+
const allowedRoots = [process.cwd()];
|
|
386
|
+
if (sessionGoal?.cwd) allowedRoots.push(sessionGoal.cwd);
|
|
387
|
+
const safeCwd = getSafeCwd(data.cwd, allowedRoots);
|
|
388
|
+
if (!safeCwd) {
|
|
314
389
|
res.statusCode = 400;
|
|
315
390
|
return res.end(JSON.stringify({ error: 'Invalid or non-existent working directory' }));
|
|
316
391
|
}
|
|
@@ -329,8 +404,11 @@ export function registerRoutes(ctx, {
|
|
|
329
404
|
res.statusCode = 404;
|
|
330
405
|
return res.end(JSON.stringify({ error: 'No active goal to save artifact for' }));
|
|
331
406
|
}
|
|
332
|
-
const
|
|
333
|
-
|
|
407
|
+
const sessionGoal = engine.getGoal(sid);
|
|
408
|
+
const allowedRoots = [process.cwd()];
|
|
409
|
+
if (sessionGoal?.cwd) allowedRoots.push(sessionGoal.cwd);
|
|
410
|
+
const safeCwd = getSafeCwd(data.cwd, allowedRoots);
|
|
411
|
+
if (!safeCwd) {
|
|
334
412
|
res.statusCode = 400;
|
|
335
413
|
return res.end(JSON.stringify({ error: 'Invalid or non-existent working directory' }));
|
|
336
414
|
}
|
package/package.json
CHANGED