@ocap/util 1.25.1 → 1.25.3

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/esm/url.d.ts CHANGED
@@ -1,2 +1,13 @@
1
- export declare function isIPv4(host: string): boolean;
2
- export declare function verifyUrl(url: string): boolean;
1
+ import * as ipaddr from 'ipaddr.js';
2
+ export declare function parseIp(host: string): ipaddr.IPv4 | ipaddr.IPv6 | null;
3
+ export declare function isUnicastAddress(host: string): boolean;
4
+ export declare function resolveHostAddresses(host: string): Promise<string[]>;
5
+ export interface VerifyUrlOptions {
6
+ allowIp?: boolean;
7
+ maxLength?: number;
8
+ protocols?: string[];
9
+ blockedHosts?: string[];
10
+ blockedSuffixes?: string[];
11
+ checkDns?: boolean;
12
+ }
13
+ export declare function verifyUrl(url: string, options?: VerifyUrlOptions): Promise<boolean>;
package/esm/url.js CHANGED
@@ -1,39 +1,101 @@
1
1
  import { URL } from 'url';
2
- export function isIPv4(host) {
3
- const m = host.match(/^(\d{1,3}\.){3}\d{1,3}$/);
4
- if (!m)
5
- return false;
6
- return host.split('.').every((p) => {
7
- if (p.length === 0 || (p.length > 1 && p.startsWith('0')))
8
- return false;
9
- const n = Number(p);
10
- return Number.isInteger(n) && n >= 0 && n <= 255;
11
- });
2
+ import * as ipaddr from 'ipaddr.js';
3
+ import { Resolver } from 'dns/promises';
4
+ const MAX_URL_LENGTH = 256;
5
+ export function parseIp(host) {
6
+ if (!ipaddr.isValid(host))
7
+ return null;
8
+ try {
9
+ return ipaddr.parse(host);
10
+ }
11
+ catch {
12
+ return null;
13
+ }
12
14
  }
13
- export function verifyUrl(url) {
14
- if (!url)
15
- return false;
16
- if (url.length > 2048)
15
+ function stripTrailingDots(host) {
16
+ return host.replace(/\.+$/, '');
17
+ }
18
+ function isBlockedHostname(host, blockedHosts, blockedSuffixes) {
19
+ const normalized = host.toLowerCase();
20
+ if (blockedHosts.includes(normalized))
21
+ return true;
22
+ if (blockedSuffixes.some((suffix) => normalized.endsWith(suffix)))
23
+ return true;
24
+ return false;
25
+ }
26
+ export function isUnicastAddress(host) {
27
+ const parsed = parseIp(host);
28
+ if (!parsed)
17
29
  return false;
30
+ const range = parsed.range();
31
+ if (range === 'ipv4Mapped' && parsed.kind() === 'ipv6') {
32
+ const mapped = parsed.toIPv4Address();
33
+ return mapped.range() === 'unicast';
34
+ }
35
+ return range === 'unicast';
36
+ }
37
+ export async function resolveHostAddresses(host) {
18
38
  try {
19
- const u = new URL(url);
20
- if (!['http:', 'https:'].includes(u.protocol))
21
- return false;
22
- if (!u.hostname)
23
- return false;
24
- // should not block localhost in test environment
25
- if (process.env.NODE_ENV !== 'development' && process.env.NODE_ENV !== 'test' && !process.env.CI) {
26
- const host = u.hostname.toLowerCase();
27
- // Block IP addresses
28
- if (isIPv4(host) || host.includes(':'))
29
- return false; // treat ":" as IPv6
30
- // Block localhost
31
- if (host === 'localhost')
32
- return false;
39
+ const resolver = new Resolver();
40
+ resolver.setServers(['8.8.8.8', '1.1.1.1']);
41
+ const addresses = [];
42
+ // resolve ipv4
43
+ try {
44
+ const ipv4Addresses = await resolver.resolve4(host);
45
+ addresses.push(...ipv4Addresses);
33
46
  }
34
- return true;
47
+ catch (error) {
48
+ console.warn(`IPv4 resolution failed for ${host}:`, error?.message);
49
+ }
50
+ // resolve ipv6
51
+ if (!addresses.length) {
52
+ try {
53
+ const ipv6Addresses = await resolver.resolve6(host);
54
+ addresses.push(...ipv6Addresses);
55
+ }
56
+ catch (error) {
57
+ console.warn(`IPv6 resolution failed for ${host}:`, error?.message);
58
+ }
59
+ }
60
+ return addresses.filter(Boolean);
35
61
  }
36
- catch {
37
- return false;
62
+ catch (error) {
63
+ console.warn(`DNS resolution failed for host: ${host}, error: ${error?.message}`);
64
+ return [];
65
+ }
66
+ }
67
+ export async function verifyUrl(url, options = {}) {
68
+ if (!url)
69
+ throw new Error('URL is required');
70
+ const { maxLength = MAX_URL_LENGTH, protocols = ['https:'], blockedHosts = ['localhost'], blockedSuffixes = [], allowIp = false, checkDns = false, } = options;
71
+ if (url.length > maxLength) {
72
+ throw new Error(`URL exceeds maximum length of ${maxLength} characters: ${url.length}`);
73
+ }
74
+ const { hostname, protocol } = new URL(url);
75
+ if (!protocols.includes(protocol)) {
76
+ throw new Error(`Protocol '${protocol}' is not allowed. Allowed protocols: ${protocols.join(', ')}`);
77
+ }
78
+ const normalizedHost = stripTrailingDots(hostname);
79
+ if (isBlockedHostname(normalizedHost, blockedHosts, blockedSuffixes)) {
80
+ throw new Error(`Hostname '${normalizedHost}' is blocked`);
81
+ }
82
+ const isIP = Boolean(parseIp(normalizedHost));
83
+ if (isIP) {
84
+ if (!allowIp) {
85
+ throw new Error(`IP addresses are not allowed: ${normalizedHost}`);
86
+ }
87
+ if (!isUnicastAddress(normalizedHost)) {
88
+ throw new Error(`IP address is not unicast: ${normalizedHost}`);
89
+ }
90
+ }
91
+ if (checkDns && !isIP) {
92
+ const resolved = await resolveHostAddresses(normalizedHost);
93
+ if (resolved.length === 0) {
94
+ throw new Error(`DNS resolution failed: no addresses found for ${normalizedHost}`);
95
+ }
96
+ if (!resolved.every((address) => isUnicastAddress(address))) {
97
+ throw new Error(`DNS resolved to non-unicast addresses for ${normalizedHost}`);
98
+ }
38
99
  }
100
+ return true;
39
101
  }
package/lib/url.d.ts CHANGED
@@ -1,2 +1,13 @@
1
- export declare function isIPv4(host: string): boolean;
2
- export declare function verifyUrl(url: string): boolean;
1
+ import * as ipaddr from 'ipaddr.js';
2
+ export declare function parseIp(host: string): ipaddr.IPv4 | ipaddr.IPv6 | null;
3
+ export declare function isUnicastAddress(host: string): boolean;
4
+ export declare function resolveHostAddresses(host: string): Promise<string[]>;
5
+ export interface VerifyUrlOptions {
6
+ allowIp?: boolean;
7
+ maxLength?: number;
8
+ protocols?: string[];
9
+ blockedHosts?: string[];
10
+ blockedSuffixes?: string[];
11
+ checkDns?: boolean;
12
+ }
13
+ export declare function verifyUrl(url: string, options?: VerifyUrlOptions): Promise<boolean>;
package/lib/url.js CHANGED
@@ -1,43 +1,130 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
2
25
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isIPv4 = isIPv4;
26
+ exports.parseIp = parseIp;
27
+ exports.isUnicastAddress = isUnicastAddress;
28
+ exports.resolveHostAddresses = resolveHostAddresses;
4
29
  exports.verifyUrl = verifyUrl;
5
30
  const url_1 = require("url");
6
- function isIPv4(host) {
7
- const m = host.match(/^(\d{1,3}\.){3}\d{1,3}$/);
8
- if (!m)
9
- return false;
10
- return host.split('.').every((p) => {
11
- if (p.length === 0 || (p.length > 1 && p.startsWith('0')))
12
- return false;
13
- const n = Number(p);
14
- return Number.isInteger(n) && n >= 0 && n <= 255;
15
- });
31
+ const ipaddr = __importStar(require("ipaddr.js"));
32
+ const promises_1 = require("dns/promises");
33
+ const MAX_URL_LENGTH = 256;
34
+ function parseIp(host) {
35
+ if (!ipaddr.isValid(host))
36
+ return null;
37
+ try {
38
+ return ipaddr.parse(host);
39
+ }
40
+ catch {
41
+ return null;
42
+ }
16
43
  }
17
- function verifyUrl(url) {
18
- if (!url)
19
- return false;
20
- if (url.length > 2048)
44
+ function stripTrailingDots(host) {
45
+ return host.replace(/\.+$/, '');
46
+ }
47
+ function isBlockedHostname(host, blockedHosts, blockedSuffixes) {
48
+ const normalized = host.toLowerCase();
49
+ if (blockedHosts.includes(normalized))
50
+ return true;
51
+ if (blockedSuffixes.some((suffix) => normalized.endsWith(suffix)))
52
+ return true;
53
+ return false;
54
+ }
55
+ function isUnicastAddress(host) {
56
+ const parsed = parseIp(host);
57
+ if (!parsed)
21
58
  return false;
59
+ const range = parsed.range();
60
+ if (range === 'ipv4Mapped' && parsed.kind() === 'ipv6') {
61
+ const mapped = parsed.toIPv4Address();
62
+ return mapped.range() === 'unicast';
63
+ }
64
+ return range === 'unicast';
65
+ }
66
+ async function resolveHostAddresses(host) {
22
67
  try {
23
- const u = new url_1.URL(url);
24
- if (!['http:', 'https:'].includes(u.protocol))
25
- return false;
26
- if (!u.hostname)
27
- return false;
28
- // should not block localhost in test environment
29
- if (process.env.NODE_ENV !== 'development' && process.env.NODE_ENV !== 'test' && !process.env.CI) {
30
- const host = u.hostname.toLowerCase();
31
- // Block IP addresses
32
- if (isIPv4(host) || host.includes(':'))
33
- return false; // treat ":" as IPv6
34
- // Block localhost
35
- if (host === 'localhost')
36
- return false;
68
+ const resolver = new promises_1.Resolver();
69
+ resolver.setServers(['8.8.8.8', '1.1.1.1']);
70
+ const addresses = [];
71
+ // resolve ipv4
72
+ try {
73
+ const ipv4Addresses = await resolver.resolve4(host);
74
+ addresses.push(...ipv4Addresses);
37
75
  }
38
- return true;
76
+ catch (error) {
77
+ console.warn(`IPv4 resolution failed for ${host}:`, error?.message);
78
+ }
79
+ // resolve ipv6
80
+ if (!addresses.length) {
81
+ try {
82
+ const ipv6Addresses = await resolver.resolve6(host);
83
+ addresses.push(...ipv6Addresses);
84
+ }
85
+ catch (error) {
86
+ console.warn(`IPv6 resolution failed for ${host}:`, error?.message);
87
+ }
88
+ }
89
+ return addresses.filter(Boolean);
39
90
  }
40
- catch {
41
- return false;
91
+ catch (error) {
92
+ console.warn(`DNS resolution failed for host: ${host}, error: ${error?.message}`);
93
+ return [];
94
+ }
95
+ }
96
+ async function verifyUrl(url, options = {}) {
97
+ if (!url)
98
+ throw new Error('URL is required');
99
+ const { maxLength = MAX_URL_LENGTH, protocols = ['https:'], blockedHosts = ['localhost'], blockedSuffixes = [], allowIp = false, checkDns = false, } = options;
100
+ if (url.length > maxLength) {
101
+ throw new Error(`URL exceeds maximum length of ${maxLength} characters: ${url.length}`);
102
+ }
103
+ const { hostname, protocol } = new url_1.URL(url);
104
+ if (!protocols.includes(protocol)) {
105
+ throw new Error(`Protocol '${protocol}' is not allowed. Allowed protocols: ${protocols.join(', ')}`);
106
+ }
107
+ const normalizedHost = stripTrailingDots(hostname);
108
+ if (isBlockedHostname(normalizedHost, blockedHosts, blockedSuffixes)) {
109
+ throw new Error(`Hostname '${normalizedHost}' is blocked`);
110
+ }
111
+ const isIP = Boolean(parseIp(normalizedHost));
112
+ if (isIP) {
113
+ if (!allowIp) {
114
+ throw new Error(`IP addresses are not allowed: ${normalizedHost}`);
115
+ }
116
+ if (!isUnicastAddress(normalizedHost)) {
117
+ throw new Error(`IP address is not unicast: ${normalizedHost}`);
118
+ }
119
+ }
120
+ if (checkDns && !isIP) {
121
+ const resolved = await resolveHostAddresses(normalizedHost);
122
+ if (resolved.length === 0) {
123
+ throw new Error(`DNS resolution failed: no addresses found for ${normalizedHost}`);
124
+ }
125
+ if (!resolved.every((address) => isUnicastAddress(address))) {
126
+ throw new Error(`DNS resolved to non-unicast addresses for ${normalizedHost}`);
127
+ }
42
128
  }
129
+ return true;
43
130
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ocap/util",
3
- "version": "1.25.1",
3
+ "version": "1.25.3",
4
4
  "description": "utils shared across multiple forge js libs, works in both node.js and browser",
5
5
  "keywords": [
6
6
  "arcblock",
@@ -15,9 +15,10 @@
15
15
  "base64-url": "^2.3.3",
16
16
  "bn.js": "5.2.2",
17
17
  "bs58": "^5.0.0",
18
+ "ipaddr.js": "^2.1.0",
18
19
  "lodash": "^4.17.21",
19
20
  "utf8": "^3.0.0",
20
- "@ocap/types": "^1.25.1"
21
+ "@ocap/types": "^1.25.3"
21
22
  },
22
23
  "resolutions": {
23
24
  "elliptic": "6.5.3"