@apidevtools/json-schema-ref-parser 13.0.5 → 14.0.0

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.
@@ -5,6 +5,7 @@ import { JSONParserError, InvalidPointerError, MissingPointerError, ResolverErro
5
5
  import type { ParserOptions } from "./options.js";
6
6
  import { getJsonSchemaRefParserDefaultOptions } from "./options.js";
7
7
  import type { $RefsCallback, JSONSchema, SchemaCallback, FileInfo, Plugin, ResolverOptions, HTTPResolverOptions } from "./types/index.js";
8
+ import { isUnsafeUrl } from "./util/url.js";
8
9
  export type RefParserSchema = string | JSONSchema;
9
10
  /**
10
11
  * This class parses a JSON schema, builds a map of its JSON references and their resolved values,
@@ -159,4 +160,4 @@ export declare const parse: typeof $RefParser.parse;
159
160
  export declare const resolve: typeof $RefParser.resolve;
160
161
  export declare const bundle: typeof $RefParser.bundle;
161
162
  export declare const dereference: typeof $RefParser.dereference;
162
- export { UnmatchedResolverError, JSONParserError, JSONSchema, InvalidPointerError, MissingPointerError, ResolverError, ParserError, UnmatchedParserError, ParserOptions, $RefsCallback, isHandledError, JSONParserErrorGroup, SchemaCallback, FileInfo, Plugin, ResolverOptions, HTTPResolverOptions, _dereference as dereferenceInternal, normalizeArgs as jsonSchemaParserNormalizeArgs, getJsonSchemaRefParserDefaultOptions, $Refs, };
163
+ export { UnmatchedResolverError, JSONParserError, JSONSchema, InvalidPointerError, MissingPointerError, ResolverError, ParserError, UnmatchedParserError, ParserOptions, $RefsCallback, isHandledError, JSONParserErrorGroup, SchemaCallback, FileInfo, Plugin, ResolverOptions, HTTPResolverOptions, _dereference as dereferenceInternal, normalizeArgs as jsonSchemaParserNormalizeArgs, getJsonSchemaRefParserDefaultOptions, $Refs, isUnsafeUrl, };
package/dist/lib/index.js CHANGED
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.$Refs = exports.getJsonSchemaRefParserDefaultOptions = exports.jsonSchemaParserNormalizeArgs = exports.dereferenceInternal = exports.JSONParserErrorGroup = exports.isHandledError = exports.UnmatchedParserError = exports.ParserError = exports.ResolverError = exports.MissingPointerError = exports.InvalidPointerError = exports.JSONParserError = exports.UnmatchedResolverError = exports.dereference = exports.bundle = exports.resolve = exports.parse = exports.$RefParser = void 0;
39
+ exports.isUnsafeUrl = exports.$Refs = exports.getJsonSchemaRefParserDefaultOptions = exports.jsonSchemaParserNormalizeArgs = exports.dereferenceInternal = exports.JSONParserErrorGroup = exports.isHandledError = exports.UnmatchedParserError = exports.ParserError = exports.ResolverError = exports.MissingPointerError = exports.InvalidPointerError = exports.JSONParserError = exports.UnmatchedResolverError = exports.dereference = exports.bundle = exports.resolve = exports.parse = exports.$RefParser = void 0;
40
40
  const refs_js_1 = __importDefault(require("./refs.js"));
41
41
  exports.$Refs = refs_js_1.default;
42
42
  const parse_js_1 = __importDefault(require("./parse.js"));
@@ -60,6 +60,8 @@ Object.defineProperty(exports, "JSONParserErrorGroup", { enumerable: true, get:
60
60
  const maybe_js_1 = __importDefault(require("./util/maybe.js"));
61
61
  const options_js_1 = require("./options.js");
62
62
  Object.defineProperty(exports, "getJsonSchemaRefParserDefaultOptions", { enumerable: true, get: function () { return options_js_1.getJsonSchemaRefParserDefaultOptions; } });
63
+ const url_js_1 = require("./util/url.js");
64
+ Object.defineProperty(exports, "isUnsafeUrl", { enumerable: true, get: function () { return url_js_1.isUnsafeUrl; } });
63
65
  /**
64
66
  * This class parses a JSON schema, builds a map of its JSON references and their resolved values,
65
67
  * and provides methods for traversing, manipulating, and dereferencing those references.
@@ -64,13 +64,17 @@ exports.default = {
64
64
  * Set this to `true` if you're downloading files from a CORS-enabled server that requires authentication
65
65
  */
66
66
  withCredentials: false,
67
+ /**
68
+ * Set this to `false` if you want to allow unsafe URLs (e.g., `127.0.0.1`, localhost, and other internal URLs).
69
+ */
70
+ safeUrlResolver: true,
67
71
  /**
68
72
  * Determines whether this resolver can read a given file reference.
69
73
  * Resolvers that return true will be tried in order, until one successfully resolves the file.
70
74
  * Resolvers that return false will not be given a chance to resolve the file.
71
75
  */
72
76
  canRead(file) {
73
- return url.isHttp(file.url);
77
+ return url.isHttp(file.url) && (!this.safeUrlResolver || !url.isUnsafeUrl(file.url));
74
78
  },
75
79
  /**
76
80
  * Reads the given URL and returns its raw contents as a Buffer.
@@ -25,6 +25,10 @@ export interface HTTPResolverOptions<S extends object = JSONSchema> extends Part
25
25
  * Set this to `true` if you're downloading files from a CORS-enabled server that requires authentication
26
26
  */
27
27
  withCredentials?: boolean;
28
+ /**
29
+ * Set this to `false` if you want to allow unsafe URLs (e.g., `127.0.0.1`, localhost, and other internal URLs).
30
+ */
31
+ safeUrlResolver: true;
28
32
  }
