@goodandready/dsh-model-sync 0.5.9 → 0.5.10
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/client.js +2 -0
- package/lib/credentials.js +10 -1
- package/lib/diff-notifier.js +65 -14
- package/lib/history.js +2 -1
- package/lib/http.js +28 -19
- package/lib/policy.js +6 -0
- package/lib/reporting.js +2 -4
- package/lib/synchronizer-helpers.js +2 -1
- package/lib/synchronizer.js +9 -5
- package/lib/updater.js +39 -10
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -600,6 +600,7 @@ window.__ModuleLoader__.load({
|
|
|
600
600
|
setBusy(true); setWebhookErr(''); setWebhookMsg('')
|
|
601
601
|
try {
|
|
602
602
|
await scope.set('diffWebhookUrl', webhookDraft.diffWebhookUrl || '')
|
|
603
|
+
await scope.set('diffWebhookSecretRef', webhookDraft.diffWebhookSecretRef || webhookDraft.diffWebhookSecret || '')
|
|
603
604
|
await scope.set('diffWebhookSecret', webhookDraft.diffWebhookSecret || '')
|
|
604
605
|
await scope.set('notifyOnModelAdded', !!webhookDraft.notifyOnModelAdded)
|
|
605
606
|
await scope.set('notifyOnModelDeprecated', !!webhookDraft.notifyOnModelDeprecated)
|
|
@@ -622,6 +623,7 @@ window.__ModuleLoader__.load({
|
|
|
622
623
|
body: JSON.stringify({
|
|
623
624
|
url: webhookDraft.diffWebhookUrl,
|
|
624
625
|
secret: webhookDraft.diffWebhookSecret || '',
|
|
626
|
+
secretRef: webhookDraft.diffWebhookSecretRef || webhookDraft.diffWebhookSecret || '',
|
|
625
627
|
}),
|
|
626
628
|
})
|
|
627
629
|
const body = await res.json()
|
package/lib/credentials.js
CHANGED
|
@@ -38,9 +38,18 @@ function safeCode(error) {
|
|
|
38
38
|
return 'credential-error'
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
export function sanitizeSecretString(value) {
|
|
42
|
+
if (value === null || value === undefined) return ''
|
|
43
|
+
return String(value)
|
|
44
|
+
.replace(/(authorization)\s*[:=]\s*(?:bearer|basic|token)\s+[^\s,;&]+/gi, '$1=[redacted]')
|
|
45
|
+
.replace(/(api[_-]?key|token|secret|authorization|password)\s*[:=]\s*[^\s,;&]+/gi, '$1=[redacted]')
|
|
46
|
+
.replace(/\b(bearer\s+)[A-Za-z0-9._~+/-]+=*/gi, '$1[redacted]')
|
|
47
|
+
.replace(/([?&](?:key|token|api[_-]?key|access[_-]?token|secret)=)[^&#\s]+/gi, '$1[redacted]')
|
|
48
|
+
}
|
|
49
|
+
|
|
41
50
|
function safeMessage(error) {
|
|
42
51
|
const source = error instanceof Error ? error.message : String(error ?? '')
|
|
43
|
-
return source
|
|
52
|
+
return sanitizeSecretString(source).slice(0, 160)
|
|
44
53
|
}
|
|
45
54
|
|
|
46
55
|
export function createCredentialResolver(ctx) {
|
package/lib/diff-notifier.js
CHANGED
|
@@ -104,6 +104,43 @@ export function extractDiffEvents(results, options = {}) {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
export function isLoopbackIPv4(address) {
|
|
108
|
+
if (typeof address !== 'string' || !address.startsWith('127.')) return false
|
|
109
|
+
const parts = address.split('.')
|
|
110
|
+
if (parts.length !== 4) return false
|
|
111
|
+
return parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) >= 0 && Number(part) <= 255)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function isForbiddenWebhookHost(rawHostname) {
|
|
115
|
+
const host = String(rawHostname || '').toLowerCase().replace(/^\[|\]$/g, '')
|
|
116
|
+
if (host === '169.254.169.254' || host.startsWith('169.254.')) return true
|
|
117
|
+
if (host.startsWith('::ffff:')) {
|
|
118
|
+
const v4 = host.slice('::ffff:'.length)
|
|
119
|
+
if (v4.startsWith('169.254.') || v4.startsWith('a9fe:')) return true
|
|
120
|
+
if (v4 === '100.100.100.200' || v4 === '6464:64c8') return true
|
|
121
|
+
}
|
|
122
|
+
if (host.startsWith('fe80:') || host.startsWith('fe80::')) return true
|
|
123
|
+
if (host === '100.100.100.200') return true
|
|
124
|
+
if (host === '0.0.0.0' || host === '255.255.255.255' || host === '::' || host === '0:0:0:0:0:0:0:0') return true
|
|
125
|
+
if (
|
|
126
|
+
host === 'metadata.google.internal' ||
|
|
127
|
+
host === 'instance-data' ||
|
|
128
|
+
host === 'metadata' ||
|
|
129
|
+
host.endsWith('.metadata.google.internal') ||
|
|
130
|
+
host.endsWith('.metadata')
|
|
131
|
+
) return true
|
|
132
|
+
return false
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function isLoopbackHost(rawHostname) {
|
|
136
|
+
const host = String(rawHostname || '').toLowerCase().replace(/^\[|\]$/g, '')
|
|
137
|
+
if (host === '127.0.0.1' || host === 'localhost' || host === 'localhost.' || host === '::1') return true
|
|
138
|
+
if (host.startsWith('::ffff:')) {
|
|
139
|
+
return isLoopbackIPv4(host.slice('::ffff:'.length))
|
|
140
|
+
}
|
|
141
|
+
return isLoopbackIPv4(host)
|
|
142
|
+
}
|
|
143
|
+
|
|
107
144
|
export async function sendWebhookNotification(url, secret, payload, { fetchImpl = globalThis.fetch, timeoutMs = 5000 } = {}) {
|
|
108
145
|
if (typeof url !== "string" || !url.trim()) {
|
|
109
146
|
return { ok: false, error: "URL is required" }
|
|
@@ -115,16 +152,10 @@ export async function sendWebhookNotification(url, secret, payload, { fetchImpl
|
|
|
115
152
|
return { ok: false, error: "URL must use http or https protocol" }
|
|
116
153
|
}
|
|
117
154
|
const hostname = parsedUrl.hostname.toLowerCase().replace(/^\[|\]$/g, '')
|
|
118
|
-
if (
|
|
119
|
-
hostname === '169.254.169.254' ||
|
|
120
|
-
hostname.startsWith('169.254.') ||
|
|
121
|
-
hostname === 'metadata.google.internal' ||
|
|
122
|
-
hostname === '0.0.0.0' ||
|
|
123
|
-
hostname === '255.255.255.255'
|
|
124
|
-
) {
|
|
155
|
+
if (isForbiddenWebhookHost(hostname)) {
|
|
125
156
|
return { ok: false, error: "webhook url targets a forbidden metadata or link-local address" }
|
|
126
157
|
}
|
|
127
|
-
const isLoopback =
|
|
158
|
+
const isLoopback = isLoopbackHost(hostname)
|
|
128
159
|
if (parsedUrl.protocol === "http:" && !isLoopback) {
|
|
129
160
|
return { ok: false, error: "insecure http webhook url is only permitted on loopback" }
|
|
130
161
|
}
|
|
@@ -195,17 +226,37 @@ export async function dispatchDiffNotifications(ctx, config, results, { fetchImp
|
|
|
195
226
|
|
|
196
227
|
let webhookResult = null
|
|
197
228
|
if (config?.diffWebhookUrl && typeof config.diffWebhookUrl === "string" && config.diffWebhookUrl.trim()) {
|
|
198
|
-
let secret =
|
|
199
|
-
const secretRef = config.diffWebhookSecretRef ||
|
|
229
|
+
let secret = ''
|
|
230
|
+
const secretRef = (config.diffWebhookSecretRef || '').trim()
|
|
200
231
|
if (secretRef) {
|
|
232
|
+
let resolvedSecret = ''
|
|
201
233
|
try {
|
|
202
|
-
const
|
|
203
|
-
if (
|
|
234
|
+
const creds = ctx?.credentials || ctx?.get?.('credentials')
|
|
235
|
+
if (creds?.resolve) {
|
|
236
|
+
const res = await creds.resolve(secretRef)
|
|
237
|
+
resolvedSecret = typeof res === 'string' ? res : res?.value || ''
|
|
238
|
+
}
|
|
204
239
|
} catch (err) {
|
|
205
|
-
if (typeof ctx?.logger?.
|
|
206
|
-
ctx.logger.
|
|
240
|
+
if (typeof ctx?.logger?.warn === 'function') {
|
|
241
|
+
ctx.logger.warn('[dsh-model-sync] webhook secret resolution failed:', err)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (!resolvedSecret) {
|
|
245
|
+
// FAIL-CLOSED: do not dispatch unsigned webhook when secret was expected (#204)
|
|
246
|
+
if (typeof ctx?.logger?.warn === 'function') {
|
|
247
|
+
ctx.logger.warn(`[dsh-model-sync] webhook secret resolution failed for '${secretRef}', skipping webhook dispatch (fail-closed)`)
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
ok: false,
|
|
251
|
+
error: 'credential_resolution_failed',
|
|
252
|
+
message: `Failed to resolve webhook secret credential reference '${secretRef}'`,
|
|
253
|
+
events,
|
|
254
|
+
dispatched: false,
|
|
207
255
|
}
|
|
208
256
|
}
|
|
257
|
+
secret = resolvedSecret
|
|
258
|
+
} else if (config.diffWebhookSecret) {
|
|
259
|
+
secret = String(config.diffWebhookSecret).trim()
|
|
209
260
|
}
|
|
210
261
|
webhookResult = await sendWebhookNotification(
|
|
211
262
|
config.diffWebhookUrl,
|
package/lib/history.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sanitizeSecretString } from './credentials.js'
|
|
1
2
|
import { normalizeModels } from './models.js'
|
|
2
3
|
|
|
3
4
|
export const DEFAULT_HISTORY_LIMIT = 50
|
|
@@ -41,7 +42,7 @@ function catalog(provider, models) {
|
|
|
41
42
|
|
|
42
43
|
function message(value) {
|
|
43
44
|
if (value === undefined || value === null) return undefined
|
|
44
|
-
return
|
|
45
|
+
return sanitizeSecretString(value).slice(0, MAX_MESSAGE_LENGTH)
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
function diffFor(provider, diff) {
|
package/lib/http.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isForbiddenWebhookHost, isLoopbackHost } from './diff-notifier.js'
|
|
1
2
|
import crypto from 'node:crypto'
|
|
2
3
|
import { validateAliasName } from './alias-resolver.js'
|
|
3
4
|
import { validatePolicy } from './policy.js'
|
|
@@ -15,22 +16,17 @@ function normalizeWebhookTestRequest(body) {
|
|
|
15
16
|
throw Object.assign(new Error("URL must use http or https protocol"), { code: "INVALID_REQUEST" })
|
|
16
17
|
}
|
|
17
18
|
const hostname = parsed.hostname.toLowerCase().replace(/^\[|\]$/g, '')
|
|
18
|
-
|
|
19
|
+
if (isForbiddenWebhookHost(hostname)) {
|
|
20
|
+
throw Object.assign(new Error("webhook url targets a forbidden metadata or link-local address"), { code: "INVALID_REQUEST" })
|
|
21
|
+
}
|
|
22
|
+
const isLoopback = isLoopbackHost(hostname)
|
|
19
23
|
if (parsed.protocol === "http:" && !isLoopback) {
|
|
20
24
|
throw Object.assign(new Error("insecure http webhook url is only permitted on loopback"), { code: "INVALID_REQUEST" })
|
|
21
25
|
}
|
|
22
|
-
if (
|
|
23
|
-
hostname === '169.254.169.254' ||
|
|
24
|
-
hostname.startsWith('169.254.') ||
|
|
25
|
-
hostname === 'metadata.google.internal' ||
|
|
26
|
-
hostname === '0.0.0.0' ||
|
|
27
|
-
hostname === '255.255.255.255'
|
|
28
|
-
) {
|
|
29
|
-
throw Object.assign(new Error("webhook url targets a forbidden metadata or link-local address"), { code: "INVALID_REQUEST" })
|
|
30
|
-
}
|
|
31
26
|
return {
|
|
32
27
|
url: parsed.toString(),
|
|
33
28
|
secret: typeof body.secret === "string" ? body.secret.trim() : "",
|
|
29
|
+
...(body.secretRef && typeof body.secretRef === "string" ? { secretRef: body.secretRef.trim() } : {}),
|
|
34
30
|
}
|
|
35
31
|
}
|
|
36
32
|
|
|
@@ -42,14 +38,17 @@ function json(res, status, body) {
|
|
|
42
38
|
}
|
|
43
39
|
|
|
44
40
|
export function isLoopbackAddress(ip) {
|
|
45
|
-
|
|
46
|
-
const clean = ip.toLowerCase().replace(/^\[|\]$/g, '')
|
|
47
|
-
if (clean === '127.0.0.1' || clean === '::1' || clean === '::ffff:127.0.0.1' || clean === 'localhost') return true
|
|
48
|
-
if (clean.startsWith('127.') || clean.startsWith('::ffff:127.')) return true
|
|
49
|
-
return false
|
|
41
|
+
return isLoopbackHost(ip)
|
|
50
42
|
}
|
|
51
43
|
|
|
52
|
-
export function trusted(req, { requireLoopback = false } = {}) {
|
|
44
|
+
export function trusted(req, { requireLoopback = false, ctx = null } = {}) {
|
|
45
|
+
// 1. Core connection rejection check (#237)
|
|
46
|
+
const conn = ctx?.connection || ctx?.get?.('connection') || req?.connection
|
|
47
|
+
if (conn && typeof conn.requestRejection === 'function') {
|
|
48
|
+
const rejection = conn.requestRejection(req)
|
|
49
|
+
if (rejection !== undefined) return false
|
|
50
|
+
}
|
|
51
|
+
|
|
53
52
|
const remoteAddress = req?.socket?.remoteAddress
|
|
54
53
|
if (requireLoopback || req?.method === 'POST') {
|
|
55
54
|
if (remoteAddress && !isLoopbackAddress(remoteAddress)) return false
|
|
@@ -66,7 +65,7 @@ export function trusted(req, { requireLoopback = false } = {}) {
|
|
|
66
65
|
const url = new URL(origin)
|
|
67
66
|
if (url.host !== host) return false
|
|
68
67
|
if (requireLoopback || req?.method === 'POST') {
|
|
69
|
-
if (!isLoopbackAddress(url.hostname)
|
|
68
|
+
if (!isLoopbackAddress(url.hostname)) return false
|
|
70
69
|
}
|
|
71
70
|
} catch { return false }
|
|
72
71
|
}
|
|
@@ -77,7 +76,7 @@ export function trusted(req, { requireLoopback = false } = {}) {
|
|
|
77
76
|
const url = new URL(referer)
|
|
78
77
|
if (url.host !== host) return false
|
|
79
78
|
if (requireLoopback || req?.method === 'POST') {
|
|
80
|
-
if (!isLoopbackAddress(url.hostname)
|
|
79
|
+
if (!isLoopbackAddress(url.hostname)) return false
|
|
81
80
|
}
|
|
82
81
|
} catch { return false }
|
|
83
82
|
}
|
|
@@ -273,8 +272,18 @@ export function registerHttpApi(ctx, synchronizer, { getSchedulerStatus = () =>
|
|
|
273
272
|
return []
|
|
274
273
|
}
|
|
275
274
|
const makeHandler = (route) => async (req, res) => {
|
|
275
|
+
// 1. Core connection check on all routes (#237)
|
|
276
|
+
const conn = ctx?.connection || ctx?.get?.('connection') || req?.connection
|
|
277
|
+
if (conn && typeof conn.requestRejection === 'function') {
|
|
278
|
+
const rejection = conn.requestRejection(req)
|
|
279
|
+
if (rejection !== undefined) {
|
|
280
|
+
const statusCode = typeof rejection === 'number' ? rejection : 401
|
|
281
|
+
return json(res, statusCode, { error: { code: 'unauthorized', message: 'dsh-model-sync: core connection rejected request' } })
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
276
285
|
const isPublicStatus = route === 'status' && req.method === 'GET'
|
|
277
|
-
if (!trusted(req, { requireLoopback: !isPublicStatus })) return json(res, 403, { error: { code: 'forbidden', message: 'dsh-model-sync: cross-origin or non-loopback request rejected' } })
|
|
286
|
+
if (!trusted(req, { requireLoopback: !isPublicStatus, ctx })) return json(res, 403, { error: { code: 'forbidden', message: 'dsh-model-sync: cross-origin or non-loopback request rejected' } })
|
|
278
287
|
if (route === 'status') {
|
|
279
288
|
if (req.method !== 'GET') return json(res, 405, { error: { code: 'method', message: 'GET only' } })
|
|
280
289
|
const payload = { ...synchronizer.status(), scheduler: getSchedulerStatus(), providers: synchronizer.listProviders() }
|
package/lib/policy.js
CHANGED
|
@@ -41,6 +41,12 @@ function isDangerousPattern(pattern) {
|
|
|
41
41
|
if (/\([^\)]*([+*]|\{[0-9]+,[0-9]*\})[^\)]*\)([+*]|\{[0-9]+,[0-9]*\})/.test(pattern)) return true
|
|
42
42
|
// Check for multiple consecutive open-ended wildcards (e.g. .*.*)
|
|
43
43
|
if (/(\.\*|\.\+){2,}/.test(pattern)) return true
|
|
44
|
+
// Check for quantified alternation groups like (a|aa)+, (a|b)+, (a|b)*, (a|aa){2,}
|
|
45
|
+
let simplified = pattern
|
|
46
|
+
while (/\(\(([^()]+)\)\)/.test(simplified)) {
|
|
47
|
+
simplified = simplified.replace(/\(\(([^()]+)\)\)/g, '($1)')
|
|
48
|
+
}
|
|
49
|
+
if (/\((?:[^()]*\|)+[^()]*\)\s*([+*]|\{[0-9]+,[0-9]*\})/.test(simplified)) return true
|
|
44
50
|
return false
|
|
45
51
|
}
|
|
46
52
|
|
package/lib/reporting.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sanitizeSecretString } from './credentials.js'
|
|
1
2
|
import { createHash } from 'node:crypto'
|
|
2
3
|
|
|
3
4
|
export const DEFAULT_NOTIFICATION_LIMIT = 50
|
|
@@ -14,10 +15,7 @@ export function notificationLimit(value) {
|
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
function safeMessage(value) {
|
|
17
|
-
return
|
|
18
|
-
.replace(/(api[_-]?key|token|secret|authorization|password)\s*[:=]\s*[^\s,;]+/gi, '$1=[redacted]')
|
|
19
|
-
.replace(/([?&](?:key|token|api[_-]?key|access[_-]?token)=)[^&#\s]+/gi, '$1[redacted]')
|
|
20
|
-
.slice(0, 240)
|
|
18
|
+
return sanitizeSecretString(value).slice(0, 240)
|
|
21
19
|
}
|
|
22
20
|
|
|
23
21
|
function stable(value) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sanitizeSecretString } from './credentials.js'
|
|
1
2
|
import { statusOfError } from './reliability.js'
|
|
2
3
|
import { historyEntry } from './history.js'
|
|
3
4
|
import { normalizeModels } from './models.js'
|
|
@@ -89,7 +90,7 @@ export function errorMessage(error) {
|
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
export function diagnosticMessage(error) {
|
|
92
|
-
return errorMessage(error)
|
|
93
|
+
return sanitizeSecretString(errorMessage(error)).slice(0, 160)
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
export function adapterErrorType(error) {
|
package/lib/synchronizer.js
CHANGED
|
@@ -571,14 +571,18 @@ export function createModelSynchronizer(ctx, { getConfig = () => ({}), saveConfi
|
|
|
571
571
|
notifications: notificationLedger,
|
|
572
572
|
updateNotification: updateNotificationState,
|
|
573
573
|
rollbackHistory,
|
|
574
|
-
testWebhook: async ({ url, secret }) => {
|
|
574
|
+
testWebhook: async ({ url, secret, secretRef }) => {
|
|
575
575
|
let resolvedSecret = secret || ''
|
|
576
|
-
|
|
576
|
+
const targetRef = (secretRef || (typeof secret === 'string' && /^[A-Za-z_][A-Za-z0-9_]*$/.test(secret) ? secret : '')).trim()
|
|
577
|
+
if (targetRef) {
|
|
577
578
|
try {
|
|
578
|
-
const res = await (ctx?.credentials?.resolve ? ctx.credentials.resolve(
|
|
579
|
-
if (res) resolvedSecret = typeof res === 'string' ? res : res?.value ||
|
|
579
|
+
const res = await (ctx?.credentials?.resolve ? ctx.credentials.resolve(targetRef) : ctx?.get?.('credentials')?.resolve?.(targetRef))
|
|
580
|
+
if (res) resolvedSecret = typeof res === 'string' ? res : res?.value || ''
|
|
580
581
|
} catch (err) {
|
|
581
|
-
ctx?.logger?.
|
|
582
|
+
ctx?.logger?.warn?.('[dsh-model-sync] secret ref resolution failed in testWebhook:', err)
|
|
583
|
+
}
|
|
584
|
+
if (!resolvedSecret && secretRef) {
|
|
585
|
+
return { ok: false, error: `Failed to resolve webhook secret credential reference '${secretRef}'` }
|
|
582
586
|
}
|
|
583
587
|
}
|
|
584
588
|
return sendWebhookNotification(url, resolvedSecret, { event: 'model-sync.test', timestamp: Date.now(), test: true })
|
package/lib/updater.js
CHANGED
|
@@ -20,19 +20,33 @@ function header(request, name) {
|
|
|
20
20
|
return Array.isArray(value) ? value[0] : value
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
function isLoopbackIPv4(address) {
|
|
24
|
+
if (typeof address !== 'string' || !address.startsWith('127.')) return false
|
|
25
|
+
const parts = address.split('.')
|
|
26
|
+
if (parts.length !== 4) return false
|
|
27
|
+
return parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) >= 0 && Number(part) <= 255)
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
export function isLoopback(value) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
address
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
)
|
|
31
|
+
if (typeof value !== 'string') return false
|
|
32
|
+
const address = value.toLowerCase().replace(/^\[|\]$/g, '')
|
|
33
|
+
if (address === 'localhost' || address === 'localhost.' || address === '::1') return true
|
|
34
|
+
if (address.startsWith('::ffff:')) {
|
|
35
|
+
return isLoopbackIPv4(address.slice('::ffff:'.length))
|
|
36
|
+
}
|
|
37
|
+
return isLoopbackIPv4(address)
|
|
32
38
|
}
|
|
33
39
|
|
|
34
|
-
export function isTrustedUpdateRequest(request) {
|
|
40
|
+
export function isTrustedUpdateRequest(request, targetCtx = null) {
|
|
35
41
|
if (header(request, UPDATE_HEADER) !== '1') return false
|
|
42
|
+
|
|
43
|
+
// 1. Core connection rejection check (#237)
|
|
44
|
+
const conn = targetCtx?.connection || targetCtx?.get?.('connection') || request?.connection
|
|
45
|
+
if (conn && typeof conn.requestRejection === 'function') {
|
|
46
|
+
const rejection = conn.requestRejection(request)
|
|
47
|
+
if (rejection !== undefined) return false
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
const remote = request.socket?.remoteAddress
|
|
37
51
|
if (!isLoopback(remote)) return false
|
|
38
52
|
const site = header(request, 'sec-fetch-site')
|
|
@@ -295,6 +309,21 @@ export function registerPluginUpdater(ctx, options = {}) {
|
|
|
295
309
|
|
|
296
310
|
const handler = async (request, response) => {
|
|
297
311
|
try {
|
|
312
|
+
// 1. Core connection rejection check on all updater routes (#237)
|
|
313
|
+
const conn = ctx?.connection || ctx?.get?.('connection') || request?.connection
|
|
314
|
+
if (conn && typeof conn.requestRejection === 'function') {
|
|
315
|
+
const rejection = conn.requestRejection(request)
|
|
316
|
+
if (rejection !== undefined) {
|
|
317
|
+
const status = typeof rejection === 'number' ? rejection : 401
|
|
318
|
+
response.writeHead(status, {
|
|
319
|
+
'content-type': 'application/json; charset=utf-8',
|
|
320
|
+
'cache-control': 'no-store',
|
|
321
|
+
})
|
|
322
|
+
response.end(JSON.stringify({ error: 'unauthorized', code: status }))
|
|
323
|
+
return
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
298
327
|
const target = runtime()
|
|
299
328
|
if (request.method === 'GET' || request.method === 'HEAD') {
|
|
300
329
|
const isFresh = Boolean(
|
|
@@ -317,7 +346,7 @@ export function registerPluginUpdater(ctx, options = {}) {
|
|
|
317
346
|
response.end()
|
|
318
347
|
return
|
|
319
348
|
}
|
|
320
|
-
if (!isTrustedUpdateRequest(request)) {
|
|
349
|
+
if (!isTrustedUpdateRequest(request, ctx)) {
|
|
321
350
|
json(response, 403, { error: 'Rejected non-local or cross-origin update request.' })
|
|
322
351
|
return
|
|
323
352
|
}
|