@uncefact/untp-utils 0.0.1 → 0.2.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.
- package/LICENSE +202 -674
- package/README.md +23 -1
- package/build/artefacts/detect-version-from-context.d.ts +46 -0
- package/build/artefacts/detect-version-from-context.js +77 -0
- package/build/artefacts/detect-version-from-context.js.map +1 -0
- package/build/artefacts/detect-version-from-context.test.d.ts +1 -0
- package/build/artefacts/detect-version-from-context.test.js +69 -0
- package/build/artefacts/detect-version-from-context.test.js.map +1 -0
- package/build/artefacts/index.d.ts +3 -0
- package/build/artefacts/index.js +4 -0
- package/build/artefacts/index.js.map +1 -0
- package/build/artefacts/urls.d.ts +79 -0
- package/build/artefacts/urls.js +133 -0
- package/build/artefacts/urls.js.map +1 -0
- package/build/artefacts/urls.test.d.ts +1 -0
- package/build/artefacts/urls.test.js +91 -0
- package/build/artefacts/urls.test.js.map +1 -0
- package/build/artefacts/version.d.ts +9 -0
- package/build/artefacts/version.js +17 -0
- package/build/artefacts/version.js.map +1 -0
- package/build/artefacts/version.test.d.ts +1 -0
- package/build/artefacts/version.test.js +16 -0
- package/build/artefacts/version.test.js.map +1 -0
- package/build/cache/in-memory-ttl-cache.d.ts +2 -0
- package/build/cache/in-memory-ttl-cache.js +76 -0
- package/build/cache/in-memory-ttl-cache.js.map +1 -0
- package/build/cache/in-memory-ttl-cache.test.d.ts +1 -0
- package/build/cache/in-memory-ttl-cache.test.js +236 -0
- package/build/cache/in-memory-ttl-cache.test.js.map +1 -0
- package/build/cache/index.d.ts +2 -0
- package/build/cache/index.js +2 -0
- package/build/cache/index.js.map +1 -0
- package/build/cache/ttl-cache.d.ts +22 -0
- package/build/cache/ttl-cache.js +2 -0
- package/build/cache/ttl-cache.js.map +1 -0
- package/build/common/as-non-empty-string.d.ts +7 -0
- package/build/common/as-non-empty-string.js +10 -0
- package/build/common/as-non-empty-string.js.map +1 -0
- package/build/common/as-non-empty-string.test.d.ts +1 -0
- package/build/common/as-non-empty-string.test.js +25 -0
- package/build/common/as-non-empty-string.test.js.map +1 -0
- package/build/common/require-string.d.ts +12 -0
- package/build/common/require-string.js +27 -0
- package/build/common/require-string.js.map +1 -0
- package/build/common/require-string.test.d.ts +1 -0
- package/build/common/require-string.test.js +53 -0
- package/build/common/require-string.test.js.map +1 -0
- package/build/conformity-vocabulary/codes.d.ts +10 -0
- package/build/conformity-vocabulary/codes.js +10 -0
- package/build/conformity-vocabulary/codes.js.map +1 -0
- package/build/conformity-vocabulary/errors.d.ts +44 -0
- package/build/conformity-vocabulary/errors.js +68 -0
- package/build/conformity-vocabulary/errors.js.map +1 -0
- package/build/conformity-vocabulary/index.d.ts +6 -0
- package/build/conformity-vocabulary/index.js +7 -0
- package/build/conformity-vocabulary/index.js.map +1 -0
- package/build/conformity-vocabulary/parse-conformity-catalogue.d.ts +26 -0
- package/build/conformity-vocabulary/parse-conformity-catalogue.js +106 -0
- package/build/conformity-vocabulary/parse-conformity-catalogue.js.map +1 -0
- package/build/conformity-vocabulary/parse-conformity-catalogue.test.d.ts +1 -0
- package/build/conformity-vocabulary/parse-conformity-catalogue.test.js +327 -0
- package/build/conformity-vocabulary/parse-conformity-catalogue.test.js.map +1 -0
- package/build/conformity-vocabulary/parse-conformity-scheme.d.ts +28 -0
- package/build/conformity-vocabulary/parse-conformity-scheme.js +47 -0
- package/build/conformity-vocabulary/parse-conformity-scheme.js.map +1 -0
- package/build/conformity-vocabulary/parse-conformity-scheme.test.d.ts +1 -0
- package/build/conformity-vocabulary/parse-conformity-scheme.test.js +284 -0
- package/build/conformity-vocabulary/parse-conformity-scheme.test.js.map +1 -0
- package/build/conformity-vocabulary/parsers/v0-7-0.parser.d.ts +19 -0
- package/build/conformity-vocabulary/parsers/v0-7-0.parser.js +219 -0
- package/build/conformity-vocabulary/parsers/v0-7-0.parser.js.map +1 -0
- package/build/conformity-vocabulary/types.d.ts +180 -0
- package/build/conformity-vocabulary/types.js +2 -0
- package/build/conformity-vocabulary/types.js.map +1 -0
- package/build/conformity-vocabulary/validate-conformity-claim.d.ts +26 -0
- package/build/conformity-vocabulary/validate-conformity-claim.js +165 -0
- package/build/conformity-vocabulary/validate-conformity-claim.js.map +1 -0
- package/build/conformity-vocabulary/validate-conformity-claim.test.d.ts +1 -0
- package/build/conformity-vocabulary/validate-conformity-claim.test.js +478 -0
- package/build/conformity-vocabulary/validate-conformity-claim.test.js.map +1 -0
- package/build/http-headers/index.d.ts +5 -0
- package/build/http-headers/index.js +6 -0
- package/build/http-headers/index.js.map +1 -0
- package/build/http-headers/is-safe-header-value.d.ts +10 -0
- package/build/http-headers/is-safe-header-value.js +15 -0
- package/build/http-headers/is-safe-header-value.js.map +1 -0
- package/build/http-headers/is-safe-header-value.test.d.ts +1 -0
- package/build/http-headers/is-safe-header-value.test.js +37 -0
- package/build/http-headers/is-safe-header-value.test.js.map +1 -0
- package/build/http-headers/parse-entity-tag.d.ts +9 -0
- package/build/http-headers/parse-entity-tag.js +18 -0
- package/build/http-headers/parse-entity-tag.js.map +1 -0
- package/build/http-headers/parse-entity-tag.test.d.ts +1 -0
- package/build/http-headers/parse-entity-tag.test.js +41 -0
- package/build/http-headers/parse-entity-tag.test.js.map +1 -0
- package/build/http-headers/parse-imf-date.d.ts +13 -0
- package/build/http-headers/parse-imf-date.js +23 -0
- package/build/http-headers/parse-imf-date.js.map +1 -0
- package/build/http-headers/parse-imf-date.test.d.ts +1 -0
- package/build/http-headers/parse-imf-date.test.js +31 -0
- package/build/http-headers/parse-imf-date.test.js.map +1 -0
- package/build/http-headers/parse-media-type.d.ts +8 -0
- package/build/http-headers/parse-media-type.js +21 -0
- package/build/http-headers/parse-media-type.js.map +1 -0
- package/build/http-headers/parse-media-type.test.d.ts +1 -0
- package/build/http-headers/parse-media-type.test.js +39 -0
- package/build/http-headers/parse-media-type.test.js.map +1 -0
- package/build/http-headers/user-agent.d.ts +35 -0
- package/build/http-headers/user-agent.js +45 -0
- package/build/http-headers/user-agent.js.map +1 -0
- package/build/http-headers/user-agent.test.d.ts +1 -0
- package/build/http-headers/user-agent.test.js +27 -0
- package/build/http-headers/user-agent.test.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.js +1 -1
- package/build/index.js.map +1 -1
- package/build/loaders/errors.d.ts +21 -0
- package/build/loaders/errors.js +45 -0
- package/build/loaders/errors.js.map +1 -0
- package/build/loaders/index.d.ts +3 -0
- package/build/loaders/index.js +4 -0
- package/build/loaders/index.js.map +1 -0
- package/build/loaders/jsonld-document-loader.d.ts +46 -0
- package/build/loaders/jsonld-document-loader.js +34 -0
- package/build/loaders/jsonld-document-loader.js.map +1 -0
- package/build/loaders/jsonld-document-loader.test.d.ts +1 -0
- package/build/loaders/jsonld-document-loader.test.js +71 -0
- package/build/loaders/jsonld-document-loader.test.js.map +1 -0
- package/build/loaders/schema-loader.d.ts +21 -0
- package/build/loaders/schema-loader.js +67 -0
- package/build/loaders/schema-loader.js.map +1 -0
- package/build/loaders/schema-loader.ssrf.test.d.ts +1 -0
- package/build/loaders/schema-loader.ssrf.test.js +55 -0
- package/build/loaders/schema-loader.ssrf.test.js.map +1 -0
- package/build/loaders/schema-loader.test.d.ts +1 -0
- package/build/loaders/schema-loader.test.js +162 -0
- package/build/loaders/schema-loader.test.js.map +1 -0
- package/build/multibase-digest/multibase-digest.d.ts +23 -0
- package/build/multibase-digest/multibase-digest.js +40 -0
- package/build/multibase-digest/multibase-digest.js.map +1 -1
- package/build/multibase-digest/multibase-digest.test.js +46 -0
- package/build/multibase-digest/multibase-digest.test.js.map +1 -1
- package/build/node/errors.d.ts +41 -0
- package/build/node/errors.js +80 -0
- package/build/node/errors.js.map +1 -0
- package/build/node/index.d.ts +3 -0
- package/build/node/index.js +4 -0
- package/build/node/index.js.map +1 -0
- package/build/node/is-private-ip.d.ts +41 -0
- package/build/node/is-private-ip.js +138 -0
- package/build/node/is-private-ip.js.map +1 -0
- package/build/node/is-private-ip.test.d.ts +1 -0
- package/build/node/is-private-ip.test.js +96 -0
- package/build/node/is-private-ip.test.js.map +1 -0
- package/build/node/validate-public-url.d.ts +49 -0
- package/build/node/validate-public-url.js +94 -0
- package/build/node/validate-public-url.js.map +1 -0
- package/build/node/validate-public-url.test.d.ts +1 -0
- package/build/node/validate-public-url.test.js +177 -0
- package/build/node/validate-public-url.test.js.map +1 -0
- package/build/resolvers/errors.d.ts +43 -0
- package/build/resolvers/errors.js +99 -0
- package/build/resolvers/errors.js.map +1 -0
- package/build/resolvers/index.d.ts +5 -0
- package/build/resolvers/index.js +6 -0
- package/build/resolvers/index.js.map +1 -0
- package/build/resolvers/resolve-document-if-changed.d.ts +48 -0
- package/build/resolvers/resolve-document-if-changed.js +42 -0
- package/build/resolvers/resolve-document-if-changed.js.map +1 -0
- package/build/resolvers/resolve-document-if-changed.test.d.ts +1 -0
- package/build/resolvers/resolve-document-if-changed.test.js +109 -0
- package/build/resolvers/resolve-document-if-changed.test.js.map +1 -0
- package/build/resolvers/resolve-document.d.ts +86 -0
- package/build/resolvers/resolve-document.js +239 -0
- package/build/resolvers/resolve-document.js.map +1 -0
- package/build/resolvers/resolve-document.test.d.ts +1 -0
- package/build/resolvers/resolve-document.test.js +299 -0
- package/build/resolvers/resolve-document.test.js.map +1 -0
- package/build/resolvers/resolve-json-document.d.ts +40 -0
- package/build/resolvers/resolve-json-document.js +42 -0
- package/build/resolvers/resolve-json-document.js.map +1 -0
- package/build/resolvers/resolve-json-document.test.d.ts +1 -0
- package/build/resolvers/resolve-json-document.test.js +61 -0
- package/build/resolvers/resolve-json-document.test.js.map +1 -0
- package/build/structured-error.d.ts +49 -0
- package/build/structured-error.js +17 -0
- package/build/structured-error.js.map +1 -0
- package/build/structured-error.test.d.ts +1 -0
- package/build/structured-error.test.js +112 -0
- package/build/structured-error.test.js.map +1 -0
- package/build/validation/describe-jsonld-failure.d.ts +50 -0
- package/build/validation/describe-jsonld-failure.js +104 -0
- package/build/validation/describe-jsonld-failure.js.map +1 -0
- package/build/validation/describe-jsonld-failure.test.d.ts +1 -0
- package/build/validation/describe-jsonld-failure.test.js +141 -0
- package/build/validation/describe-jsonld-failure.test.js.map +1 -0
- package/build/validation/errors.d.ts +40 -0
- package/build/validation/errors.js +75 -0
- package/build/validation/errors.js.map +1 -0
- package/build/validation/index.d.ts +4 -0
- package/build/validation/index.js +5 -0
- package/build/validation/index.js.map +1 -0
- package/build/validation/validate-against-schemas.d.ts +22 -0
- package/build/validation/validate-against-schemas.js +65 -0
- package/build/validation/validate-against-schemas.js.map +1 -0
- package/build/validation/validate-against-schemas.test.d.ts +1 -0
- package/build/validation/validate-against-schemas.test.js +169 -0
- package/build/validation/validate-against-schemas.test.js.map +1 -0
- package/build/validation/validate-jsonld.context-cache.test.d.ts +1 -0
- package/build/validation/validate-jsonld.context-cache.test.js +32 -0
- package/build/validation/validate-jsonld.context-cache.test.js.map +1 -0
- package/build/validation/validate-jsonld.d.ts +33 -0
- package/build/validation/validate-jsonld.js +99 -0
- package/build/validation/validate-jsonld.js.map +1 -0
- package/build/validation/validate-jsonld.ssrf.test.d.ts +1 -0
- package/build/validation/validate-jsonld.ssrf.test.js +108 -0
- package/build/validation/validate-jsonld.ssrf.test.js.map +1 -0
- package/build/validation/validate-jsonld.test.d.ts +1 -0
- package/build/validation/validate-jsonld.test.js +200 -0
- package/build/validation/validate-jsonld.test.js.map +1 -0
- package/package.json +77 -11
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { StructuredError } from '../structured-error.js';
|
|
2
|
+
/**
|
|
3
|
+
* Base for every diagnostic from `@uncefact/untp-utils/node`. Catch to
|
|
4
|
+
* handle any URL-validation failure generically; catch a concrete subclass
|
|
5
|
+
* for specific handling.
|
|
6
|
+
*/
|
|
7
|
+
export class UrlValidationError extends StructuredError {
|
|
8
|
+
}
|
|
9
|
+
/** The string passed could not be parsed as a URL by `new URL(...)`. */
|
|
10
|
+
export class InvalidUrlError extends UrlValidationError {
|
|
11
|
+
constructor(received, cause) {
|
|
12
|
+
super({ code: 'url.invalid', message: 'URL could not be parsed.', received, cause });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** The URL's scheme is not in the allowed list (default `http`, `https`). */
|
|
16
|
+
export class UnsupportedSchemeError extends UrlValidationError {
|
|
17
|
+
constructor(scheme, allowedSchemes) {
|
|
18
|
+
super({
|
|
19
|
+
code: 'url.unsupported-scheme',
|
|
20
|
+
message: `URL scheme ${scheme} is not in the allowed list.`,
|
|
21
|
+
received: scheme,
|
|
22
|
+
expected: [...allowedSchemes],
|
|
23
|
+
remediation: `Use one of: ${allowedSchemes.join(', ')}.`,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The hostname is a known private/local name (`localhost`, `*.localhost`,
|
|
29
|
+
* `*.local`, `*.internal`, etc.), an IP literal in a non-public range, or
|
|
30
|
+
* empty.
|
|
31
|
+
*/
|
|
32
|
+
export class PrivateHostnameError extends UrlValidationError {
|
|
33
|
+
constructor(hostname) {
|
|
34
|
+
super({
|
|
35
|
+
code: 'url.private-hostname',
|
|
36
|
+
message: `Hostname ${hostname || '(empty)'} names a private or local resource.`,
|
|
37
|
+
received: hostname,
|
|
38
|
+
remediation: 'Use a publicly-routable hostname.',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** DNS resolution rejected (`ENOTFOUND`, `EAI_AGAIN`, etc.). */
|
|
43
|
+
export class ResolutionFailedError extends UrlValidationError {
|
|
44
|
+
constructor(hostname, cause) {
|
|
45
|
+
super({
|
|
46
|
+
code: 'url.resolution-failed',
|
|
47
|
+
message: `DNS resolution failed for ${hostname}.`,
|
|
48
|
+
received: cause instanceof Error ? cause.message : String(cause),
|
|
49
|
+
cause,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** DNS resolution succeeded but returned no addresses. */
|
|
54
|
+
export class ResolutionEmptyError extends UrlValidationError {
|
|
55
|
+
constructor(hostname) {
|
|
56
|
+
super({
|
|
57
|
+
code: 'url.resolution-empty',
|
|
58
|
+
message: `DNS resolver returned no addresses for ${hostname}.`,
|
|
59
|
+
received: hostname,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* At least one resolved IP is in a private / loopback / link-local /
|
|
65
|
+
* cloud-metadata range. The full list is exposed on
|
|
66
|
+
* {@link resolvedAddresses} for triage.
|
|
67
|
+
*/
|
|
68
|
+
export class PrivateAddressError extends UrlValidationError {
|
|
69
|
+
resolvedAddresses;
|
|
70
|
+
constructor(hostname, resolvedAddresses) {
|
|
71
|
+
super({
|
|
72
|
+
code: 'url.private-address',
|
|
73
|
+
message: `Hostname ${hostname} resolved to a private address.`,
|
|
74
|
+
received: resolvedAddresses,
|
|
75
|
+
remediation: 'Verify the hostname does not resolve to private or reserved network space.',
|
|
76
|
+
});
|
|
77
|
+
this.resolvedAddresses = resolvedAddresses;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/node/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD;;;;GAIG;AACH,MAAM,OAAO,kBAAmB,SAAQ,eAAe;CAAG;AAE1D,wEAAwE;AACxE,MAAM,OAAO,eAAgB,SAAQ,kBAAkB;IACrD,YAAY,QAAgB,EAAE,KAAc;QAC1C,KAAK,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,0BAA0B,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;IACvF,CAAC;CACF;AAED,6EAA6E;AAC7E,MAAM,OAAO,sBAAuB,SAAQ,kBAAkB;IAC5D,YAAY,MAAc,EAAE,cAAiC;QAC3D,KAAK,CAAC;YACJ,IAAI,EAAE,wBAAwB;YAC9B,OAAO,EAAE,cAAc,MAAM,8BAA8B;YAC3D,QAAQ,EAAE,MAAM;YAChB,QAAQ,EAAE,CAAC,GAAG,cAAc,CAAC;YAC7B,WAAW,EAAE,eAAe,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;SACzD,CAAC,CAAC;IACL,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,OAAO,oBAAqB,SAAQ,kBAAkB;IAC1D,YAAY,QAAgB;QAC1B,KAAK,CAAC;YACJ,IAAI,EAAE,sBAAsB;YAC5B,OAAO,EAAE,YAAY,QAAQ,IAAI,SAAS,qCAAqC;YAC/E,QAAQ,EAAE,QAAQ;YAClB,WAAW,EAAE,mCAAmC;SACjD,CAAC,CAAC;IACL,CAAC;CACF;AAED,gEAAgE;AAChE,MAAM,OAAO,qBAAsB,SAAQ,kBAAkB;IAC3D,YAAY,QAAgB,EAAE,KAAc;QAC1C,KAAK,CAAC;YACJ,IAAI,EAAE,uBAAuB;YAC7B,OAAO,EAAE,6BAA6B,QAAQ,GAAG;YACjD,QAAQ,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;YAChE,KAAK;SACN,CAAC,CAAC;IACL,CAAC;CACF;AAED,0DAA0D;AAC1D,MAAM,OAAO,oBAAqB,SAAQ,kBAAkB;IAC1D,YAAY,QAAgB;QAC1B,KAAK,CAAC;YACJ,IAAI,EAAE,sBAAsB;YAC5B,OAAO,EAAE,0CAA0C,QAAQ,GAAG;YAC9D,QAAQ,EAAE,QAAQ;SACnB,CAAC,CAAC;IACL,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,OAAO,mBAAoB,SAAQ,kBAAkB;IAChD,iBAAiB,CAAoB;IAC9C,YAAY,QAAgB,EAAE,iBAAoC;QAChE,KAAK,CAAC;YACJ,IAAI,EAAE,qBAAqB;YAC3B,OAAO,EAAE,YAAY,QAAQ,iCAAiC;YAC9D,QAAQ,EAAE,iBAAiB;YAC3B,WAAW,EAAE,4EAA4E;SAC1F,CAAC,CAAC;QACH,IAAI,CAAC,iBAAiB,GAAG,iBAAiB,CAAC;IAC7C,CAAC;CACF"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { UrlValidationError, InvalidUrlError, UnsupportedSchemeError, PrivateHostnameError, ResolutionFailedError, ResolutionEmptyError, PrivateAddressError, } from './errors.js';
|
|
2
|
+
export { isPrivateHostname, isPrivateIpv4, isPrivateIpv6 } from './is-private-ip.js';
|
|
3
|
+
export { validatePublicUrl, type ValidatePublicUrlOptions, type ResolvedAddress } from './validate-public-url.js';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { UrlValidationError, InvalidUrlError, UnsupportedSchemeError, PrivateHostnameError, ResolutionFailedError, ResolutionEmptyError, PrivateAddressError, } from './errors.js';
|
|
2
|
+
export { isPrivateHostname, isPrivateIpv4, isPrivateIpv6 } from './is-private-ip.js';
|
|
3
|
+
export { validatePublicUrl } from './validate-public-url.js';
|
|
4
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/node/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,sBAAsB,EACtB,oBAAoB,EACpB,qBAAqB,EACrB,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACrF,OAAO,EAAE,iBAAiB,EAAuD,MAAM,0BAA0B,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns `true` if `address` is an IPv4 address string that does not fall in
|
|
3
|
+
* the public unicast range, or that matches a known cloud-metadata IP. Use
|
|
4
|
+
* this to gate outbound connections to tenant-supplied addresses.
|
|
5
|
+
*
|
|
6
|
+
* Fails closed: if `ipaddr.js` cannot parse a value that passed
|
|
7
|
+
* `node:net`'s `isIPv4` (parser/grammar skew across versions), the function
|
|
8
|
+
* returns `true` rather than letting the address through.
|
|
9
|
+
*
|
|
10
|
+
* Returns `false` for non-IPv4 input (including unparseable strings).
|
|
11
|
+
* Callers handle parsing themselves and pass only IP literals. Never throws.
|
|
12
|
+
*
|
|
13
|
+
* @see https://datatracker.ietf.org/doc/html/rfc1918 Address Allocation for Private Internets
|
|
14
|
+
* @see https://datatracker.ietf.org/doc/html/rfc6598 IANA-Reserved IPv4 Prefix for Shared Address Space (CGNAT)
|
|
15
|
+
* @see https://datatracker.ietf.org/doc/html/rfc3927 Dynamic Configuration of IPv4 Link-Local Addresses
|
|
16
|
+
* @see https://datatracker.ietf.org/doc/html/rfc5735 Special Use IPv4 Addresses
|
|
17
|
+
*/
|
|
18
|
+
export declare function isPrivateIpv4(address: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Returns `true` if `address` is an IPv6 address string that does not fall in
|
|
21
|
+
* the public unicast range. IPv4-mapped addresses (`::ffff:a.b.c.d`) are
|
|
22
|
+
* additionally re-checked against {@link isPrivateIpv4} so an IPv4 private
|
|
23
|
+
* address tunnelled as IPv6 cannot bypass the predicate.
|
|
24
|
+
*
|
|
25
|
+
* Fails closed on parser drift, mirroring {@link isPrivateIpv4}.
|
|
26
|
+
*
|
|
27
|
+
* Returns `false` for non-IPv6 input. Never throws.
|
|
28
|
+
*
|
|
29
|
+
* @see https://datatracker.ietf.org/doc/html/rfc4291 IP Version 6 Addressing Architecture
|
|
30
|
+
* @see https://datatracker.ietf.org/doc/html/rfc4193 Unique Local IPv6 Unicast Addresses
|
|
31
|
+
*/
|
|
32
|
+
export declare function isPrivateIpv6(address: string): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Returns `true` if `host` always names a local resource (`localhost`,
|
|
35
|
+
* anything under `.localhost`, `.local`, `.internal`, etc.) or an IP literal
|
|
36
|
+
* in a non-public range.
|
|
37
|
+
*
|
|
38
|
+
* The empty string is treated as private (a URL with no hostname must never
|
|
39
|
+
* be allowed out).
|
|
40
|
+
*/
|
|
41
|
+
export declare function isPrivateHostname(host: string): boolean;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { isIPv4, isIPv6 } from 'node:net';
|
|
2
|
+
import ipaddr from 'ipaddr.js';
|
|
3
|
+
/**
|
|
4
|
+
* The single `ipaddr.js` range category that represents a publicly routable
|
|
5
|
+
* unicast address. Anything else (loopback, link-local, private, multicast,
|
|
6
|
+
* unique-local, reserved, 6to4, teredo, etc.) is rejected. An allowlist is
|
|
7
|
+
* safer than enumerating disallowed buckets: new range names introduced by
|
|
8
|
+
* future `ipaddr.js` versions default to "block" rather than "allow".
|
|
9
|
+
*/
|
|
10
|
+
const PUBLIC_RANGE = 'unicast';
|
|
11
|
+
/**
|
|
12
|
+
* Cloud-metadata IPv4 addresses listed explicitly for belt-and-braces
|
|
13
|
+
* coverage. Both addresses are already classified as non-`unicast` by
|
|
14
|
+
* `ipaddr.js` (the AWS / GCP / Azure address via link-local
|
|
15
|
+
* `169.254.0.0/16`, the Alibaba address via CGNAT `100.64.0.0/10`), but the
|
|
16
|
+
* explicit listing means a future change to `ipaddr.js`'s range taxonomy
|
|
17
|
+
* cannot accidentally promote them to `unicast`.
|
|
18
|
+
*
|
|
19
|
+
* @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html
|
|
20
|
+
* @see https://help.aliyun.com/document_detail/49122.html
|
|
21
|
+
*/
|
|
22
|
+
const ADDITIONAL_BLOCKED_IPV4 = new Set(['169.254.169.254', '100.100.100.200']);
|
|
23
|
+
/**
|
|
24
|
+
* Hostname suffixes that name resources reachable only on a local network and
|
|
25
|
+
* must not be allowed to leave the host. Matched case-insensitively against
|
|
26
|
+
* the full hostname (e.g. `foo.localhost`, `printer.local`, `db.internal`).
|
|
27
|
+
*
|
|
28
|
+
* `.localhost` is reserved by RFC 6761; `.local` is reserved by RFC 6762
|
|
29
|
+
* (Multicast DNS); `.internal` is on the IANA / ICANN reserved list for
|
|
30
|
+
* private use (per RFC 9499 and ICANN board resolution, 2024); the
|
|
31
|
+
* remainder (`.intranet`, `.lan`, `.home`, `.corp`, `.private`) are
|
|
32
|
+
* widely-used private-network conventions that are blocked from ICANN
|
|
33
|
+
* delegation. Treating them as private is a conservative default for an
|
|
34
|
+
* SSRF guard.
|
|
35
|
+
*
|
|
36
|
+
* @see https://datatracker.ietf.org/doc/html/rfc6761 Special-Use Domain Names
|
|
37
|
+
* @see https://datatracker.ietf.org/doc/html/rfc6762 Multicast DNS
|
|
38
|
+
* @see https://datatracker.ietf.org/doc/html/rfc9499 DNS Terminology
|
|
39
|
+
*/
|
|
40
|
+
const PRIVATE_HOSTNAME_SUFFIXES = [
|
|
41
|
+
'.localhost',
|
|
42
|
+
'.local',
|
|
43
|
+
'.internal',
|
|
44
|
+
'.intranet',
|
|
45
|
+
'.lan',
|
|
46
|
+
'.home',
|
|
47
|
+
'.corp',
|
|
48
|
+
'.private',
|
|
49
|
+
];
|
|
50
|
+
/**
|
|
51
|
+
* Returns `true` if `address` is an IPv4 address string that does not fall in
|
|
52
|
+
* the public unicast range, or that matches a known cloud-metadata IP. Use
|
|
53
|
+
* this to gate outbound connections to tenant-supplied addresses.
|
|
54
|
+
*
|
|
55
|
+
* Fails closed: if `ipaddr.js` cannot parse a value that passed
|
|
56
|
+
* `node:net`'s `isIPv4` (parser/grammar skew across versions), the function
|
|
57
|
+
* returns `true` rather than letting the address through.
|
|
58
|
+
*
|
|
59
|
+
* Returns `false` for non-IPv4 input (including unparseable strings).
|
|
60
|
+
* Callers handle parsing themselves and pass only IP literals. Never throws.
|
|
61
|
+
*
|
|
62
|
+
* @see https://datatracker.ietf.org/doc/html/rfc1918 Address Allocation for Private Internets
|
|
63
|
+
* @see https://datatracker.ietf.org/doc/html/rfc6598 IANA-Reserved IPv4 Prefix for Shared Address Space (CGNAT)
|
|
64
|
+
* @see https://datatracker.ietf.org/doc/html/rfc3927 Dynamic Configuration of IPv4 Link-Local Addresses
|
|
65
|
+
* @see https://datatracker.ietf.org/doc/html/rfc5735 Special Use IPv4 Addresses
|
|
66
|
+
*/
|
|
67
|
+
export function isPrivateIpv4(address) {
|
|
68
|
+
if (!isIPv4(address))
|
|
69
|
+
return false;
|
|
70
|
+
if (ADDITIONAL_BLOCKED_IPV4.has(address))
|
|
71
|
+
return true;
|
|
72
|
+
try {
|
|
73
|
+
return ipaddr.parse(address).range() !== PUBLIC_RANGE;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return true; // Fail closed: cannot prove the address is public, so treat as private.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Returns `true` if `address` is an IPv6 address string that does not fall in
|
|
81
|
+
* the public unicast range. IPv4-mapped addresses (`::ffff:a.b.c.d`) are
|
|
82
|
+
* additionally re-checked against {@link isPrivateIpv4} so an IPv4 private
|
|
83
|
+
* address tunnelled as IPv6 cannot bypass the predicate.
|
|
84
|
+
*
|
|
85
|
+
* Fails closed on parser drift, mirroring {@link isPrivateIpv4}.
|
|
86
|
+
*
|
|
87
|
+
* Returns `false` for non-IPv6 input. Never throws.
|
|
88
|
+
*
|
|
89
|
+
* @see https://datatracker.ietf.org/doc/html/rfc4291 IP Version 6 Addressing Architecture
|
|
90
|
+
* @see https://datatracker.ietf.org/doc/html/rfc4193 Unique Local IPv6 Unicast Addresses
|
|
91
|
+
*/
|
|
92
|
+
export function isPrivateIpv6(address) {
|
|
93
|
+
if (!isIPv6(address))
|
|
94
|
+
return false;
|
|
95
|
+
try {
|
|
96
|
+
const parsed = ipaddr.parse(address);
|
|
97
|
+
if (parsed.kind() !== 'ipv6')
|
|
98
|
+
return true; // Defensive: parser/version skew. Fail closed.
|
|
99
|
+
const v6 = parsed;
|
|
100
|
+
if (v6.isIPv4MappedAddress()) {
|
|
101
|
+
return isPrivateIpv4(v6.toIPv4Address().toString());
|
|
102
|
+
}
|
|
103
|
+
return v6.range() !== PUBLIC_RANGE;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return true; // Fail closed: cannot prove the address is public, so treat as private.
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Returns `true` if `host` always names a local resource (`localhost`,
|
|
111
|
+
* anything under `.localhost`, `.local`, `.internal`, etc.) or an IP literal
|
|
112
|
+
* in a non-public range.
|
|
113
|
+
*
|
|
114
|
+
* The empty string is treated as private (a URL with no hostname must never
|
|
115
|
+
* be allowed out).
|
|
116
|
+
*/
|
|
117
|
+
export function isPrivateHostname(host) {
|
|
118
|
+
if (!host)
|
|
119
|
+
return true;
|
|
120
|
+
// Strip leading/trailing brackets (URL.hostname wraps IPv6 literals like
|
|
121
|
+
// `[::1]`) and any trailing dot before further checks; without this, a
|
|
122
|
+
// direct caller passing `URL.hostname` for an IPv6 URL would silently
|
|
123
|
+
// miss the private-range match.
|
|
124
|
+
const lower = host
|
|
125
|
+
.toLowerCase()
|
|
126
|
+
.replace(/^\[|\]$/g, '')
|
|
127
|
+
.replace(/\.$/, '');
|
|
128
|
+
if (lower === 'localhost')
|
|
129
|
+
return true;
|
|
130
|
+
if (PRIVATE_HOSTNAME_SUFFIXES.some((suffix) => lower.endsWith(suffix)))
|
|
131
|
+
return true;
|
|
132
|
+
if (isIPv4(lower))
|
|
133
|
+
return isPrivateIpv4(lower);
|
|
134
|
+
if (isIPv6(lower))
|
|
135
|
+
return isPrivateIpv6(lower);
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=is-private-ip.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"is-private-ip.js","sourceRoot":"","sources":["../../src/node/is-private-ip.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,MAAM,MAAM,WAAW,CAAC;AAE/B;;;;;;GAMG;AACH,MAAM,YAAY,GAAG,SAAS,CAAC;AAE/B;;;;;;;;;;GAUG;AACH,MAAM,uBAAuB,GAAG,IAAI,GAAG,CAAC,CAAC,iBAAiB,EAAE,iBAAiB,CAAC,CAAC,CAAC;AAEhF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,yBAAyB,GAAG;IAChC,YAAY;IACZ,QAAQ;IACR,WAAW;IACX,WAAW;IACX,MAAM;IACN,OAAO;IACP,OAAO;IACP,UAAU;CACX,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,aAAa,CAAC,OAAe;IAC3C,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC;QAAE,OAAO,KAAK,CAAC;IACnC,IAAI,uBAAuB,CAAC,GAAG,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC;IACtD,IAAI,CAAC;QACH,OAAO,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,KAAK,YAAY,CAAC;IACxD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC,CAAC,wEAAwE;IACvF,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,aAAa,CAAC,OAAe;IAC3C,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC;QAAE,OAAO,KAAK,CAAC;IACnC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACrC,IAAI,MAAM,CAAC,IAAI,EAAE,KAAK,MAAM;YAAE,OAAO,IAAI,CAAC,CAAC,+CAA+C;QAC1F,MAAM,EAAE,GAAG,MAAqB,CAAC;QACjC,IAAI,EAAE,CAAC,mBAAmB,EAAE,EAAE,CAAC;YAC7B,OAAO,aAAa,CAAC,EAAE,CAAC,aAAa,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAC;QACtD,CAAC;QACD,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,YAAY,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC,CAAC,wEAAwE;IACvF,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,yEAAyE;IACzE,uEAAuE;IACvE,sEAAsE;IACtE,gCAAgC;IAChC,MAAM,KAAK,GAAG,IAAI;SACf,WAAW,EAAE;SACb,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;SACvB,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACtB,IAAI,KAAK,KAAK,WAAW;QAAE,OAAO,IAAI,CAAC;IACvC,IAAI,yBAAyB,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACpF,IAAI,MAAM,CAAC,KAAK,CAAC;QAAE,OAAO,aAAa,CAAC,KAAK,CAAC,CAAC;IAC/C,IAAI,MAAM,CAAC,KAAK,CAAC;QAAE,OAAO,aAAa,CAAC,KAAK,CAAC,CAAC;IAC/C,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { isPrivateHostname, isPrivateIpv4, isPrivateIpv6 } from './is-private-ip.js';
|
|
2
|
+
describe('isPrivateIpv4', () => {
|
|
3
|
+
it.each([['1.1.1.1'], ['8.8.8.8'], ['151.101.0.1']])('returns false for the unmistakeably public address %s', (addr) => {
|
|
4
|
+
expect(isPrivateIpv4(addr)).toBe(false);
|
|
5
|
+
});
|
|
6
|
+
it.each([
|
|
7
|
+
['0.0.0.0'], // unspecified
|
|
8
|
+
['10.0.0.1'], // RFC 1918
|
|
9
|
+
['10.255.255.255'],
|
|
10
|
+
['100.64.0.1'], // CGNAT
|
|
11
|
+
['127.0.0.1'], // loopback
|
|
12
|
+
['127.255.255.255'],
|
|
13
|
+
['169.254.0.1'], // link-local
|
|
14
|
+
['169.254.169.254'], // cloud metadata (AWS/GCP/Azure)
|
|
15
|
+
['172.16.0.1'], // RFC 1918
|
|
16
|
+
['172.31.255.255'],
|
|
17
|
+
['192.168.0.1'], // RFC 1918
|
|
18
|
+
['192.168.255.255'],
|
|
19
|
+
['224.0.0.1'], // multicast
|
|
20
|
+
['255.255.255.255'], // broadcast
|
|
21
|
+
['198.18.0.1'], // benchmarking
|
|
22
|
+
['203.0.113.1'], // TEST-NET-3 (reserved)
|
|
23
|
+
['100.100.100.200'], // Alibaba Cloud metadata (publicly routable, but explicitly blocked)
|
|
24
|
+
])('returns true for the private/reserved/metadata address %s', (addr) => {
|
|
25
|
+
expect(isPrivateIpv4(addr)).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
it('returns false for non-IPv4 input', () => {
|
|
28
|
+
expect(isPrivateIpv4('not an ip')).toBe(false);
|
|
29
|
+
expect(isPrivateIpv4('::1')).toBe(false);
|
|
30
|
+
expect(isPrivateIpv4('')).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe('isPrivateIpv6', () => {
|
|
34
|
+
it.each([
|
|
35
|
+
['2606:4700:4700::1111'], // Cloudflare public DNS
|
|
36
|
+
['2001:4860:4860::8888'], // Google public DNS
|
|
37
|
+
])('returns false for the unmistakeably public address %s', (addr) => {
|
|
38
|
+
expect(isPrivateIpv6(addr)).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
it.each([
|
|
41
|
+
['::'], // unspecified
|
|
42
|
+
['::1'], // loopback
|
|
43
|
+
['fe80::1'], // link-local
|
|
44
|
+
['fc00::1'], // unique local
|
|
45
|
+
['fd00::1'], // unique local
|
|
46
|
+
['ff02::1'], // multicast
|
|
47
|
+
['2001:db8::1'], // documentation / reserved
|
|
48
|
+
['2002::1'], // 6to4
|
|
49
|
+
])('returns true for the non-public address %s', (addr) => {
|
|
50
|
+
expect(isPrivateIpv6(addr)).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
it('treats IPv4-mapped IPv6 addresses by their embedded IPv4 status', () => {
|
|
53
|
+
expect(isPrivateIpv6('::ffff:127.0.0.1')).toBe(true);
|
|
54
|
+
expect(isPrivateIpv6('::ffff:169.254.169.254')).toBe(true);
|
|
55
|
+
expect(isPrivateIpv6('::ffff:1.1.1.1')).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
it('returns false for non-IPv6 input', () => {
|
|
58
|
+
expect(isPrivateIpv6('not an ip')).toBe(false);
|
|
59
|
+
expect(isPrivateIpv6('10.0.0.1')).toBe(false);
|
|
60
|
+
expect(isPrivateIpv6('')).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe('isPrivateHostname', () => {
|
|
64
|
+
it.each([
|
|
65
|
+
[''],
|
|
66
|
+
['localhost'],
|
|
67
|
+
['LOCALHOST'],
|
|
68
|
+
['foo.localhost'],
|
|
69
|
+
['bar.local'],
|
|
70
|
+
['printer.local.'], // trailing dot is stripped
|
|
71
|
+
['db.internal'],
|
|
72
|
+
['svc.intranet'],
|
|
73
|
+
['file-server.lan'],
|
|
74
|
+
['router.home'],
|
|
75
|
+
['app.corp'],
|
|
76
|
+
['vault.private'],
|
|
77
|
+
])('returns true for the private hostname %s', (host) => {
|
|
78
|
+
expect(isPrivateHostname(host)).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
it.each([['example.com'], ['api.cloudflare.com'], ['google.com.']])('returns false for the public hostname %s', (host) => {
|
|
81
|
+
expect(isPrivateHostname(host)).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
it('defers to IP predicates when the hostname is an IP literal', () => {
|
|
84
|
+
expect(isPrivateHostname('127.0.0.1')).toBe(true);
|
|
85
|
+
expect(isPrivateHostname('169.254.169.254')).toBe(true);
|
|
86
|
+
expect(isPrivateHostname('::1')).toBe(true);
|
|
87
|
+
expect(isPrivateHostname('1.1.1.1')).toBe(false);
|
|
88
|
+
expect(isPrivateHostname('2606:4700:4700::1111')).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
it('handles IPv6 literals wrapped in brackets (as URL.hostname returns)', () => {
|
|
91
|
+
expect(isPrivateHostname('[::1]')).toBe(true);
|
|
92
|
+
expect(isPrivateHostname('[fc00::1]')).toBe(true);
|
|
93
|
+
expect(isPrivateHostname('[2606:4700:4700::1111]')).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
//# sourceMappingURL=is-private-ip.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"is-private-ip.test.js","sourceRoot":"","sources":["../../src/node/is-private-ip.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for {@link validatePublicUrl}.
|
|
3
|
+
*/
|
|
4
|
+
export interface ValidatePublicUrlOptions {
|
|
5
|
+
/**
|
|
6
|
+
* URL schemes that are allowed (e.g. `['http', 'https']`). Compared
|
|
7
|
+
* case-insensitively. Defaults to `['http', 'https']`.
|
|
8
|
+
*/
|
|
9
|
+
allowedSchemes?: readonly string[];
|
|
10
|
+
/**
|
|
11
|
+
* IP family hint passed to `dns.lookup`. `0` (default) returns whichever
|
|
12
|
+
* family the resolver prefers, `4` forces IPv4-only, `6` forces IPv6-only.
|
|
13
|
+
*/
|
|
14
|
+
family?: 0 | 4 | 6;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The resolved address that callers must use as the connect target so the
|
|
18
|
+
* connection lands on the IP that validation checked. Connecting via the
|
|
19
|
+
* hostname instead opens a DNS rebinding window.
|
|
20
|
+
*/
|
|
21
|
+
export interface ResolvedAddress {
|
|
22
|
+
address: string;
|
|
23
|
+
family: 4 | 6;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Validates that `url` is a parseable HTTP(S) URL whose hostname resolves
|
|
27
|
+
* to publicly routable IP addresses, and returns one of those addresses
|
|
28
|
+
* pinned for the caller to use as the connect target.
|
|
29
|
+
*
|
|
30
|
+
* DNS resolution is performed with `all: true`; the URL is rejected if any
|
|
31
|
+
* resolved address is in a private / loopback / link-local /
|
|
32
|
+
* cloud-metadata range, so a mixed public/private DNS response cannot
|
|
33
|
+
* sneak a private record through.
|
|
34
|
+
*
|
|
35
|
+
* Per ADR-035, this function throws subclasses of {@link UrlValidationError}
|
|
36
|
+
* on failure. The structured payload (`code`, `message`, `received`,
|
|
37
|
+
* `expected`, `remediation`, `pointer`, `cause`) is available on the thrown
|
|
38
|
+
* instance.
|
|
39
|
+
*
|
|
40
|
+
* @see https://owasp.org/www-community/attacks/Server_Side_Request_Forgery
|
|
41
|
+
* @see ../../../docs/adrs/035-utils-throws-structured-errors.md
|
|
42
|
+
* @throws {InvalidUrlError} `url` is not a parseable URL.
|
|
43
|
+
* @throws {UnsupportedSchemeError} the URL's scheme is not allowed.
|
|
44
|
+
* @throws {PrivateHostnameError} the hostname names a private resource.
|
|
45
|
+
* @throws {ResolutionFailedError} DNS resolution rejected.
|
|
46
|
+
* @throws {ResolutionEmptyError} DNS resolution returned no records.
|
|
47
|
+
* @throws {PrivateAddressError} any resolved record is private.
|
|
48
|
+
*/
|
|
49
|
+
export declare function validatePublicUrl(url: string, options?: ValidatePublicUrlOptions): Promise<ResolvedAddress>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { isIP } from 'node:net';
|
|
2
|
+
import { lookup as dnsLookup } from 'node:dns/promises';
|
|
3
|
+
import { isPrivateHostname, isPrivateIpv4, isPrivateIpv6 } from './is-private-ip.js';
|
|
4
|
+
import { InvalidUrlError, PrivateAddressError, PrivateHostnameError, ResolutionEmptyError, ResolutionFailedError, UnsupportedSchemeError, } from './errors.js';
|
|
5
|
+
const DEFAULT_ALLOWED_SCHEMES = ['http', 'https'];
|
|
6
|
+
/**
|
|
7
|
+
* Validates that `url` is a parseable HTTP(S) URL whose hostname resolves
|
|
8
|
+
* to publicly routable IP addresses, and returns one of those addresses
|
|
9
|
+
* pinned for the caller to use as the connect target.
|
|
10
|
+
*
|
|
11
|
+
* DNS resolution is performed with `all: true`; the URL is rejected if any
|
|
12
|
+
* resolved address is in a private / loopback / link-local /
|
|
13
|
+
* cloud-metadata range, so a mixed public/private DNS response cannot
|
|
14
|
+
* sneak a private record through.
|
|
15
|
+
*
|
|
16
|
+
* Per ADR-035, this function throws subclasses of {@link UrlValidationError}
|
|
17
|
+
* on failure. The structured payload (`code`, `message`, `received`,
|
|
18
|
+
* `expected`, `remediation`, `pointer`, `cause`) is available on the thrown
|
|
19
|
+
* instance.
|
|
20
|
+
*
|
|
21
|
+
* @see https://owasp.org/www-community/attacks/Server_Side_Request_Forgery
|
|
22
|
+
* @see ../../../docs/adrs/035-utils-throws-structured-errors.md
|
|
23
|
+
* @throws {InvalidUrlError} `url` is not a parseable URL.
|
|
24
|
+
* @throws {UnsupportedSchemeError} the URL's scheme is not allowed.
|
|
25
|
+
* @throws {PrivateHostnameError} the hostname names a private resource.
|
|
26
|
+
* @throws {ResolutionFailedError} DNS resolution rejected.
|
|
27
|
+
* @throws {ResolutionEmptyError} DNS resolution returned no records.
|
|
28
|
+
* @throws {PrivateAddressError} any resolved record is private.
|
|
29
|
+
*/
|
|
30
|
+
export async function validatePublicUrl(url, options) {
|
|
31
|
+
let parsed;
|
|
32
|
+
try {
|
|
33
|
+
parsed = new URL(url);
|
|
34
|
+
}
|
|
35
|
+
catch (cause) {
|
|
36
|
+
throw new InvalidUrlError(url, cause);
|
|
37
|
+
}
|
|
38
|
+
const allowedSchemes = options?.allowedSchemes ?? DEFAULT_ALLOWED_SCHEMES;
|
|
39
|
+
const scheme = parsed.protocol.toLowerCase().replace(/:$/, '');
|
|
40
|
+
if (!allowedSchemes.some((s) => s.toLowerCase() === scheme)) {
|
|
41
|
+
throw new UnsupportedSchemeError(scheme, allowedSchemes);
|
|
42
|
+
}
|
|
43
|
+
// URL.hostname wraps IPv6 literals in brackets (e.g. `[::1]`); strip them
|
|
44
|
+
// so the hostname can be passed to predicates and DNS resolution. Note
|
|
45
|
+
// that `URL` already discards any `userinfo@` prefix from the hostname,
|
|
46
|
+
// so smuggling attempts like `http://evil.com@127.0.0.1/` resolve to
|
|
47
|
+
// `127.0.0.1` and are caught by isPrivateHostname below.
|
|
48
|
+
const hostname = parsed.hostname.replace(/^\[|\]$/g, '');
|
|
49
|
+
if (isPrivateHostname(hostname)) {
|
|
50
|
+
throw new PrivateHostnameError(hostname);
|
|
51
|
+
}
|
|
52
|
+
// If the hostname is already an IP literal, skip DNS resolution; the
|
|
53
|
+
// literal itself is the resolved address, and the private-range check
|
|
54
|
+
// above (via isPrivateHostname) has already validated it.
|
|
55
|
+
const literalFamily = isIP(hostname);
|
|
56
|
+
if (literalFamily === 4 || literalFamily === 6) {
|
|
57
|
+
return { address: hostname, family: literalFamily };
|
|
58
|
+
}
|
|
59
|
+
let records;
|
|
60
|
+
try {
|
|
61
|
+
records = await dnsLookup(hostname, { family: options?.family ?? 0, all: true });
|
|
62
|
+
}
|
|
63
|
+
catch (cause) {
|
|
64
|
+
throw new ResolutionFailedError(hostname, cause);
|
|
65
|
+
}
|
|
66
|
+
if (records.length === 0) {
|
|
67
|
+
throw new ResolutionEmptyError(hostname);
|
|
68
|
+
}
|
|
69
|
+
const privateRecords = [];
|
|
70
|
+
let firstPublicRecord = null;
|
|
71
|
+
for (const record of records) {
|
|
72
|
+
if (record.family !== 4 && record.family !== 6) {
|
|
73
|
+
throw new ResolutionFailedError(hostname, new Error(`unsupported address family ${record.family}`));
|
|
74
|
+
}
|
|
75
|
+
const isPrivate = record.family === 4 ? isPrivateIpv4(record.address) : isPrivateIpv6(record.address);
|
|
76
|
+
if (isPrivate) {
|
|
77
|
+
privateRecords.push(record.address);
|
|
78
|
+
}
|
|
79
|
+
else if (!firstPublicRecord) {
|
|
80
|
+
firstPublicRecord = { address: record.address, family: record.family };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (privateRecords.length > 0) {
|
|
84
|
+
throw new PrivateAddressError(hostname, privateRecords);
|
|
85
|
+
}
|
|
86
|
+
// Defensive: `records.length > 0` and `privateRecords.length === 0`
|
|
87
|
+
// implies a public record was assigned in the loop. Throwing rather
|
|
88
|
+
// than `!`-asserting keeps the invariant explicit at the boundary.
|
|
89
|
+
if (!firstPublicRecord) {
|
|
90
|
+
throw new ResolutionEmptyError(hostname);
|
|
91
|
+
}
|
|
92
|
+
return firstPublicRecord;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=validate-public-url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-public-url.js","sourceRoot":"","sources":["../../src/node/validate-public-url.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|