29
33
  /**
30
34
  * JSON Schema `$Ref` Parser comes with built-in resolvers for HTTP and HTTPS URLs, as well as local filesystem paths (when running in Node.js). You can add your own custom resolvers to support additional protocols, or even replace any of the built-in resolvers with your own custom implementation.
@@ -55,6 +55,13 @@ export declare function stripHash(path?: string | undefined): string;
55
55
  * @returns
56
56
  */
57
57
  export declare function isHttp(path: string): boolean;
58
+ /**
59
+ * Determines whether the given url is an unsafe or internal url.
60
+ *
61
+ * @param path - The URL or path to check
62
+ * @returns true if the URL is unsafe/internal, false otherwise
63
+ */
64
+ export declare function isUnsafeUrl(path: string): boolean;
58
65
  /**
59
66
  * Determines whether the given path is a filesystem path.
60
67
  * This includes "file://" URLs.
@@ -45,6 +45,7 @@ exports.stripQuery = stripQuery;
45
45
  exports.getHash = getHash;
46
46
  exports.stripHash = stripHash;
47
47
  exports.isHttp = isHttp;
48
+ exports.isUnsafeUrl = isUnsafeUrl;
48
49
  exports.isFileSystemPath = isFileSystemPath;
49
50
  exports.fromFileSystemPath = fromFileSystemPath;
50
51
  exports.toFileSystemPath = toFileSystemPath;
@@ -209,6 +210,152 @@ function isHttp(path) {
209
210
  return false;
210
211
  }
211
212
  }
213
+ /**
214
+ * Determines whether the given url is an unsafe or internal url.
215
+ *
216
+ * @param path - The URL or path to check
217
+ * @returns true if the URL is unsafe/internal, false otherwise
218
+ */
219
+ function isUnsafeUrl(path) {
220
+ if (!path || typeof path !== "string") {
221
+ return true;
222
+ }
223
+ // Trim whitespace and convert to lowercase for comparison
224
+ const normalizedPath = path.trim().toLowerCase();
225
+ // Empty or just whitespace
226
+ if (!normalizedPath) {
227
+ return true;
228
+ }
229
+ // JavaScript protocols
230
+ if (normalizedPath.startsWith("javascript:") ||
231
+ normalizedPath.startsWith("vbscript:") ||
232
+ normalizedPath.startsWith("data:")) {
233
+ return true;
234
+ }
235
+ // File protocol
236
+ if (normalizedPath.startsWith("file:")) {
237
+ return true;
238
+ }
239
+ // if we're in the browser, we assume that it is safe
240
+ if (typeof window !== "undefined" && window.location && window.location.href) {
241
+ return false;
242
+ }
243
+ // Local/internal network addresses
244
+ const localPatterns = [
245
+ // Localhost variations
246
+ "localhost",
247
+ "127.0.0.1",
248
+ "::1",
249
+ // Private IP ranges (RFC 1918)
250
+ "10.",
251
+ "172.16.",
252
+ "172.17.",
253
+ "172.18.",
254
+ "172.19.",
255
+ "172.20.",
256
+ "172.21.",
257
+ "172.22.",
258
+ "172.23.",
259
+ "172.24.",
260
+ "172.25.",
261
+ "172.26.",
262
+ "172.27.",
263
+ "172.28.",
264
+ "172.29.",
265
+ "172.30.",
266
+ "172.31.",
267
+ "192.168.",
268
+ // Link-local addresses
269
+ "169.254.",
270
+ // Internal domains
271
+ ".local",
272
+ ".internal",
273
+ ".intranet",
274
+ ".corp",
275
+ ".home",
276
+ ".lan",
277
+ ];
278
+ try {
279
+ // Try to parse as URL
280
+ const url = new URL(normalizedPath.startsWith("//") ? "http:" + normalizedPath : normalizedPath);
281
+ const hostname = url.hostname.toLowerCase();
282
+ // Check against local patterns
283
+ for (const pattern of localPatterns) {
284
+ if (hostname === pattern || hostname.startsWith(pattern) || hostname.endsWith(pattern)) {
285
+ return true;
286
+ }
287
+ }
288
+ // Check for IP addresses in private ranges
289
+ if (isPrivateIP(hostname)) {
290
+ return true;
291
+ }
292
+ // Check for non-standard ports that might indicate internal services
293
+ const port = url.port;
294
+ if (port && isInternalPort(parseInt(port))) {
295
+ return true;
296
+ }
297
+ }
298
+ catch (e) {
299
+ // If URL parsing fails, check if it's a relative path or contains suspicious patterns
300
+ // Relative paths starting with / are generally safe for same-origin
301
+ if (normalizedPath.startsWith("/") && !normalizedPath.startsWith("//")) {
302
+ return false;
303
+ }
304
+ // Check for localhost patterns in non-URL strings
305
+ for (const pattern of localPatterns) {
306
+ if (normalizedPath.includes(pattern)) {
307
+ return true;
308
+ }
309
+ }
310
+ }
311
+ return false;
312
+ }
313
+ /**
314
+ * Helper function to check if an IP address is in a private range
315
+ */
316
+ function isPrivateIP(ip) {
317
+ const ipRegex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
318
+ const match = ip.match(ipRegex);
319
+ if (!match) {
320
+ return false;
321
+ }
322
+ const [, a, b, c, d] = match.map(Number);
323
+ // Validate IP format
324
+ if (a > 255 || b > 255 || c > 255 || d > 255) {
325
+ return false;
326
+ }
327
+ // Private IP ranges
328
+ return (a === 10 || a === 127 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || (a === 169 && b === 254) // Link-local
329
+ );
330
+ }
331
+ /**
332
+ * Helper function to check if a port is typically used for internal services
333
+ */
334
+ function isInternalPort(port) {
335
+ const internalPorts = [
336
+ 22, // SSH
337
+ 23, // Telnet
338
+ 25, // SMTP
339
+ 53, // DNS
340
+ 135, // RPC
341
+ 139, // NetBIOS
342
+ 445, // SMB
343
+ 993, // IMAPS
344
+ 995, // POP3S
345
+ 1433, // SQL Server
346
+ 1521, // Oracle
347
+ 3306, // MySQL
348
+ 3389, // RDP
349
+ 5432, // PostgreSQL
350
+ 5900, // VNC
351
+ 6379, // Redis
352
+ 8080, // Common internal web
353
+ 8443, // Common internal HTTPS
354
+ 9200, // Elasticsearch
355
+ 27017, // MongoDB
356
+ ];
357
+ return internalPorts.includes(port);
358
+ }
212
359
  /**
213
360
  * Determines whether the given path is a filesystem path.
214
361
  * This includes "file://" URLs.
package/lib/index.ts CHANGED
@@ -28,6 +28,7 @@ import type {
28
28
  ResolverOptions,
29
29
  HTTPResolverOptions,
30
30
  } from "./types/index.js";
31
+ import { isUnsafeUrl } from "./util/url.js";
31
32
 
32
33
  export type RefParserSchema = string | JSONSchema;
33
34
 
@@ -439,4 +440,5 @@ export {
439
440
  normalizeArgs as jsonSchemaParserNormalizeArgs,
440
441
  getJsonSchemaRefParserDefaultOptions,
441
442
  $Refs,
443
+ isUnsafeUrl,
442
444
  };
@@ -36,13 +36,18 @@ export default {
36
36
  */
