@apidevtools/json-schema-ref-parser 15.3.5 → 15.3.6

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/README.md CHANGED
@@ -15,7 +15,7 @@ Install using [npm](https://docs.npmjs.com/about-npm/):
15
15
 
16
16
  ```bash
17
17
  npm install @apidevtools/json-schema-ref-parser
18
- yarn add @apidevtools/json-schema-ref-parser
18
+ pnpm add @apidevtools/json-schema-ref-parser
19
19
  bun add @apidevtools/json-schema-ref-parser
20
20
  ```
21
21
 
@@ -142,10 +142,10 @@ To build/test the project locally on your computer:
142
142
  `git clone https://github.com/APIDevTools/json-schema-ref-parser.git`
143
143
 
144
144
  2. **Install dependencies**<br>
145
- `yarn install`
145
+ `pnpm install`
146
146
 
147
147
  3. **Run the tests**<br>
148
- `yarn test`
148
+ `pnpm test`
149
149
 
150
150
  ## License
151
151
 
@@ -7,6 +7,7 @@ const slashes = /\//g;
7
7
  const tildes = /~/g;
8
8
  const escapedSlash = /~1/g;
9
9
  const escapedTilde = /~0/g;
10
+ const unsafeSetTokens = new Set(["__proto__", "constructor", "prototype"]);
10
11
  /**
11
12
  * This class represents a single JSON pointer and its resolved value.
12
13
  *
@@ -161,6 +162,7 @@ class Pointer {
161
162
  this.value = value;
162
163
  return value;
163
164
  }
165
+ assertSafeSetTokens(this.path, tokens);
164
166
  // Crawl the object, one token at a time
165
167
  this.value = unwrapOrThrow(obj);
166
168
  if (this.$ref.dynamicIdScope && !isAliasedResource(this.$ref)) {
@@ -316,6 +318,13 @@ function setValue(pointer, token, value) {
316
318
  }
317
319
  return value;
318
320
  }
321
+ function assertSafeSetTokens(pointerPath, tokens) {
322
+ for (const token of tokens) {
323
+ if (unsafeSetTokens.has(token)) {
324
+ throw new JSONParserError(`Error assigning $ref pointer "${pointerPath}". \nUnsafe JSON Pointer token "${token}" cannot be used for assignment.`);
325
+ }
326
+ }
327
+ }
319
328
  function unwrapOrThrow(value) {
320
329
  if (isHandledError(value)) {
321
330
  throw value;
@@ -63,6 +63,9 @@ async function download(u, httpOptions, _redirects) {
63
63
  const redirects = _redirects || [];
64
64
  redirects.push(u.href);
65
65
  try {
66
+ if (httpOptions.safeUrlResolver && url.isUnsafeUrl(u.href)) {
67
+ throw new Error(`Unsafe URL blocked by safeUrlResolver: ${u.href}`);
68
+ }
66
69
  const res = await get(u, httpOptions);
67
70
  if (res.status >= 400) {
68
71
  const error = new Error(`HTTP ERROR ${res.status}`);
@@ -75,13 +78,14 @@ async function download(u, httpOptions, _redirects) {
75
78
  error.status = res.status;
76
79
  throw new ResolverError(error);
77
80
  }
78
- else if (!("location" in res.headers) || !res.headers.location) {
79
- const error = new Error(`HTTP ${res.status} redirect with no location header`);
80
- error.status = res.status;
81
- throw error;
82
- }
83
81
  else {
84
- const redirectTo = url.resolve(u.href, res.headers.location);
82
+ const location = getHeader(res, "location");
83
+ if (!location) {
84
+ const error = new Error(`HTTP ${res.status} redirect with no location header`);
85
+ error.status = res.status;
86
+ throw error;
87
+ }
88
+ const redirectTo = url.resolve(u.href, location);
85
89
  return download(redirectTo, httpOptions, redirects);
86
90
  }
87
91
  }
@@ -114,6 +118,7 @@ async function get(u, httpOptions) {
114
118
  method: "GET",
115
119
  headers: httpOptions.headers || {},
116
120
  credentials: httpOptions.withCredentials ? "include" : "same-origin",
121
+ redirect: "manual",
117
122
  signal: controller ? controller.signal : null,
118
123
  });
119
124
  if (timeoutId) {
@@ -121,3 +126,6 @@ async function get(u, httpOptions) {
121
126
  }
122
127
  return response;
123
128
  }
129
+ function getHeader(response, name) {
130
+ return response.headers.get(name);
131
+ }
@@ -12,6 +12,7 @@ const urlEncodePatterns = [
12
12
  ];
13
13
  // RegExp patterns to URL-decode special characters for local filesystem paths
14
14
  const urlDecodePatterns = [/%23/g, "#", /%24/g, "$", /%26/g, "&", /%2C/g, ",", /%40/g, "@"];
15
+ const unsafeDomainSuffixes = [".localhost", ".local", ".internal", ".intranet", ".corp", ".home", ".lan"];
15
16
  export const parse = (u) => new URL(u);
16
17
  /**
17
18
  * Returns resolved target URL relative to a base URL in a manner similar to that of a Web browser resolving an anchor tag HREF.
@@ -191,53 +192,10 @@ export function isUnsafeUrl(path) {
191
192
  if (typeof window !== "undefined" && window.location && window.location.href) {
192
193
  return false;
193
194
  }
194
- // Local/internal network addresses
195
- const localPatterns = [
196
- // Localhost variations
197
- "localhost",
198
- "127.0.0.1",
199
- "::1",
200
- // Private IP ranges (RFC 1918)
201
- "10.",
202
- "172.16.",
203
- "172.17.",
204
- "172.18.",
205
- "172.19.",
206
- "172.20.",
207
- "172.21.",
208
- "172.22.",
209
- "172.23.",
210
- "172.24.",
211
- "172.25.",
212
- "172.26.",
213
- "172.27.",
214
- "172.28.",
215
- "172.29.",
216
- "172.30.",
217
- "172.31.",
218
- "192.168.",
219
- // Link-local addresses
220
- "169.254.",
221
- // Internal domains
222
- ".local",
223
- ".internal",
224
- ".intranet",
225
- ".corp",
226
- ".home",
227
- ".lan",
228
- ];
229
195
  try {
230
196
  // Try to parse as URL
231
197
  const url = new URL(normalizedPath.startsWith("//") ? "http:" + normalizedPath : normalizedPath);
232
- const hostname = url.hostname.toLowerCase();
233
- // Check against local patterns
234
- for (const pattern of localPatterns) {
235
- if (hostname === pattern || hostname.startsWith(pattern) || hostname.endsWith(pattern)) {
236
- return true;
237
- }
238
- }
239
- // Check for IP addresses in private ranges
240
- if (isPrivateIP(hostname)) {
198
+ if (isUnsafeHostname(url.hostname)) {
241
199
  return true;
242
200
  }
243
201
  // Check for non-standard ports that might indicate internal services
@@ -252,32 +210,157 @@ export function isUnsafeUrl(path) {
252
210
  if (normalizedPath.startsWith("/") && !normalizedPath.startsWith("//")) {
253
211
  return false;
254
212
  }
255
- // Check for localhost patterns in non-URL strings
256
- for (const pattern of localPatterns) {
257
- if (normalizedPath.includes(pattern)) {
258
- return true;
259
- }
213
+ if (containsUnsafeHostname(normalizedPath)) {
214
+ return true;
260
215
  }
261
216
  }
262
217
  return false;
263
218
  }
264
219
  /**
265
- * Helper function to check if an IP address is in a private range
220
+ * Helper function to check if a hostname is local or resolves to a non-public literal address.
266
221
  */
267
- function isPrivateIP(ip) {
268
- const ipRegex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
269
- const match = ip.match(ipRegex);
270
- if (!match) {
271
- return false;
222
+ function isUnsafeHostname(hostname) {
223
+ const normalizedHostname = normalizeHostname(hostname);
224
+ if (!normalizedHostname) {
225
+ return true;
226
+ }
227
+ if (normalizedHostname === "localhost" || unsafeDomainSuffixes.some((suffix) => normalizedHostname.endsWith(suffix))) {
228
+ return true;
272
229
  }
273
- const [, a, b, c, d] = match.map(Number);
274
- // Validate IP format
275
- if (a > 255 || b > 255 || c > 255 || d > 255) {
230
+ const ipv4 = parseIPv4Address(normalizedHostname);
231
+ if (ipv4) {
232
+ return isUnsafeIPv4Address(ipv4);
233
+ }
234
+ const ipv6 = parseIPv6Address(normalizedHostname);
235
+ if (ipv6) {
236
+ return isUnsafeIPv6Address(ipv6);
237
+ }
238
+ return false;
239
+ }
240
+ function normalizeHostname(hostname) {
241
+ let normalizedHostname = hostname.trim().toLowerCase();
242
+ if (normalizedHostname.startsWith("[") && normalizedHostname.endsWith("]")) {
243
+ normalizedHostname = normalizedHostname.slice(1, -1);
244
+ }
245
+ while (normalizedHostname.endsWith(".")) {
246
+ normalizedHostname = normalizedHostname.slice(0, -1);
247
+ }
248
+ return normalizedHostname;
249
+ }
250
+ function parseIPv4Address(ip) {
251
+ const parts = ip.split(".");
252
+ if (parts.length !== 4) {
253
+ return undefined;
254
+ }
255
+ const octets = parts.map((part) => {
256
+ if (!/^\d+$/.test(part)) {
257
+ return Number.NaN;
258
+ }
259
+ return Number(part);
260
+ });
261
+ if (octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255)) {
262
+ return undefined;
263
+ }
264
+ return octets;
265
+ }
266
+ /**
267
+ * Helper function to check if an IPv4 address is in a non-public range.
268
+ */
269
+ function isUnsafeIPv4Address([a, b, c, d]) {
270
+ return (a === 0 ||
271
+ a === 10 ||
272
+ a === 127 ||
273
+ (a === 100 && b >= 64 && b <= 127) ||
274
+ (a === 169 && b === 254) ||
275
+ (a === 172 && b >= 16 && b <= 31) ||
276
+ (a === 192 && b === 0 && c === 0) ||
277
+ (a === 192 && b === 168) ||
278
+ (a === 198 && (b === 18 || b === 19)) ||
279
+ a >= 224 ||
280
+ (a === 255 && b === 255 && c === 255 && d === 255));
281
+ }
282
+ function parseIPv6Address(ip) {
283
+ if (!ip.includes(":")) {
284
+ return undefined;
285
+ }
286
+ let normalizedIP = ip;
287
+ const lastSeparator = normalizedIP.lastIndexOf(":");
288
+ const possibleIPv4 = normalizedIP.slice(lastSeparator + 1);
289
+ if (possibleIPv4.includes(".")) {
290
+ const ipv4 = parseIPv4Address(possibleIPv4);
291
+ if (!ipv4) {
292
+ return undefined;
293
+ }
294
+ const firstGroup = ipv4[0] * 256 + ipv4[1];
295
+ const secondGroup = ipv4[2] * 256 + ipv4[3];
296
+ normalizedIP = `${normalizedIP.slice(0, lastSeparator + 1)}${firstGroup.toString(16)}:${secondGroup.toString(16)}`;
297
+ }
298
+ const halves = normalizedIP.split("::");
299
+ if (halves.length > 2) {
300
+ return undefined;
301
+ }
302
+ const head = parseIPv6Groups(halves[0]);
303
+ const tail = halves.length === 2 ? parseIPv6Groups(halves[1]) : [];
304
+ if (!head || !tail) {
305
+ return undefined;
306
+ }
307
+ if (halves.length === 1) {
308
+ return head.length === 8 ? head : undefined;
309
+ }
310
+ const missingGroups = 8 - head.length - tail.length;
311
+ if (missingGroups < 1) {
312
+ return undefined;
313
+ }
314
+ return [...head, ...Array(missingGroups).fill(0), ...tail];
315
+ }
316
+ function parseIPv6Groups(groups) {
317
+ if (!groups) {
318
+ return [];
319
+ }
320
+ const parsedGroups = groups.split(":").map((group) => {
321
+ if (!/^[\da-f]{1,4}$/i.test(group)) {
322
+ return Number.NaN;
323
+ }
324
+ return Number.parseInt(group, 16);
325
+ });
326
+ if (parsedGroups.some((group) => !Number.isInteger(group) || group < 0 || group > 0xffff)) {
327
+ return undefined;
328
+ }
329
+ return parsedGroups;
330
+ }
331
+ /**
332
+ * Helper function to check if an IPv6 address is in a non-public range.
333
+ */
334
+ function isUnsafeIPv6Address(groups) {
335
+ if (groups.length !== 8) {
276
336
  return false;
277
337
  }
278
- // Private IP ranges
279
- return (a === 10 || a === 127 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || (a === 169 && b === 254) // Link-local
280
- );
338
+ const isUnspecified = groups.every((group) => group === 0);
339
+ const isLoopback = groups.slice(0, 7).every((group) => group === 0) && groups[7] === 1;
340
+ const isUniqueLocal = (groups[0] & 0xfe00) === 0xfc00;
341
+ const isLinkLocal = (groups[0] & 0xffc0) === 0xfe80;
342
+ const isMulticast = (groups[0] & 0xff00) === 0xff00;
343
+ if (isUnspecified || isLoopback || isUniqueLocal || isLinkLocal || isMulticast) {
344
+ return true;
345
+ }
346
+ const mappedIPv4 = getMappedIPv4Address(groups);
347
+ return mappedIPv4 ? isUnsafeIPv4Address(mappedIPv4) : false;
348
+ }
349
+ function getMappedIPv4Address(groups) {
350
+ const firstFiveGroupsAreZero = groups.slice(0, 5).every((group) => group === 0);
351
+ const firstSixGroupsAreZero = firstFiveGroupsAreZero && groups[5] === 0;
352
+ const isIPv4Mapped = firstFiveGroupsAreZero && groups[5] === 0xffff;
353
+ if (!firstSixGroupsAreZero && !isIPv4Mapped) {
354
+ return undefined;
355
+ }
356
+ return [Math.floor(groups[6] / 256), groups[6] % 256, Math.floor(groups[7] / 256), groups[7] % 256];
357
+ }
358
+ function containsUnsafeHostname(value) {
359
+ const candidates = value
360
+ .split(/[\s/?#]+/)
361
+ .map((candidate) => candidate.replace(/^[a-z][\d+.a-z-]*:\/\//i, "").replace(/:\d+$/, ""))
362
+ .filter(Boolean);
363
+ return candidates.some((candidate) => isUnsafeHostname(candidate));
281
364
  }
282
365
  /**
283
366
  * Helper function to check if a port is typically used for internal services
package/lib/pointer.ts CHANGED
@@ -13,6 +13,7 @@ const slashes = /\//g;
13
13
  const tildes = /~/g;
14
14
  const escapedSlash = /~1/g;
15
15
  const escapedTilde = /~0/g;
16
+ const unsafeSetTokens = new Set(["__proto__", "constructor", "prototype"]);
16
17
 
17
18
  /**
18
19
  * This class represents a single JSON pointer and its resolved value.
@@ -194,6 +195,8 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
194
195
  return value;
195
196
  }
196
197
 
198
+ assertSafeSetTokens(this.path, tokens);
199
+
197
200
  // Crawl the object, one token at a time
198
201
  this.value = unwrapOrThrow(obj);
199
202
  if (this.$ref.dynamicIdScope && !isAliasedResource(this.$ref)) {
@@ -372,6 +375,16 @@ function setValue(pointer: Pointer, token: string, value: JSONSchema4Type | JSON
372
375
  return value;
373
376
  }
374
377
 
378
+ function assertSafeSetTokens(pointerPath: string, tokens: string[]) {
379
+ for (const token of tokens) {
380
+ if (unsafeSetTokens.has(token)) {
381
+ throw new JSONParserError(
382
+ `Error assigning $ref pointer "${pointerPath}". \nUnsafe JSON Pointer token "${token}" cannot be used for assignment.`,
383
+ );
384
+ }
385
+ }
386
+ }
387
+
375
388
  function unwrapOrThrow(value: unknown) {
376
389
  if (isHandledError(value)) {
377
390
  throw value;
@@ -80,6 +80,10 @@ async function download<S extends object = JSONSchema>(
80
80
  redirects.push(u.href);
81
81
 
82
82
  try {
83
+ if (httpOptions.safeUrlResolver && url.isUnsafeUrl(u.href)) {
84
+ throw new Error(`Unsafe URL blocked by safeUrlResolver: ${u.href}`);
85
+ }
86
+
83
87
  const res = await get(u, httpOptions);
84
88
  if (res.status >= 400) {
85
89
  const error = new Error(`HTTP ERROR ${res.status}`) as Error & { status?: number };
@@ -92,12 +96,16 @@ async function download<S extends object = JSONSchema>(
92
96
  ) as Error & { status?: number };
93
97
  error.status = res.status;
94
98
  throw new ResolverError(error);
95
- } else if (!("location" in res.headers) || !res.headers.location) {
96
- const error = new Error(`HTTP ${res.status} redirect with no location header`) as Error & { status?: number };
97
- error.status = res.status;
98
- throw error;
99
99
  } else {
100
- const redirectTo = url.resolve(u.href, res.headers.location as string);
100
+ const location = getHeader(res, "location");
101
+
102
+ if (!location) {
103
+ const error = new Error(`HTTP ${res.status} redirect with no location header`) as Error & { status?: number };
104
+ error.status = res.status;
105
+ throw error;
106
+ }
107
+
108
+ const redirectTo = url.resolve(u.href, location);
101
109
  return download(redirectTo, httpOptions, redirects);
102
110
  }
103
111
  } else {
@@ -130,6 +138,7 @@ async function get<S extends object = JSONSchema>(u: RequestInfo | URL, httpOpti
130
138
  method: "GET",
131
139
  headers: httpOptions.headers || {},
132
140
  credentials: httpOptions.withCredentials ? "include" : "same-origin",
141
+ redirect: "manual",
133
142
  signal: controller ? controller.signal : null,
134
143
  });
135
144
  if (timeoutId) {
@@ -138,3 +147,7 @@ async function get<S extends object = JSONSchema>(u: RequestInfo | URL, httpOpti
138
147
 
139
148
  return response;
140
149
  }
150
+
151
+ function getHeader(response: Response, name: string): string | null {
152
+ return response.headers.get(name);
153
+ }
package/lib/util/url.ts CHANGED
@@ -18,6 +18,8 @@ const urlEncodePatterns = [
18
18
  // RegExp patterns to URL-decode special characters for local filesystem paths
19
19
  const urlDecodePatterns = [/%23/g, "#", /%24/g, "$", /%26/g, "&", /%2C/g, ",", /%40/g, "@"];
20
20
 
21
+ const unsafeDomainSuffixes = [".localhost", ".local", ".internal", ".intranet", ".corp", ".home", ".lan"];
22
+
21
23
  export const parse = (u: string | URL) => new URL(u);
22
24
 
23
25
  /**
@@ -211,60 +213,11 @@ export function isUnsafeUrl(path: string | unknown): boolean {
211
213
  return false;
212
214
  }
213
215
 
214
- // Local/internal network addresses
215
- const localPatterns = [
216
- // Localhost variations
217
- "localhost",
218
- "127.0.0.1",
219
- "::1",
220
-
221
- // Private IP ranges (RFC 1918)
222
- "10.",
223
- "172.16.",
224
- "172.17.",
225
- "172.18.",
226
- "172.19.",
227
- "172.20.",
228
- "172.21.",
229
- "172.22.",
230
- "172.23.",
231
- "172.24.",
232
- "172.25.",
233
- "172.26.",
234
- "172.27.",
235
- "172.28.",
236
- "172.29.",
237
- "172.30.",
238
- "172.31.",
239
- "192.168.",
240
-
241
- // Link-local addresses
242
- "169.254.",
243
-
244
- // Internal domains
245
- ".local",
246
- ".internal",
247
- ".intranet",
248
- ".corp",
249
- ".home",
250
- ".lan",
251
- ];
252
-
253
216
  try {
254
217
  // Try to parse as URL
255
218
  const url = new URL(normalizedPath.startsWith("//") ? "http:" + normalizedPath : normalizedPath);
256
219
 
257
- const hostname = url.hostname.toLowerCase();
258
-
259
- // Check against local patterns
260
- for (const pattern of localPatterns) {
261
- if (hostname === pattern || hostname.startsWith(pattern) || hostname.endsWith(pattern)) {
262
- return true;
263
- }
264
- }
265
-
266
- // Check for IP addresses in private ranges
267
- if (isPrivateIP(hostname)) {
220
+ if (isUnsafeHostname(url.hostname)) {
268
221
  return true;
269
222
  }
270
223
 
@@ -281,11 +234,8 @@ export function isUnsafeUrl(path: string | unknown): boolean {
281
234
  return false;
282
235
  }
283
236
 
284
- // Check for localhost patterns in non-URL strings
285
- for (const pattern of localPatterns) {
286
- if (normalizedPath.includes(pattern)) {
287
- return true;
288
- }
237
+ if (containsUnsafeHostname(normalizedPath)) {
238
+ return true;
289
239
  }
290
240
  }
291
241
 
@@ -293,29 +243,198 @@ export function isUnsafeUrl(path: string | unknown): boolean {
293
243
  }
294
244
 
295
245
  /**
296
- * Helper function to check if an IP address is in a private range
246
+ * Helper function to check if a hostname is local or resolves to a non-public literal address.
297
247
  */
298
- function isPrivateIP(ip: string): boolean {
299
- const ipRegex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
300
- const match = ip.match(ipRegex);
248
+ function isUnsafeHostname(hostname: string): boolean {
249
+ const normalizedHostname = normalizeHostname(hostname);
301
250
 
302
- if (!match) {
303
- return false;
251
+ if (!normalizedHostname) {
252
+ return true;
304
253
  }
305
254
 
306
- const [, a, b, c, d] = match.map(Number);
255
+ if (normalizedHostname === "localhost" || unsafeDomainSuffixes.some((suffix) => normalizedHostname.endsWith(suffix))) {
256
+ return true;
257
+ }
307
258
 
308
- // Validate IP format
309
- if (a > 255 || b > 255 || c > 255 || d > 255) {
310
- return false;
259
+ const ipv4 = parseIPv4Address(normalizedHostname);
260
+ if (ipv4) {
261
+ return isUnsafeIPv4Address(ipv4);
262
+ }
263
+
264
+ const ipv6 = parseIPv6Address(normalizedHostname);
265
+ if (ipv6) {
266
+ return isUnsafeIPv6Address(ipv6);
311
267
  }
312
268
 
313
- // Private IP ranges
269
+ return false;
270
+ }
271
+
272
+ function normalizeHostname(hostname: string): string {
273
+ let normalizedHostname = hostname.trim().toLowerCase();
274
+
275
+ if (normalizedHostname.startsWith("[") && normalizedHostname.endsWith("]")) {
276
+ normalizedHostname = normalizedHostname.slice(1, -1);
277
+ }
278
+
279
+ while (normalizedHostname.endsWith(".")) {
280
+ normalizedHostname = normalizedHostname.slice(0, -1);
281
+ }
282
+
283
+ return normalizedHostname;
284
+ }
285
+
286
+ function parseIPv4Address(ip: string): number[] | undefined {
287
+ const parts = ip.split(".");
288
+
289
+ if (parts.length !== 4) {
290
+ return undefined;
291
+ }
292
+
293
+ const octets = parts.map((part) => {
294
+ if (!/^\d+$/.test(part)) {
295
+ return Number.NaN;
296
+ }
297
+
298
+ return Number(part);
299
+ });
300
+
301
+ if (octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255)) {
302
+ return undefined;
303
+ }
304
+
305
+ return octets;
306
+ }
307
+
308
+ /**
309
+ * Helper function to check if an IPv4 address is in a non-public range.
310
+ */
311
+ function isUnsafeIPv4Address([a, b, c, d]: number[]): boolean {
314
312
  return (
315
- a === 10 || a === 127 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || (a === 169 && b === 254) // Link-local
313
+ a === 0 ||
314
+ a === 10 ||
315
+ a === 127 ||
316
+ (a === 100 && b >= 64 && b <= 127) ||
317
+ (a === 169 && b === 254) ||
318
+ (a === 172 && b >= 16 && b <= 31) ||
319
+ (a === 192 && b === 0 && c === 0) ||
320
+ (a === 192 && b === 168) ||
321
+ (a === 198 && (b === 18 || b === 19)) ||
322
+ a >= 224 ||
323
+ (a === 255 && b === 255 && c === 255 && d === 255)
316
324
  );
317
325
  }
318
326
 
327
+ function parseIPv6Address(ip: string): number[] | undefined {
328
+ if (!ip.includes(":")) {
329
+ return undefined;
330
+ }
331
+
332
+ let normalizedIP = ip;
333
+ const lastSeparator = normalizedIP.lastIndexOf(":");
334
+ const possibleIPv4 = normalizedIP.slice(lastSeparator + 1);
335
+
336
+ if (possibleIPv4.includes(".")) {
337
+ const ipv4 = parseIPv4Address(possibleIPv4);
338
+
339
+ if (!ipv4) {
340
+ return undefined;
341
+ }
342
+
343
+ const firstGroup = ipv4[0] * 256 + ipv4[1];
344
+ const secondGroup = ipv4[2] * 256 + ipv4[3];
345
+ normalizedIP = `${normalizedIP.slice(0, lastSeparator + 1)}${firstGroup.toString(16)}:${secondGroup.toString(16)}`;
346
+ }
347
+
348
+ const halves = normalizedIP.split("::");
349
+
350
+ if (halves.length > 2) {
351
+ return undefined;
352
+ }
353
+
354
+ const head = parseIPv6Groups(halves[0]);
355
+ const tail = halves.length === 2 ? parseIPv6Groups(halves[1]) : [];
356
+
357
+ if (!head || !tail) {
358
+ return undefined;
359
+ }
360
+
361
+ if (halves.length === 1) {
362
+ return head.length === 8 ? head : undefined;
363
+ }
364
+
365
+ const missingGroups = 8 - head.length - tail.length;
366
+
367
+ if (missingGroups < 1) {
368
+ return undefined;
369
+ }
370
+
371
+ return [...head, ...Array<number>(missingGroups).fill(0), ...tail];
372
+ }
373
+
374
+ function parseIPv6Groups(groups: string): number[] | undefined {
375
+ if (!groups) {
376
+ return [];
377
+ }
378
+
379
+ const parsedGroups = groups.split(":").map((group) => {
380
+ if (!/^[\da-f]{1,4}$/i.test(group)) {
381
+ return Number.NaN;
382
+ }
383
+
384
+ return Number.parseInt(group, 16);
385
+ });
386
+
387
+ if (parsedGroups.some((group) => !Number.isInteger(group) || group < 0 || group > 0xffff)) {
388
+ return undefined;
389
+ }
390
+
391
+ return parsedGroups;
392
+ }
393
+
394
+ /**
395
+ * Helper function to check if an IPv6 address is in a non-public range.
396
+ */
397
+ function isUnsafeIPv6Address(groups: number[]): boolean {
398
+ if (groups.length !== 8) {
399
+ return false;
400
+ }
401
+
402
+ const isUnspecified = groups.every((group) => group === 0);
403
+ const isLoopback = groups.slice(0, 7).every((group) => group === 0) && groups[7] === 1;
404
+ const isUniqueLocal = (groups[0] & 0xfe00) === 0xfc00;
405
+ const isLinkLocal = (groups[0] & 0xffc0) === 0xfe80;
406
+ const isMulticast = (groups[0] & 0xff00) === 0xff00;
407
+
408
+ if (isUnspecified || isLoopback || isUniqueLocal || isLinkLocal || isMulticast) {
409
+ return true;
410
+ }
411
+
412
+ const mappedIPv4 = getMappedIPv4Address(groups);
413
+
414
+ return mappedIPv4 ? isUnsafeIPv4Address(mappedIPv4) : false;
415
+ }
416
+
417
+ function getMappedIPv4Address(groups: number[]): number[] | undefined {
418
+ const firstFiveGroupsAreZero = groups.slice(0, 5).every((group) => group === 0);
419
+ const firstSixGroupsAreZero = firstFiveGroupsAreZero && groups[5] === 0;
420
+ const isIPv4Mapped = firstFiveGroupsAreZero && groups[5] === 0xffff;
421
+
422
+ if (!firstSixGroupsAreZero && !isIPv4Mapped) {
423
+ return undefined;
424
+ }
425
+
426
+ return [Math.floor(groups[6] / 256), groups[6] % 256, Math.floor(groups[7] / 256), groups[7] % 256];
427
+ }
428
+
429
+ function containsUnsafeHostname(value: string): boolean {
430
+ const candidates = value
431
+ .split(/[\s/?#]+/)
432
+ .map((candidate) => candidate.replace(/^[a-z][\d+.a-z-]*:\/\//i, "").replace(/:\d+$/, ""))
433
+ .filter(Boolean);
434
+
435
+ return candidates.some((candidate) => isUnsafeHostname(candidate));
436
+ }
437
+
319
438
  /**
320
439
  * Helper function to check if a port is typically used for internal services
321
440
  */
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "@apidevtools/json-schema-ref-parser",
3
- "version": "15.3.5",
3
+ "version": "15.3.6",
4
4
  "description": "Parse, Resolve, and Dereference JSON Schema $ref pointers",
5
5
  "type": "module",
6
6
  "types": "dist/lib/index.d.ts",
7
7
  "module": "dist/lib/index.js",
8
8
  "main": "dist/lib/index.js",
9
9
  "scripts": {
10
- "prepublishOnly": "yarn build",
10
+ "prepublishOnly": "pnpm build",
11
11
  "lint": "eslint lib",
12
12
  "build": "rimraf dist && tsc",
13
13
  "typecheck": "tsc --noEmit",
14
14
  "prettier": "prettier --write \"**/*.+(js|jsx|ts|tsx|har||json|css|md)\"",
15
15
  "test": "vitest --coverage",
16
16
  "test:specific": "vitest invalid",
17
- "test:node": "yarn test",
17
+ "test:node": "pnpm test",
18
18
  "test:browser": "node ./test/fixtures/run-browser-tests.mjs",
19
19
  "test:update": "vitest -u",
20
20
  "test:watch": "vitest -w"
@@ -72,32 +72,32 @@
72
72
  "@types/json-schema": "^7.0.15"
73
73
  },
74
74
  "dependencies": {
75
- "js-yaml": "^4.1.1"
75
+ "js-yaml": "^4.2.0"
76
76
  },
77
77
  "devDependencies": {
78
- "@eslint/compat": "^2.0.3",
78
+ "@eslint/compat": "^2.1.0",
79
79
  "@eslint/js": "^10.0.1",
80
80
  "@types/eslint": "^9.6.1",
81
81
  "@types/js-yaml": "^4.0.9",
82
82
  "@types/json-schema": "^7.0.15",
83
- "@types/node": "^25",
84
- "@typescript-eslint/eslint-plugin": "^8.57.2",
85
- "@typescript-eslint/parser": "^8.57.2",
86
- "@vitest/coverage-v8": "^4.1.2",
83
+ "@types/node": "^25.9.3",
84
+ "@typescript-eslint/eslint-plugin": "^8.61.0",
85
+ "@typescript-eslint/parser": "^8.61.0",
86
+ "@vitest/coverage-v8": "^4.1.8",
87
87
  "cross-env": "^10.1.0",
88
- "eslint": "^10.1.0",
88
+ "eslint": "^10.4.1",
89
89
  "eslint-config-prettier": "^10.1.8",
90
90
  "eslint-plugin-import": "^2.32.0",
91
- "eslint-plugin-prettier": "^5.5.5",
92
- "eslint-plugin-promise": "^7.2.1",
91
+ "eslint-plugin-prettier": "^5.5.6",
92
+ "eslint-plugin-promise": "^7.3.0",
93
93
  "eslint-plugin-unused-imports": "^4.4.1",
94
- "globals": "^17.4.0",
95
- "jsdom": "^29.0.1",
96
- "prettier": "^3.8.1",
94
+ "globals": "^17.6.0",
95
+ "jsdom": "^29.1.1",
96
+ "prettier": "^3.8.4",
97
97
  "rimraf": "^6.1.3",
98
- "typescript": "^6.0.2",
99
- "typescript-eslint": "^8.57.2",
100
- "vitest": "^4.1.2"
98
+ "typescript": "^6.0.3",
99
+ "typescript-eslint": "^8.61.0",
100
+ "vitest": "^4.1.8"
101
101
  },
102
102
  "release": {
103
103
  "branches": [
@@ -110,5 +110,5 @@
110
110
  "@semantic-release/github"
111
111
  ]
112
112
  },
113
- "packageManager": "yarn@4.13.0"
113
+ "packageManager": "pnpm@11.5.3"
114
114
  }