@goodandready/dsh-subscriptions 0.6.21 → 0.6.23

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/http.js CHANGED
@@ -30,47 +30,94 @@ export function readBody(req, maxBytes = 64 * 1024) {
30
30
  })
31
31
  }
32
32
 
33
+ function isLoopbackIPv4(address) {
34
+ if (!address.startsWith('127.')) return false
35
+ const parts = address.split('.')
36
+ if (parts.length !== 4) return false
37
+ return parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255)
38
+ }
39
+
40
+ function peerAddress(req) {
41
+ try {
42
+ const socket = req && req.socket
43
+ const address = socket && (socket.remoteAddress || (socket._peername && socket._peername.address))
44
+ return typeof address === 'string' ? address.toLowerCase() : ''
45
+ } catch {
46
+ return ''
47
+ }
48
+ }
49
+
33
50
  export function isLoopback(value) {
34
- const address = value?.toLowerCase().replace(/^\[|\]$/g, '')
51
+ if (value && typeof value === 'object' && (value.socket || value.headers)) {
52
+ const address = peerAddress(value)
53
+ if (address) {
54
+ if (address === '::1' || address === 'localhost') return true
55
+ if (address.startsWith('::ffff:')) return isLoopbackIPv4(address.slice('::ffff:'.length))
56
+ return isLoopbackIPv4(address)
57
+ }
58
+ if (!value.socket) {
59
+ const host = (value.headers?.host || '').split(':')[0].toLowerCase()
60
+ if (host === 'localhost' || host === '127.0.0.1' || host === '::1') return true
61
+ }
62
+ return false
63
+ }
64
+ const address = String(value || '').toLowerCase().replace(/^\[|\]$/g, '')
35
65
  return address === 'localhost' || address === 'localhost.' || address === '::1'
36
- || address?.startsWith('127.') === true
37
- || address?.startsWith('::ffff:127.') === true
66
+ || address.startsWith('127.')
67
+ || address.startsWith('::ffff:127.')
38
68
  }
39
69
 
40
70
  /**
41
71
  * @param {import('node:http').IncomingMessage} request
42
72
  * @returns {boolean}
43
- * Reject cross-site writes. Do not require loopback: Web UI is used over LAN and reverse proxies. */
73
+ * Reject cross-site and untrusted writes and sensitive reads.
74
+ * Fail-closed: must be loopback, same-origin/same-site, or matching Origin/Referer.
75
+ */
44
76
  export function isTrustedSettingsRequest(request) {
45
77
  const headers = request?.headers || {}
46
- const secFetchSite = headers['sec-fetch-site']
47
- if (secFetchSite) {
48
- return secFetchSite !== 'cross-site'
49
- }
78
+
79
+ // 1. Explicit cross-site fetch is always rejected
80
+ const secFetchSite = String(headers['sec-fetch-site'] || '').toLowerCase()
81
+ if (secFetchSite === 'cross-site') return false
82
+
83
+ // 2. If origin header is present, it must match host
50
84
  const host = headers.host || headers['x-forwarded-host']
51
85
  const origin = headers.origin
52
86
  if (origin) {
53
87
  try {
54
88
  const u = new URL(origin)
55
- if (host && u.host.toLowerCase() !== host.toLowerCase()) {
89
+ if (!host || u.host.toLowerCase() !== host.toLowerCase()) {
56
90
  return false
57
91
  }
58
92
  } catch {
59
93
  return false
60
94
  }
61
95
  }
96
+
97
+ // 3. If referer header is present and no origin, referer must match host
62
98
  const referer = headers.referer
63
99
  if (!origin && referer) {
64
100
  try {
65
101
  const u = new URL(referer)
66
- if (host && u.host.toLowerCase() !== host.toLowerCase()) {
102
+ if (!host || u.host.toLowerCase() !== host.toLowerCase()) {
67
103
  return false
68
104
  }
69
105
  } catch {
70
106
  return false
71
107
  }
72
108
  }
73
- return true
109
+
110
+ // 4. Same-origin or same-site browser fetch
111
+ if (secFetchSite === 'same-origin' || secFetchSite === 'same-site') return true
112
+
113
+ // 5. Origin or referer was present and matched host
114
+ if (origin || referer) return true
115
+
116
+ // 6. No origin headers: trust only if demonstrably loopback
117
+ if (isLoopback(request)) return true
118
+
119
+ // 7. Otherwise fail-closed
120
+ return false
74
121
  }
75
122
 
76
123
  export function queryOf(req) {