37
37
  withCredentials: false,
38
38
 
39
+ /**
40
+ * Set this to `false` if you want to allow unsafe URLs (e.g., `127.0.0.1`, localhost, and other internal URLs).
41
+ */
42
+ safeUrlResolver: true,
43
+
39
44
  /**
40
45
  * Determines whether this resolver can read a given file reference.
41
46
  * Resolvers that return true will be tried in order, until one successfully resolves the file.
42
47
  * Resolvers that return false will not be given a chance to resolve the file.
43
48
  */
44
49
  canRead(file: FileInfo) {
45
- return url.isHttp(file.url);
50
+ return url.isHttp(file.url) && (!this.safeUrlResolver || !url.isUnsafeUrl(file.url));
46
51
  },
47
52
 
48
53
  /**
@@ -41,6 +41,11 @@ export interface HTTPResolverOptions<S extends object = JSONSchema> extends Part
41
41
  * Set this to `true` if you're downloading files from a CORS-enabled server that requires authentication
42
42
  */
43
43
  withCredentials?: boolean;
44
+
45
+ /**
46
+ * Set this to `false` if you want to allow unsafe URLs (e.g., `127.0.0.1`, localhost, and other internal URLs).
47
+ */
48
+ safeUrlResolver: true;
44
49
  }
