@goodandready/dsh-goal 0.2.24 → 0.2.25

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.
Files changed (2) hide show
  1. package/lib/routes.js +52 -13
  2. package/package.json +1 -1
package/lib/routes.js CHANGED
@@ -8,25 +8,63 @@ import { parseIssueChecklist } from './engine-issue-sync.js';
8
8
 
9
9
  export function isLoopback(value) {
10
10
  if (!value) return false;
11
- const address = String(value).toLowerCase().replace(/^\[|\]$/g, '');
11
+ let address = String(value).toLowerCase().replace(/^\[|\]$/g, '');
12
+ if (address.includes(':')) {
13
+ const colons = address.split(':').length - 1;
14
+ if (colons === 1) address = address.split(':')[0];
15
+ }
16
+ address = address.replace(/^::ffff:/, '');
12
17
  return Boolean(
13
18
  address === 'localhost' ||
14
19
  address === 'localhost.' ||
15
20
  address === '::1' ||
16
- address.startsWith('127.') ||
17
- address.startsWith('::ffff:127.')
21
+ address.startsWith('127.')
18
22
  );
19
23
  }
20
24
 
25
+ export function isTrustedHost(val) {
26
+ if (!val || typeof val !== 'string') return false;
27
+ let hostname = val.trim().toLowerCase();
28
+ if (hostname.startsWith('[')) {
29
+ const end = hostname.indexOf(']');
30
+ if (end > 0) hostname = hostname.slice(1, end);
31
+ } else if (hostname.includes(':')) {
32
+ const colons = hostname.split(':').length - 1;
33
+ if (colons === 1) hostname = hostname.split(':')[0];
34
+ }
35
+ const clean = hostname.replace(/^::ffff:/, '');
36
+ if (clean === 'localhost' || clean === 'localhost.' || clean.endsWith('.local') || clean.endsWith('.lan')) return true;
37
+ if (clean === '127.0.0.1' || clean === '::1' || clean.startsWith('127.')) return true;
38
+ if (clean.startsWith('10.') || clean.startsWith('192.168.')) return true;
39
+ const match172 = clean.match(/^172\.(\d+)\./);
40
+ if (match172) {
41
+ const octet = parseInt(match172[1], 10);
42
+ if (octet >= 16 && octet <= 31) return true;
43
+ }
44
+ return false;
45
+ }
46
+
21
47
  export function isTrustedCaller(req, authToken = process.env.DSH_GOAL_AUTH_TOKEN) {
22
48
  if (!req) return false;
23
- const remote = req.socket?.remoteAddress || req.connection?.remoteAddress || "";
49
+ const rawRemote = req.socket?.remoteAddress || req.connection?.remoteAddress || "";
50
+ const cleanRemote = String(rawRemote).trim().toLowerCase().replace(/^::ffff:/, '');
24
51
  const site = req.headers?.["sec-fetch-site"];
25
52
 
26
53
  // 1. Cross-site and same-site browser requests are rejected
27
54
  if (site === "cross-site" || site === "same-site") return false;
28
55
 
29
- // Optional authorization token check (supports x-dsh-auth-token or Authorization: Bearer <token>)
56
+ // 2. If remoteAddress is present, it must be a trusted local/private IP (never untrusted WAN)
57
+ if (cleanRemote && !isTrustedHost(cleanRemote)) {
58
+ return false;
59
+ }
60
+
61
+ // 3. If Host header is present, it must be a trusted local/private host (DNS rebinding guard)
62
+ const host = String(req.headers?.host || "").trim().toLowerCase();
63
+ if (host && !isTrustedHost(host)) {
64
+ return false;
65
+ }
66
+
67
+ // 4. Optional authorization token check (supports x-dsh-auth-token or Authorization: Bearer <token>)
30
68
  if (authToken) {
31
69
  const headerVal = req.headers?.["x-dsh-auth-token"] || req.headers?.authorization;
32
70
  const cleanHeader = headerVal ? String(headerVal).replace(/^Bearer\s+/i, "").trim() : "";
@@ -34,18 +72,18 @@ export function isTrustedCaller(req, authToken = process.env.DSH_GOAL_AUTH_TOKEN
34
72
  return true;
35
73
  }
36
74
  // When authToken is explicitly configured, non-loopback callers without valid token are rejected
37
- if (!isLoopback(remote)) {
75
+ if (!isLoopback(cleanRemote)) {
38
76
  return false;
39
77
  }
40
78
  }
41
79
 
42
- // 2. Same-origin browser UI requests (operator UI on LAN or localhost)
80
+ // 5. Same-origin browser UI requests (operator UI on LAN or localhost)
43
81
  if (site === "same-origin") {
44
- const host = String(req.headers?.host || "").toLowerCase();
45
82
  const origin = String(req.headers?.origin || "").trim().toLowerCase();
46
83
  if (origin && host) {
47
84
  try {
48
85
  const u = new URL(origin);
86
+ if (!isTrustedHost(u.hostname)) return false;
49
87
  if (u.host === host) return true;
50
88
  } catch (err) {
51
89
  return false;
@@ -55,6 +93,7 @@ export function isTrustedCaller(req, authToken = process.env.DSH_GOAL_AUTH_TOKEN
55
93
  if (referer && host) {
56
94
  try {
57
95
  const u = new URL(referer);
96
+ if (!isTrustedHost(u.hostname)) return false;
58
97
  if (u.host === host) return true;
59
98
  } catch (err) {
60
99
  return false;
@@ -63,12 +102,12 @@ export function isTrustedCaller(req, authToken = process.env.DSH_GOAL_AUTH_TOKEN
63
102
  return false;
64
103
  }
65
104
 
66
- // 3. No declared site: only loopback remote clients (CLI, reverse proxy) are trusted
67
- if (isLoopback(remote)) return true;
105
+ // 6. No declared site: only loopback remote clients (CLI, reverse proxy) are trusted
106
+ if (isLoopback(cleanRemote)) return true;
68
107
 
69
- // 4. Fallback for test harnesses without remote socket where host header explicitly targets loopback
70
- if (!remote && req.headers?.host) {
71
- const hostName = String(req.headers.host).split(":")[0];
108
+ // 7. Fallback for test harnesses without remote socket where host header explicitly targets loopback
109
+ if (!cleanRemote && host) {
110
+ const hostName = host.split(":")[0];
72
111
  if (isLoopback(hostName)) {
73
112
  return true;
74
113
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-goal",
3
- "version": "0.2.24",
3
+ "version": "0.2.25",
4
4
  "description": "Core Goal Addon (Token Budget, Milestones, Git Snapshots & Retrospective Reports) for DeepSeek Harness",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",