@jgardner04/ghost-mcp-server 1.14.3 → 1.14.4
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/package.json
CHANGED
|
@@ -377,6 +377,40 @@ describe('urlValidator', () => {
|
|
|
377
377
|
expect(result.isValid).toBe(true);
|
|
378
378
|
});
|
|
379
379
|
});
|
|
380
|
+
|
|
381
|
+
// Regression tests for JON-151: the previous two-parser sequence
|
|
382
|
+
// (Joi.uri() + new URL()) could disagree on these edge cases, opening a
|
|
383
|
+
// path-around-isSafeHost SSRF window. The single-parser pipeline must
|
|
384
|
+
// either reject these or canonicalize them consistently with new URL().
|
|
385
|
+
describe('parse-disagreement regressions (JON-151)', () => {
|
|
386
|
+
it('should reject credentials-prefixed URL whose true host is private (127.0.0.1)', () => {
|
|
387
|
+
// WHATWG: hostname is 127.0.0.1. A naive parser that split on '@' the
|
|
388
|
+
// wrong way could see "imgur.com" and let this through. isSafeHost
|
|
389
|
+
// must see 127.0.0.1 and reject.
|
|
390
|
+
const result = validateImageUrl('https://imgur.com@127.0.0.1/image.jpg');
|
|
391
|
+
expect(result.isValid).toBe(false);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('should reject Unicode hostname not on the allowlist', () => {
|
|
395
|
+
// new URL('https://☃.com/') canonicalizes the host to xn--n3h.com
|
|
396
|
+
// (Punycode). Either form must be rejected — neither is allowlisted.
|
|
397
|
+
const result = validateImageUrl('https://☃.com/image.jpg');
|
|
398
|
+
expect(result.isValid).toBe(false);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('should reject Punycode hostname not on the allowlist', () => {
|
|
402
|
+
const result = validateImageUrl('https://xn--n3h.com/image.jpg');
|
|
403
|
+
expect(result.isValid).toBe(false);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('should reject IPv6 loopback with zone ID', () => {
|
|
407
|
+
// [::1%25eth0] is the percent-encoded form of [::1%eth0]. The host is
|
|
408
|
+
// still ::1 — the SSRF guard must catch it regardless of whether the
|
|
409
|
+
// parser accepts the zone-ID syntax or rejects the URL outright.
|
|
410
|
+
const result = validateImageUrl('http://[::1%25eth0]/image.jpg');
|
|
411
|
+
expect(result.isValid).toBe(false);
|
|
412
|
+
});
|
|
413
|
+
});
|
|
380
414
|
});
|
|
381
415
|
|
|
382
416
|
describe('createSecureAxiosConfig', () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { z } from 'zod';
|
|
2
2
|
import { URL } from 'url';
|
|
3
3
|
|
|
4
4
|
// Configure allowed domains for image downloads
|
|
@@ -69,6 +69,25 @@ const isSafeHost = (hostname) => {
|
|
|
69
69
|
);
|
|
70
70
|
};
|
|
71
71
|
|
|
72
|
+
// Single-parser URL schema. z.string().url() uses the WHATWG URL parser
|
|
73
|
+
// (same engine as new URL() below), so the schema check and the subsequent
|
|
74
|
+
// hostname extraction cannot disagree on edge cases like embedded
|
|
75
|
+
// credentials, IDN/Unicode, or IPv6 zone IDs. See JON-151.
|
|
76
|
+
const urlSchema = z
|
|
77
|
+
.string()
|
|
78
|
+
.url()
|
|
79
|
+
.refine(
|
|
80
|
+
(s) => {
|
|
81
|
+
try {
|
|
82
|
+
const proto = new URL(s).protocol;
|
|
83
|
+
return proto === 'http:' || proto === 'https:';
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
{ message: 'must use http or https scheme' }
|
|
89
|
+
);
|
|
90
|
+
|
|
72
91
|
/**
|
|
73
92
|
* Validates and sanitizes a URL for safe external requests
|
|
74
93
|
* @param {string} url - The URL to validate
|
|
@@ -76,19 +95,11 @@ const isSafeHost = (hostname) => {
|
|
|
76
95
|
*/
|
|
77
96
|
const validateImageUrl = (url) => {
|
|
78
97
|
try {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
.uri({
|
|
82
|
-
scheme: ['http', 'https'],
|
|
83
|
-
allowRelative: false,
|
|
84
|
-
})
|
|
85
|
-
.required();
|
|
86
|
-
|
|
87
|
-
const validation = urlSchema.validate(url);
|
|
88
|
-
if (validation.error) {
|
|
98
|
+
const result = urlSchema.safeParse(url);
|
|
99
|
+
if (!result.success) {
|
|
89
100
|
return {
|
|
90
101
|
isValid: false,
|
|
91
|
-
error: `Invalid URL format: ${
|
|
102
|
+
error: `Invalid URL format: ${result.error.issues[0].message}`,
|
|
92
103
|
};
|
|
93
104
|
}
|
|
94
105
|
|