45
50
 
46
51
  /**
package/lib/util/url.ts CHANGED
@@ -167,7 +167,178 @@ export function isHttp(path: string) {
167
167
  return false;
168
168
  }
169
169
  }
170
+ /**
171
+ * Determines whether the given url is an unsafe or internal url.
172
+ *
173
+ * @param path - The URL or path to check
174
+ * @returns true if the URL is unsafe/internal, false otherwise
175
+ */
176
+ export function isUnsafeUrl(path: string): boolean {
177
+ if (!path || typeof path !== "string") {
178
+ return true;
179
+ }
180
+
181
+ // Trim whitespace and convert to lowercase for comparison
182
+ const normalizedPath = path.trim().toLowerCase();
170
183
 
184
+ // Empty or just whitespace
185
+ if (!normalizedPath) {
186
+ return true;
187
+ }
188
+
189
+ // JavaScript protocols
190
+ if (
191
+ normalizedPath.startsWith("javascript:") ||
192
+ normalizedPath.startsWith("vbscript:") ||
193
+ normalizedPath.startsWith("data:")
194
+ ) {
195
+ return true;
196
+ }
197
+
198
+ // File protocol
199
+ if (normalizedPath.startsWith("file:")) {
200
+ return true;
201
+ }
202
+
203
+ // if we're in the browser, we assume that it is safe
204
+ if (typeof window !== "undefined" && window.location && window.location.href) {
205
+ return false;
206
+ }
207
+
208
+ // Local/internal network addresses
209
+ const localPatterns = [
210
+ // Localhost variations
211
+ "localhost",
212
+ "127.0.0.1",
213
+ "::1",
214
+
215
+ // Private IP ranges (RFC 1918)
216
+ "10.",
217
+ "172.16.",
218
+ "172.17.",
219
+ "172.18.",
220
+ "172.19.",
221
+ "172.20.",
222
+ "172.21.",
223
+ "172.22.",
224
+ "172.23.",
225
+ "172.24.",
226
+ "172.25.",
227
+ "172.26.",
228
+ "172.27.",
229
+ "172.28.",
230
+ "172.29.",
231
+ "172.30.",
232
+ "172.31.",
233
+ "192.168.",
234
+
235
+ // Link-local addresses
236
+ "169.254.",
237
+
238
+ // Internal domains
239
+ ".local",
240
+ ".internal",
241
+ ".intranet",
242
+ ".corp",
243
+ ".home",
244
+ ".lan",
245
+ ];
246
+
247
+ try {
248
+ // Try to parse as URL
249
+ const url = new URL(normalizedPath.startsWith("//") ? "http:" + normalizedPath : normalizedPath);
250
+
251
+ const hostname = url.hostname.toLowerCase();
252
+
253
+ // Check against local patterns
254
+ for (const pattern of localPatterns) {
255
+ if (hostname === pattern || hostname.startsWith(pattern) || hostname.endsWith(pattern)) {
256
+ return true;
257
+ }
258
+ }
259
+
260
+ // Check for IP addresses in private ranges
261
+ if (isPrivateIP(hostname)) {
262
+ return true;
263
+ }
264
+
265
+ // Check for non-standard ports that might indicate internal services
266
+ const port = url.port;
267
+ if (port && isInternalPort(parseInt(port))) {
268
+ return true;
269
+ }
270
+ } catch (e) {
271
+ // If URL parsing fails, check if it's a relative path or contains suspicious patterns
272
+
273
+ // Relative paths starting with / are generally safe for same-origin
274
+ if (normalizedPath.startsWith("/") && !normalizedPath.startsWith("//")) {
275
+ return false;
276
+ }
277
+
278
+ // Check for localhost patterns in non-URL strings
279
+ for (const pattern of localPatterns) {
280
+ if (normalizedPath.includes(pattern)) {
281
+ return true;
282
+ }
283
+ }
284
+ }
285
+
286
+ return false;
287
+ }
288
+
289
+ /**
290
+ * Helper function to check if an IP address is in a private range
291
+ */
292
+ function isPrivateIP(ip: string): boolean {
293
+ const ipRegex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
294
+ const match = ip.match(ipRegex);
295
+
296
+ if (!match) {
297
+ return false;
298
+ }
299
+
300
+ const [, a, b, c, d] = match.map(Number);
301
+
302
+ // Validate IP format
303
+ if (a > 255 || b > 255 || c > 255 || d > 255) {
304
+ return false;
305
+ }
306
+
307
+ // Private IP ranges
308
+ return (
309
+ a === 10 || a === 127 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || (a === 169 && b === 254) // Link-local
310
+ );
311
+ }
312
+
313
+ /**
314
+ * Helper function to check if a port is typically used for internal services
315
+ */
316
+ function isInternalPort(port: number): boolean {
317
+ const internalPorts = [
318
+ 22, // SSH
319
+ 23, // Telnet
320
+ 25, // SMTP
321
+ 53, // DNS
322
+ 135, // RPC
323
+ 139, // NetBIOS
324
+ 445, // SMB
325
+ 993, // IMAPS
326
+ 995, // POP3S
327
+ 1433, // SQL Server
328
+ 1521, // Oracle
329
+ 3306, // MySQL
330
+ 3389, // RDP
331
+ 5432, // PostgreSQL
332
+ 5900, // VNC
333
+ 6379, // Redis
334
+ 8080, // Common internal web
335
+ 8443, // Common internal HTTPS
336
+ 9200, // Elasticsearch
337
+ 27017, // MongoDB
338
+ ];
339
+
340
+ return internalPorts.includes(port);
341
+ }
171
342
  /**
172
343
  * Determines whether the given path is a filesystem path.
173
344
  * This includes "file://" URLs.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apidevtools/json-schema-ref-parser",
3
- "version": "13.0.5",
3
+ "version": "14.0.0",
4
4
  "description": "Parse, Resolve, and Dereference JSON Schema $ref pointers",
5
5
  "scripts": {
6
6
  "prepublishOnly": "yarn build",
@@ -67,16 +67,16 @@
67
67
  "cjs"
68
68
  ],
69
69
  "devDependencies": {
70
- "@eslint/compat": "^1.2.9",
71
- "@eslint/js": "^9.28.0",
70
+ "@eslint/compat": "^1.3.0",
71
+ "@eslint/js": "^9.29.0",
72
72
  "@types/eslint": "^9.6.1",
73
73
  "@types/js-yaml": "^4.0.9",
74
74
  "@types/node": "^24",
75
- "@typescript-eslint/eslint-plugin": "^8.34.0",
76
- "@typescript-eslint/parser": "^8.34.0",
75
+ "@typescript-eslint/eslint-plugin": "^8.34.1",
76
+ "@typescript-eslint/parser": "^8.34.1",
77
77
  "@vitest/coverage-v8": "^3.2.3",
78
78
  "cross-env": "^7.0.3",
79
- "eslint": "^9.28.0",
79
+ "eslint": "^9.29.0",
80
80
  "eslint-config-prettier": "^10.1.5",
81
81
  "eslint-config-standard": "^17.1.0",
82
82
  "eslint-plugin-import": "^2.31.0",
@@ -88,7 +88,7 @@
88
88
  "prettier": "^3.5.3",
89
89
  "rimraf": "^6.0.1",
90
90
  "typescript": "^5.8.3",
91
- "typescript-eslint": "^8.34.0",
91
+ "typescript-eslint": "^8.34.1",
92
92
  "vitest": "^3.2.3"
93
93
  },
94
94
  "dependencies": {