@rexezuge/shared 0.0.0-stage → 1.0.1
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 +21 -0
- package/dist/Base64.d.ts +52 -0
- package/dist/Base64.d.ts.map +1 -0
- package/dist/Base64.js +81 -0
- package/dist/Base64.js.map +1 -0
- package/dist/Clock.d.ts +56 -0
- package/dist/Clock.d.ts.map +1 -0
- package/dist/Clock.js +59 -0
- package/dist/Clock.js.map +1 -0
- package/dist/CryptoUtil.d.ts +79 -0
- package/dist/CryptoUtil.d.ts.map +1 -0
- package/dist/CryptoUtil.js +97 -0
- package/dist/CryptoUtil.js.map +1 -0
- package/dist/CursorCodec.d.ts +61 -0
- package/dist/CursorCodec.d.ts.map +1 -0
- package/dist/CursorCodec.js +60 -0
- package/dist/CursorCodec.js.map +1 -0
- package/dist/EmailUtil.d.ts +29 -0
- package/dist/EmailUtil.d.ts.map +1 -0
- package/dist/EmailUtil.js +31 -0
- package/dist/EmailUtil.js.map +1 -0
- package/dist/ErrorSanitizationUtil.d.ts +69 -0
- package/dist/ErrorSanitizationUtil.d.ts.map +1 -0
- package/dist/ErrorSanitizationUtil.js +98 -0
- package/dist/ErrorSanitizationUtil.js.map +1 -0
- package/dist/IdGenerator.d.ts +27 -0
- package/dist/IdGenerator.d.ts.map +1 -0
- package/dist/IdGenerator.js +28 -0
- package/dist/IdGenerator.js.map +1 -0
- package/dist/Identity.d.ts +36 -0
- package/dist/Identity.d.ts.map +1 -0
- package/dist/Identity.js +39 -0
- package/dist/Identity.js.map +1 -0
- package/dist/LanguageTag.d.ts +25 -0
- package/dist/LanguageTag.d.ts.map +1 -0
- package/dist/LanguageTag.js +29 -0
- package/dist/LanguageTag.js.map +1 -0
- package/dist/LocaleUtil.d.ts +35 -0
- package/dist/LocaleUtil.d.ts.map +1 -0
- package/dist/LocaleUtil.js +97 -0
- package/dist/LocaleUtil.js.map +1 -0
- package/dist/PasswordFingerprint.d.ts +30 -0
- package/dist/PasswordFingerprint.d.ts.map +1 -0
- package/dist/PasswordFingerprint.js +33 -0
- package/dist/PasswordFingerprint.js.map +1 -0
- package/dist/RemoteUrlPolicy.d.ts +121 -0
- package/dist/RemoteUrlPolicy.d.ts.map +1 -0
- package/dist/RemoteUrlPolicy.js +339 -0
- package/dist/RemoteUrlPolicy.js.map +1 -0
- package/dist/Result.d.ts +32 -0
- package/dist/Result.d.ts.map +1 -0
- package/dist/Result.js +19 -0
- package/dist/Result.js.map +1 -0
- package/dist/SubrequestMeter.d.ts +204 -0
- package/dist/SubrequestMeter.d.ts.map +1 -0
- package/dist/SubrequestMeter.js +169 -0
- package/dist/SubrequestMeter.js.map +1 -0
- package/dist/TimeZoneUtil.d.ts +47 -0
- package/dist/TimeZoneUtil.d.ts.map +1 -0
- package/dist/TimeZoneUtil.js +101 -0
- package/dist/TimeZoneUtil.js.map +1 -0
- package/dist/TimestampUtil.d.ts +53 -0
- package/dist/TimestampUtil.d.ts.map +1 -0
- package/dist/TimestampUtil.js +69 -0
- package/dist/TimestampUtil.js.map +1 -0
- package/dist/TokenHashUtil.d.ts +34 -0
- package/dist/TokenHashUtil.d.ts.map +1 -0
- package/dist/TokenHashUtil.js +37 -0
- package/dist/TokenHashUtil.js.map +1 -0
- package/dist/UUIDUtil.d.ts +70 -0
- package/dist/UUIDUtil.d.ts.map +1 -0
- package/dist/UUIDUtil.js +79 -0
- package/dist/UUIDUtil.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/package.json +34 -3
- package/README.md +0 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PasswordFingerprint.js","sourceRoot":"","sources":["../src/PasswordFingerprint.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,KAAK,UAAU,mBAAmB,CAAC,QAAgB;IACjD,OAAO,UAAU,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;AACxC,CAAC;AAED,OAAO,EAAE,mBAAmB,EAAE,CAAC"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Egress policy and SSRF host predicates for user-supplied URLs.
|
|
3
|
+
*
|
|
4
|
+
* ## Provenance
|
|
5
|
+
*
|
|
6
|
+
* Merged from Durable-DAV's `packages/shared/src/net/RemoteUrlPolicy.ts` and the
|
|
7
|
+
* `SsrfHosts.ts` of Durable-DAV-Router, Edge-Git and Edge-Sonic. The two were the
|
|
8
|
+
* same problem solved twice: Durable-DAV's `RemoteUrlPolicy` is the full
|
|
9
|
+
* validate-and-canonicalize policy (HTTPS-only, public-address-only, operator
|
|
10
|
+
* allow-list, redirect re-validation); the Router/Edge-Git/Edge-Sonic
|
|
11
|
+
* `SsrfHosts` are the lower-level "is this host private?" predicate the policy in
|
|
12
|
+
* each repo leaned on. Canonical name is **`RemoteUrlPolicy`** (Durable-DAV's);
|
|
13
|
+
* the host predicate it converges onto is exported as **`isPrivateOrInternalHost`**
|
|
14
|
+
* (the common `isSsrfBlockedHost`-style composite from SsrfHosts).
|
|
15
|
+
*
|
|
16
|
+
* Scheduled replication / webhook delivery / git import turns a caller into
|
|
17
|
+
* someone who can name a host this Worker connects to. Without a policy that is
|
|
18
|
+
* server-side request forgery against Cloudflare's own egress: the metadata
|
|
19
|
+
* service, the loopback interface, and anything reachable on a private network the
|
|
20
|
+
* Worker happens to share.
|
|
21
|
+
*
|
|
22
|
+
* ## Design decisions
|
|
23
|
+
*
|
|
24
|
+
* - **The octet-accurate IP checks win over SsrfHosts' naive string prefixes.**
|
|
25
|
+
* SsrfHosts' `isBlockedIpv6Host` matched `startsWith('fc')`/`'fd')`, which also
|
|
26
|
+
* matches a *hostname* like `fcb.example.com`; Durable-DAV's `isBlockedIpv6` /
|
|
27
|
+
* `isBlockedIpv4` parse the address (and decode IPv4-mapped IPv6) instead. The
|
|
28
|
+
* kit keeps the rigorous form for both the URL policy and the exported
|
|
29
|
+
* predicate, so there is one correct answer rather than two that drift.
|
|
30
|
+
* - **`isPrivateOrInternalHost` is strictly safer than either source.** It folds
|
|
31
|
+
* in SsrfHosts' raw-hostname handling (bare-integer/octal/hex/`0x`-dotted forms,
|
|
32
|
+
* trailing-dot and bracket stripping, localhost names and `.local`/`.internal`/
|
|
33
|
+
* `.home.arpa` suffixes) that Durable-DAV's URL-path checks could skip because
|
|
34
|
+
* `new URL` had already canonicalized the host. A public IPv6 literal is allowed
|
|
35
|
+
* exactly as `normalizeRemoteUrl` allows it; every private/loopback/unique-local/
|
|
36
|
+
* link-local/zone/mapped form is refused.
|
|
37
|
+
*
|
|
38
|
+
* ## Known gap, stated rather than hidden
|
|
39
|
+
*
|
|
40
|
+
* DNS rebinding is not covered. A host that resolves to a public address at
|
|
41
|
+
* validation time and a private one when the connection is made defeats every
|
|
42
|
+
* check here. Closing it needs a resolution-aware fetch, which the platform does
|
|
43
|
+
* not expose to a Worker. The upheld rule for such a deployment is to pin an
|
|
44
|
+
* origin allow-list.
|
|
45
|
+
*/
|
|
46
|
+
declare const MAX_REMOTE_URL_LENGTH = 2048;
|
|
47
|
+
declare const MAX_REDIRECTS = 3;
|
|
48
|
+
/**
|
|
49
|
+
* Host suffixes that never name a real internet peer.
|
|
50
|
+
*/
|
|
51
|
+
declare const BLOCKED_HOST_SUFFIXES: readonly [".localhost", ".local", ".internal", ".home.arpa"];
|
|
52
|
+
/**
|
|
53
|
+
* Hostnames that resolve inward by definition.
|
|
54
|
+
*/
|
|
55
|
+
declare const BLOCKED_HOSTNAMES: Set<string>;
|
|
56
|
+
type UrlPolicyOptions = {
|
|
57
|
+
/**
|
|
58
|
+
* Hostnames (exact, or `.suffix` form) permitted regardless of the address
|
|
59
|
+
* rules. Only ever populated from operator configuration, never from the
|
|
60
|
+
* request body.
|
|
61
|
+
*/
|
|
62
|
+
allowedHosts?: readonly string[];
|
|
63
|
+
};
|
|
64
|
+
declare class RemoteUrlRejectedError extends Error {
|
|
65
|
+
constructor(message: string);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Strip one enclosing `[`…`]` pair (an IPv6 literal's URL brackets).
|
|
69
|
+
*/
|
|
70
|
+
declare function stripBrackets(host: string): string;
|
|
71
|
+
/**
|
|
72
|
+
* Strip a bracketed wrapper and every trailing root-label dot.
|
|
73
|
+
*
|
|
74
|
+
* A fully-qualified name ends in a root label (`example.com.`) and `new URL` keeps
|
|
75
|
+
* it; RFC 4918 §9.1.2 clients vary on sending it, so the canonical form is compared.
|
|
76
|
+
*/
|
|
77
|
+
declare function stripTrailingDot(host: string): string;
|
|
78
|
+
/**
|
|
79
|
+
* True when a URL host resolves to this machine or a private network.
|
|
80
|
+
*
|
|
81
|
+
* Intended for user-supplied URLs that the server will subsequently fetch. Such a
|
|
82
|
+
* URL turns the process into a proxy into its own network: without this, a
|
|
83
|
+
* user-supplied `baseUrl` can point at cloud metadata, a database host, or a
|
|
84
|
+
* loopback admin port, and the response is readable by the caller.
|
|
85
|
+
*
|
|
86
|
+
* Encoded-numeric hosts are included because `http://2130706433/` and
|
|
87
|
+
* `http://0177.0.0.1/` are ordinary-looking strings that name `127.0.0.1` after
|
|
88
|
+
* parsing.
|
|
89
|
+
*
|
|
90
|
+
* A *strict, safer* predicate: an empty host, any private/loopback/link-local/
|
|
91
|
+
* unique-local/mapped form, any localhost/suffix name, any obfuscated numeric
|
|
92
|
+
* form, and any `:`-bearing host that is not a public IPv6 literal all return
|
|
93
|
+
* true. It is a *syntactic* check — a public hostname can still resolve to a
|
|
94
|
+
* private address (DNS rebinding) — so a hard-guarantee deployment additionally
|
|
95
|
+
* pins an allow-list (see the file header).
|
|
96
|
+
*/
|
|
97
|
+
declare function isPrivateOrInternalHost(hostname: string): boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Validate and canonicalise a remote URL.
|
|
100
|
+
*
|
|
101
|
+
* Throws `RemoteUrlRejectedError` with a message meant for the owner: every
|
|
102
|
+
* rejection here is a configuration mistake or an attack, and saying which one
|
|
103
|
+
* is what makes the difference between a fixable setup error and a support
|
|
104
|
+
* ticket. Query and fragment are dropped — a WebDAV base URL has no use for
|
|
105
|
+
* either, and keeping them would let `?a=b` smuggle a second meaning past the
|
|
106
|
+
* path checks the sync engine does.
|
|
107
|
+
*/
|
|
108
|
+
declare function normalizeRemoteUrl(raw: unknown, options?: UrlPolicyOptions): string;
|
|
109
|
+
/**
|
|
110
|
+
* Re-validate the target of a redirect.
|
|
111
|
+
*
|
|
112
|
+
* Called for every hop rather than once at the start. Validating the configured
|
|
113
|
+
* URL and then following `Location` unchecked is a classic TOCTOU: the first
|
|
114
|
+
* hop is public, the second is `169.254.169.254`, and the policy has already
|
|
115
|
+
* returned by then.
|
|
116
|
+
*/
|
|
117
|
+
declare function resolveRedirectUrl(baseUrl: string, location: string, options?: UrlPolicyOptions): string;
|
|
118
|
+
declare function parseAllowedHosts(raw: unknown): string[];
|
|
119
|
+
export { normalizeRemoteUrl, resolveRedirectUrl, parseAllowedHosts, isPrivateOrInternalHost, stripBrackets, stripTrailingDot, RemoteUrlRejectedError, MAX_REMOTE_URL_LENGTH, MAX_REDIRECTS, BLOCKED_HOST_SUFFIXES, BLOCKED_HOSTNAMES, };
|
|
120
|
+
export type { UrlPolicyOptions };
|
|
121
|
+
//# sourceMappingURL=RemoteUrlPolicy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RemoteUrlPolicy.d.ts","sourceRoot":"","sources":["../src/RemoteUrlPolicy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,QAAA,MAAM,qBAAqB,OAAO,CAAC;AACnC,QAAA,MAAM,aAAa,IAAI,CAAC;AAExB;;GAEG;AACH,QAAA,MAAM,qBAAqB,8DAA+D,CAAC;AAE3F;;GAEG;AACH,QAAA,MAAM,iBAAiB,aAAmF,CAAC;AAE3G,KAAK,gBAAgB,GAAG;IACtB;;;;OAIG;IACH,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC,CAAC;AAEF,cAAM,sBAAuB,SAAQ,KAAK;gBAC5B,OAAO,EAAE,MAAM;CAI5B;AA8GD;;GAEG;AACH,iBAAS,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG3C;AAED;;;;;GAKG;AACH,iBAAS,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAK9C;AASD;;;;;;;;;;;;;;;;;;GAkBG;AACH,iBAAS,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAa1D;AAED;;;;;;;;;GASG;AACH,iBAAS,kBAAkB,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CA4ChF;AAED;;;;;;;GAOG;AACH,iBAAS,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CAgBrG;AAED,iBAAS,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,EAAE,CAKjD;AAED,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,uBAAuB,EACvB,aAAa,EACb,gBAAgB,EAChB,sBAAsB,EACtB,qBAAqB,EACrB,aAAa,EACb,qBAAqB,EACrB,iBAAiB,GAClB,CAAC;AACF,YAAY,EAAE,gBAAgB,EAAE,CAAC"}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Egress policy and SSRF host predicates for user-supplied URLs.
|
|
3
|
+
*
|
|
4
|
+
* ## Provenance
|
|
5
|
+
*
|
|
6
|
+
* Merged from Durable-DAV's `packages/shared/src/net/RemoteUrlPolicy.ts` and the
|
|
7
|
+
* `SsrfHosts.ts` of Durable-DAV-Router, Edge-Git and Edge-Sonic. The two were the
|
|
8
|
+
* same problem solved twice: Durable-DAV's `RemoteUrlPolicy` is the full
|
|
9
|
+
* validate-and-canonicalize policy (HTTPS-only, public-address-only, operator
|
|
10
|
+
* allow-list, redirect re-validation); the Router/Edge-Git/Edge-Sonic
|
|
11
|
+
* `SsrfHosts` are the lower-level "is this host private?" predicate the policy in
|
|
12
|
+
* each repo leaned on. Canonical name is **`RemoteUrlPolicy`** (Durable-DAV's);
|
|
13
|
+
* the host predicate it converges onto is exported as **`isPrivateOrInternalHost`**
|
|
14
|
+
* (the common `isSsrfBlockedHost`-style composite from SsrfHosts).
|
|
15
|
+
*
|
|
16
|
+
* Scheduled replication / webhook delivery / git import turns a caller into
|
|
17
|
+
* someone who can name a host this Worker connects to. Without a policy that is
|
|
18
|
+
* server-side request forgery against Cloudflare's own egress: the metadata
|
|
19
|
+
* service, the loopback interface, and anything reachable on a private network the
|
|
20
|
+
* Worker happens to share.
|
|
21
|
+
*
|
|
22
|
+
* ## Design decisions
|
|
23
|
+
*
|
|
24
|
+
* - **The octet-accurate IP checks win over SsrfHosts' naive string prefixes.**
|
|
25
|
+
* SsrfHosts' `isBlockedIpv6Host` matched `startsWith('fc')`/`'fd')`, which also
|
|
26
|
+
* matches a *hostname* like `fcb.example.com`; Durable-DAV's `isBlockedIpv6` /
|
|
27
|
+
* `isBlockedIpv4` parse the address (and decode IPv4-mapped IPv6) instead. The
|
|
28
|
+
* kit keeps the rigorous form for both the URL policy and the exported
|
|
29
|
+
* predicate, so there is one correct answer rather than two that drift.
|
|
30
|
+
* - **`isPrivateOrInternalHost` is strictly safer than either source.** It folds
|
|
31
|
+
* in SsrfHosts' raw-hostname handling (bare-integer/octal/hex/`0x`-dotted forms,
|
|
32
|
+
* trailing-dot and bracket stripping, localhost names and `.local`/`.internal`/
|
|
33
|
+
* `.home.arpa` suffixes) that Durable-DAV's URL-path checks could skip because
|
|
34
|
+
* `new URL` had already canonicalized the host. A public IPv6 literal is allowed
|
|
35
|
+
* exactly as `normalizeRemoteUrl` allows it; every private/loopback/unique-local/
|
|
36
|
+
* link-local/zone/mapped form is refused.
|
|
37
|
+
*
|
|
38
|
+
* ## Known gap, stated rather than hidden
|
|
39
|
+
*
|
|
40
|
+
* DNS rebinding is not covered. A host that resolves to a public address at
|
|
41
|
+
* validation time and a private one when the connection is made defeats every
|
|
42
|
+
* check here. Closing it needs a resolution-aware fetch, which the platform does
|
|
43
|
+
* not expose to a Worker. The upheld rule for such a deployment is to pin an
|
|
44
|
+
* origin allow-list.
|
|
45
|
+
*/
|
|
46
|
+
const MAX_REMOTE_URL_LENGTH = 2048;
|
|
47
|
+
const MAX_REDIRECTS = 3;
|
|
48
|
+
/**
|
|
49
|
+
* Host suffixes that never name a real internet peer.
|
|
50
|
+
*/
|
|
51
|
+
const BLOCKED_HOST_SUFFIXES = ['.localhost', '.local', '.internal', '.home.arpa'];
|
|
52
|
+
/**
|
|
53
|
+
* Hostnames that resolve inward by definition.
|
|
54
|
+
*/
|
|
55
|
+
const BLOCKED_HOSTNAMES = new Set(['localhost', 'localhost.localdomain', 'ip6-localhost', 'ip6-loopback']);
|
|
56
|
+
class RemoteUrlRejectedError extends Error {
|
|
57
|
+
constructor(message) {
|
|
58
|
+
super(message);
|
|
59
|
+
this.name = 'RemoteUrlRejectedError';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function isIpv4Literal(host) {
|
|
63
|
+
return /^\d{1,3}(?:\.\d{1,3}){3}$/.test(host);
|
|
64
|
+
}
|
|
65
|
+
function ipv4Octets(host) {
|
|
66
|
+
const parts = host.split('.');
|
|
67
|
+
if (parts.length !== 4)
|
|
68
|
+
return null;
|
|
69
|
+
const octets = [];
|
|
70
|
+
for (const part of parts) {
|
|
71
|
+
if (!/^\d{1,3}$/.test(part))
|
|
72
|
+
return null;
|
|
73
|
+
const value = Number(part);
|
|
74
|
+
if (value > 255)
|
|
75
|
+
return null;
|
|
76
|
+
octets.push(value);
|
|
77
|
+
}
|
|
78
|
+
return octets;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Is this IPv4 literal in a range a Worker must never reach?
|
|
82
|
+
*
|
|
83
|
+
* `0.0.0.0/8` and `169.254.0.0/16` matter as much as RFC 1918: the first is
|
|
84
|
+
* "this host" on Linux, the second is the cloud metadata endpoint.
|
|
85
|
+
*/
|
|
86
|
+
function isBlockedIpv4(octets) {
|
|
87
|
+
const [a = 0, b = 0] = octets;
|
|
88
|
+
if ([0, 10, 127].includes(a))
|
|
89
|
+
return true;
|
|
90
|
+
if (a === 169 && b === 254)
|
|
91
|
+
return true;
|
|
92
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
93
|
+
return true;
|
|
94
|
+
if (a === 192 && b === 168)
|
|
95
|
+
return true;
|
|
96
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
97
|
+
return true;
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Decode an IPv4-mapped IPv6 address back into its four octets, or null.
|
|
102
|
+
*
|
|
103
|
+
* Both spellings have to be handled. `new URL` canonicalizes
|
|
104
|
+
* `https://[::ffff:10.0.0.1]/` to the hex form `[::ffff:a00:1]`, so a check
|
|
105
|
+
* written only against the dotted text never fires — and that address connects to
|
|
106
|
+
* a private network.
|
|
107
|
+
*/
|
|
108
|
+
function mappedIpv4Octets(literal) {
|
|
109
|
+
const dotted = /^::ffff:\d{1,3}(?:\.\d{1,3}){3}$/.exec(literal);
|
|
110
|
+
const dottedText = literal.slice('::ffff:'.length);
|
|
111
|
+
if (dotted !== null)
|
|
112
|
+
return ipv4Octets(dottedText);
|
|
113
|
+
const hex = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(literal);
|
|
114
|
+
if (!hex?.[1] || !hex[2])
|
|
115
|
+
return null;
|
|
116
|
+
const high = Number.parseInt(hex[1], 16);
|
|
117
|
+
const low = Number.parseInt(hex[2], 16);
|
|
118
|
+
return !Number.isFinite(high) || !Number.isFinite(low) ? null : [(high >> 8) & 0xff, high & 0xff, (low >> 8) & 0xff, low & 0xff];
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Is this IPv6 literal private, loopback, or an IPv4 address in disguise? `literal`
|
|
122
|
+
* is the **unbracketed** inner form (e.g. `::1`, `fe80::1`).
|
|
123
|
+
*
|
|
124
|
+
* The last case is the one that matters: `::ffff:127.0.0.1` is a perfectly valid
|
|
125
|
+
* URL and connects to loopback, so the embedded IPv4 must be re-checked rather
|
|
126
|
+
* than the literal waved through because it "is" IPv6.
|
|
127
|
+
*/
|
|
128
|
+
function isBlockedIpv6(literal) {
|
|
129
|
+
const lower = literal.toLowerCase();
|
|
130
|
+
if (lower === '::' || lower === '::1')
|
|
131
|
+
return true;
|
|
132
|
+
// Zone ids (`fe80::1%eth0`) name a local interface directly.
|
|
133
|
+
if (lower.includes('%'))
|
|
134
|
+
return true;
|
|
135
|
+
const mapped = mappedIpv4Octets(lower);
|
|
136
|
+
if (mapped !== null)
|
|
137
|
+
return isBlockedIpv4(mapped);
|
|
138
|
+
const head = lower.split(':', 1)[0] ?? '';
|
|
139
|
+
if (head === '')
|
|
140
|
+
return false;
|
|
141
|
+
const group = Number.parseInt(head.padStart(4, '0').slice(0, 4), 16);
|
|
142
|
+
if (!Number.isFinite(group))
|
|
143
|
+
return false;
|
|
144
|
+
// fc00::/7 unique-local, fe80::/10 link-local.
|
|
145
|
+
return (group & 0xfe_00) === 0xfc_00 || (group & 0xff_c0) === 0xfe_80;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Does this host name a numeric address in a non-canonical spelling? The strict
|
|
149
|
+
* superset of Durable-DAV's `looksLikeObfuscatedNumericHost` and SsrfHosts'
|
|
150
|
+
* `isEncodedNumericHost`, for the raw-hostname predicate.
|
|
151
|
+
*
|
|
152
|
+
* `2130706433`, `0177.0.0.1`, `0x7f000001` and `0x7f.1` all name `127.0.0.1`.
|
|
153
|
+
* Through `new URL` these arrive already canonicalized (so `isBlockedIpv4` catches
|
|
154
|
+
* them); a hostname handed to `isPrivateOrInternalHost` need not have gone through
|
|
155
|
+
* `URL`, so a bare integer/octal/hex form is caught here directly. A legitimate
|
|
156
|
+
* internet hostname is never all-digits, so over-blocking is not a risk.
|
|
157
|
+
*/
|
|
158
|
+
function looksLikeObfuscatedNumericHost(host) {
|
|
159
|
+
if (isIpv4Literal(host) || host.startsWith('[') || !/^[0-9a-fx.]+$/i.test(host))
|
|
160
|
+
return false;
|
|
161
|
+
return (/^0x[0-9a-f]+$/i.test(host) ||
|
|
162
|
+
/^\d+$/.test(host) ||
|
|
163
|
+
/^0[0-7]+(?:\.0[0-7]+)*$/.test(host) ||
|
|
164
|
+
// An all-digits-and-dots host that is not a canonical dotted-quad — e.g.
|
|
165
|
+
// `0177.0.0.1` or `0200.0250.0001.0001` — is an obfuscated numeric address.
|
|
166
|
+
/^[\d.]+$/.test(host) ||
|
|
167
|
+
host.split('.').some((group) => /^0x[0-9a-f]+$/i.test(group)));
|
|
168
|
+
}
|
|
169
|
+
function hostIsAllowed(host, allowedHosts) {
|
|
170
|
+
const lower = host.toLowerCase();
|
|
171
|
+
return allowedHosts.some((entry) => {
|
|
172
|
+
const candidate = entry.trim().toLowerCase();
|
|
173
|
+
if (candidate === '')
|
|
174
|
+
return false;
|
|
175
|
+
return candidate.startsWith('.') ? lower.endsWith(candidate) : lower === candidate;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Strip one enclosing `[`…`]` pair (an IPv6 literal's URL brackets).
|
|
180
|
+
*/
|
|
181
|
+
function stripBrackets(host) {
|
|
182
|
+
const h = host.trim();
|
|
183
|
+
return h.startsWith('[') && h.endsWith(']') ? h.slice(1, -1) : h;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Strip a bracketed wrapper and every trailing root-label dot.
|
|
187
|
+
*
|
|
188
|
+
* A fully-qualified name ends in a root label (`example.com.`) and `new URL` keeps
|
|
189
|
+
* it; RFC 4918 §9.1.2 clients vary on sending it, so the canonical form is compared.
|
|
190
|
+
*/
|
|
191
|
+
function stripTrailingDot(host) {
|
|
192
|
+
const unbracketed = stripBrackets(host);
|
|
193
|
+
let end = unbracketed.length;
|
|
194
|
+
while (end > 0 && unbracketed.charAt(end - 1) === '.')
|
|
195
|
+
end -= 1;
|
|
196
|
+
return unbracketed.slice(0, end);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Host names that mean "this machine or a local suffix", not an internet peer.
|
|
200
|
+
*/
|
|
201
|
+
function isLocalhostName(host) {
|
|
202
|
+
return BLOCKED_HOSTNAMES.has(host) || host.endsWith('.localhost');
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* True when a URL host resolves to this machine or a private network.
|
|
206
|
+
*
|
|
207
|
+
* Intended for user-supplied URLs that the server will subsequently fetch. Such a
|
|
208
|
+
* URL turns the process into a proxy into its own network: without this, a
|
|
209
|
+
* user-supplied `baseUrl` can point at cloud metadata, a database host, or a
|
|
210
|
+
* loopback admin port, and the response is readable by the caller.
|
|
211
|
+
*
|
|
212
|
+
* Encoded-numeric hosts are included because `http://2130706433/` and
|
|
213
|
+
* `http://0177.0.0.1/` are ordinary-looking strings that name `127.0.0.1` after
|
|
214
|
+
* parsing.
|
|
215
|
+
*
|
|
216
|
+
* A *strict, safer* predicate: an empty host, any private/loopback/link-local/
|
|
217
|
+
* unique-local/mapped form, any localhost/suffix name, any obfuscated numeric
|
|
218
|
+
* form, and any `:`-bearing host that is not a public IPv6 literal all return
|
|
219
|
+
* true. It is a *syntactic* check — a public hostname can still resolve to a
|
|
220
|
+
* private address (DNS rebinding) — so a hard-guarantee deployment additionally
|
|
221
|
+
* pins an allow-list (see the file header).
|
|
222
|
+
*/
|
|
223
|
+
function isPrivateOrInternalHost(hostname) {
|
|
224
|
+
const host = stripTrailingDot(hostname).toLowerCase();
|
|
225
|
+
if (host.length === 0)
|
|
226
|
+
return true;
|
|
227
|
+
if (isLocalhostName(host))
|
|
228
|
+
return true;
|
|
229
|
+
if (BLOCKED_HOST_SUFFIXES.some((suffix) => host.endsWith(suffix)))
|
|
230
|
+
return true;
|
|
231
|
+
if (isIpv4Literal(host)) {
|
|
232
|
+
const octets = ipv4Octets(host);
|
|
233
|
+
return octets === null || isBlockedIpv4(octets);
|
|
234
|
+
}
|
|
235
|
+
if (host.includes(':'))
|
|
236
|
+
return isBlockedIpv6(host);
|
|
237
|
+
if (looksLikeObfuscatedNumericHost(host))
|
|
238
|
+
return true;
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Validate and canonicalise a remote URL.
|
|
243
|
+
*
|
|
244
|
+
* Throws `RemoteUrlRejectedError` with a message meant for the owner: every
|
|
245
|
+
* rejection here is a configuration mistake or an attack, and saying which one
|
|
246
|
+
* is what makes the difference between a fixable setup error and a support
|
|
247
|
+
* ticket. Query and fragment are dropped — a WebDAV base URL has no use for
|
|
248
|
+
* either, and keeping them would let `?a=b` smuggle a second meaning past the
|
|
249
|
+
* path checks the sync engine does.
|
|
250
|
+
*/
|
|
251
|
+
function normalizeRemoteUrl(raw, options = {}) {
|
|
252
|
+
if (typeof raw !== 'string')
|
|
253
|
+
throw new RemoteUrlRejectedError('remoteUrl must be a string');
|
|
254
|
+
const trimmed = raw.trim();
|
|
255
|
+
if (trimmed === '')
|
|
256
|
+
throw new RemoteUrlRejectedError('remoteUrl is required');
|
|
257
|
+
if (trimmed.length > MAX_REMOTE_URL_LENGTH)
|
|
258
|
+
throw new RemoteUrlRejectedError(`remoteUrl must be at most ${MAX_REMOTE_URL_LENGTH} characters`);
|
|
259
|
+
let url;
|
|
260
|
+
try {
|
|
261
|
+
url = new URL(trimmed);
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
throw new RemoteUrlRejectedError('remoteUrl is not a valid absolute URL');
|
|
265
|
+
}
|
|
266
|
+
if (url.protocol !== 'https:')
|
|
267
|
+
throw new RemoteUrlRejectedError('remoteUrl must use https');
|
|
268
|
+
if (url.username !== '' || url.password !== '')
|
|
269
|
+
throw new RemoteUrlRejectedError('remoteUrl must not embed credentials; use the auth fields instead');
|
|
270
|
+
if (url.port !== '' && url.port !== '443')
|
|
271
|
+
throw new RemoteUrlRejectedError('remoteUrl must use the default https port');
|
|
272
|
+
const host = url.hostname.toLowerCase();
|
|
273
|
+
if (host === '')
|
|
274
|
+
throw new RemoteUrlRejectedError('remoteUrl must include a host');
|
|
275
|
+
if (hostIsAllowed(host, options.allowedHosts ?? [])) {
|
|
276
|
+
url.search = '';
|
|
277
|
+
url.hash = '';
|
|
278
|
+
return url.href;
|
|
279
|
+
}
|
|
280
|
+
if (BLOCKED_HOSTNAMES.has(host))
|
|
281
|
+
throw new RemoteUrlRejectedError(`remoteUrl host ${host} is not routable`);
|
|
282
|
+
for (const suffix of BLOCKED_HOST_SUFFIXES) {
|
|
283
|
+
if (host.endsWith(suffix))
|
|
284
|
+
throw new RemoteUrlRejectedError(`remoteUrl host ${host} is not a public hostname`);
|
|
285
|
+
}
|
|
286
|
+
if (looksLikeObfuscatedNumericHost(host))
|
|
287
|
+
throw new RemoteUrlRejectedError(`remoteUrl host ${host} is a numeric address in a non-canonical form`);
|
|
288
|
+
if (isIpv4Literal(host)) {
|
|
289
|
+
const octets = ipv4Octets(host);
|
|
290
|
+
if (octets === null || isBlockedIpv4(octets))
|
|
291
|
+
throw new RemoteUrlRejectedError(`remoteUrl host ${host} is not a public address`);
|
|
292
|
+
}
|
|
293
|
+
else if (host.startsWith('[')) {
|
|
294
|
+
if (isBlockedIpv6(host.slice(1, -1)))
|
|
295
|
+
throw new RemoteUrlRejectedError(`remoteUrl host ${host} is not a public address`);
|
|
296
|
+
}
|
|
297
|
+
else if (!host.includes('.')) {
|
|
298
|
+
// A single-label name can only be resolved through a local search domain.
|
|
299
|
+
throw new RemoteUrlRejectedError(`remoteUrl host ${host} is not a fully qualified hostname`);
|
|
300
|
+
}
|
|
301
|
+
url.search = '';
|
|
302
|
+
url.hash = '';
|
|
303
|
+
return url.href;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Re-validate the target of a redirect.
|
|
307
|
+
*
|
|
308
|
+
* Called for every hop rather than once at the start. Validating the configured
|
|
309
|
+
* URL and then following `Location` unchecked is a classic TOCTOU: the first
|
|
310
|
+
* hop is public, the second is `169.254.169.254`, and the policy has already
|
|
311
|
+
* returned by then.
|
|
312
|
+
*/
|
|
313
|
+
function resolveRedirectUrl(baseUrl, location, options = {}) {
|
|
314
|
+
let next;
|
|
315
|
+
try {
|
|
316
|
+
next = new URL(location, baseUrl);
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
throw new RemoteUrlRejectedError('redirect target is not a valid URL');
|
|
320
|
+
}
|
|
321
|
+
if (next.protocol !== 'https:')
|
|
322
|
+
throw new RemoteUrlRejectedError('redirect target must use https');
|
|
323
|
+
// Same host: no need to re-run the whole policy, and a redirect back to the
|
|
324
|
+
// origin's own host is the overwhelmingly common case.
|
|
325
|
+
if (next.host.toLowerCase() === new URL(baseUrl).host.toLowerCase()) {
|
|
326
|
+
next.search = '';
|
|
327
|
+
next.hash = '';
|
|
328
|
+
return next.href;
|
|
329
|
+
}
|
|
330
|
+
return normalizeRemoteUrl(next.href, options);
|
|
331
|
+
}
|
|
332
|
+
function parseAllowedHosts(raw) {
|
|
333
|
+
return typeof raw !== 'string' || raw.trim() === '' ? [] : raw
|
|
334
|
+
.split(',')
|
|
335
|
+
.map((entry) => entry.trim())
|
|
336
|
+
.filter((entry) => entry !== '');
|
|
337
|
+
}
|
|
338
|
+
export { normalizeRemoteUrl, resolveRedirectUrl, parseAllowedHosts, isPrivateOrInternalHost, stripBrackets, stripTrailingDot, RemoteUrlRejectedError, MAX_REMOTE_URL_LENGTH, MAX_REDIRECTS, BLOCKED_HOST_SUFFIXES, BLOCKED_HOSTNAMES, };
|
|
339
|
+
//# sourceMappingURL=RemoteUrlPolicy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RemoteUrlPolicy.js","sourceRoot":"","sources":["../src/RemoteUrlPolicy.ts"],"names":[],"mappings":"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"}
|
package/dist/Result.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal `Result` type for explicit error handling without exceptions.
|
|
3
|
+
*
|
|
4
|
+
* ## Provenance
|
|
5
|
+
*
|
|
6
|
+
* Converged from Edge-Git's `Result.ts` and ChordDHT-Tracker's `Result.ts`. The
|
|
7
|
+
* two are byte-identical apart from a doc comment (Edge-Git added the header);
|
|
8
|
+
* ChordDHT is the origin, Edge-Git the copy. Mail-Otter and the DAV repos are
|
|
9
|
+
* the kind of repo this was ported *to*, so Edge-Git's documented form is kept
|
|
10
|
+
* verbatim.
|
|
11
|
+
*
|
|
12
|
+
* When two variants are identical there is nothing stricter to choose; the
|
|
13
|
+
* supported shape is `{ ok: true, value }` / `{ ok: false, error }` frozen at the
|
|
14
|
+
* type level rather than thrown.
|
|
15
|
+
*/
|
|
16
|
+
type Ok<T> = {
|
|
17
|
+
readonly ok: true;
|
|
18
|
+
readonly value: T;
|
|
19
|
+
};
|
|
20
|
+
type Err<E> = {
|
|
21
|
+
readonly ok: false;
|
|
22
|
+
readonly error: E;
|
|
23
|
+
};
|
|
24
|
+
type Result<T, E = Error> = Ok<T> | Err<E>;
|
|
25
|
+
declare function ok<T>(value: T): Result<T, never>;
|
|
26
|
+
declare function err<E>(error: E): Result<never, E>;
|
|
27
|
+
declare function isOk<T, E>(result: Result<T, E>): result is Ok<T>;
|
|
28
|
+
declare function mapResult<T, U, E>(result: Result<T, E>, fn: (value: T) => U): Result<U, E>;
|
|
29
|
+
declare function getOrThrow<T, E = Error>(result: Result<T, E>): T;
|
|
30
|
+
export { err, getOrThrow, isOk, mapResult, ok };
|
|
31
|
+
export type { Err, Ok, Result };
|
|
32
|
+
//# sourceMappingURL=Result.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Result.d.ts","sourceRoot":"","sources":["../src/Result.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,KAAK,EAAE,CAAC,CAAC,IAAI;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAA;CAAE,CAAC;AACtD,KAAK,GAAG,CAAC,CAAC,IAAI;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAA;CAAE,CAAC;AACxD,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,GAAG,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;AAE3C,iBAAS,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,EAAE,KAAK,CAAC,CAEzC;AAED,iBAAS,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAE1C;AAED,iBAAS,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,MAAM,IAAI,EAAE,CAAC,CAAC,CAAC,CAEzD;AAED,iBAAS,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAEnF;AAED,iBAAS,UAAU,CAAC,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAGzD;AAED,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC;AAChD,YAAY,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC"}
|
package/dist/Result.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
function ok(value) {
|
|
2
|
+
return { ok: true, value };
|
|
3
|
+
}
|
|
4
|
+
function err(error) {
|
|
5
|
+
return { ok: false, error };
|
|
6
|
+
}
|
|
7
|
+
function isOk(result) {
|
|
8
|
+
return result.ok;
|
|
9
|
+
}
|
|
10
|
+
function mapResult(result, fn) {
|
|
11
|
+
return result.ok ? ok(fn(result.value)) : result;
|
|
12
|
+
}
|
|
13
|
+
function getOrThrow(result) {
|
|
14
|
+
if (!result.ok)
|
|
15
|
+
throw result.error;
|
|
16
|
+
return result.value;
|
|
17
|
+
}
|
|
18
|
+
export { err, getOrThrow, isOk, mapResult, ok };
|
|
19
|
+
//# sourceMappingURL=Result.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Result.js","sourceRoot":"","sources":["../src/Result.ts"],"names":[],"mappings":"AAmBA,SAAS,EAAE,CAAI,KAAQ;IACrB,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AAC7B,CAAC;AAED,SAAS,GAAG,CAAI,KAAQ;IACtB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAC9B,CAAC;AAED,SAAS,IAAI,CAAO,MAAoB;IACtC,OAAO,MAAM,CAAC,EAAE,CAAC;AACnB,CAAC;AAED,SAAS,SAAS,CAAU,MAAoB,EAAE,EAAmB;IACnE,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;AACnD,CAAC;AAED,SAAS,UAAU,CAAe,MAAoB;IACpD,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,MAAM,MAAM,CAAC,KAAK,CAAC;IACnC,OAAO,MAAM,CAAC,KAAK,CAAC;AACtB,CAAC;AAED,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC"}
|