@actuate-media/cms-core 0.11.1 → 0.12.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/dist/__tests__/api/cron-routes.test.d.ts +2 -0
- package/dist/__tests__/api/cron-routes.test.d.ts.map +1 -0
- package/dist/__tests__/api/cron-routes.test.js +67 -0
- package/dist/__tests__/api/cron-routes.test.js.map +1 -0
- package/dist/__tests__/auth/password.test.js +82 -3
- package/dist/__tests__/auth/password.test.js.map +1 -1
- package/dist/__tests__/auth/session.test.js +54 -1
- package/dist/__tests__/auth/session.test.js.map +1 -1
- package/dist/__tests__/cron/cron.test.d.ts +2 -0
- package/dist/__tests__/cron/cron.test.d.ts.map +1 -0
- package/dist/__tests__/cron/cron.test.js +262 -0
- package/dist/__tests__/cron/cron.test.js.map +1 -0
- package/dist/__tests__/security/encrypted-fields.test.d.ts +2 -0
- package/dist/__tests__/security/encrypted-fields.test.d.ts.map +1 -0
- package/dist/__tests__/security/encrypted-fields.test.js +60 -0
- package/dist/__tests__/security/encrypted-fields.test.js.map +1 -0
- package/dist/__tests__/security/safe-fetch.test.d.ts +2 -0
- package/dist/__tests__/security/safe-fetch.test.d.ts.map +1 -0
- package/dist/__tests__/security/safe-fetch.test.js +97 -0
- package/dist/__tests__/security/safe-fetch.test.js.map +1 -0
- package/dist/__tests__/security/ssrf.test.d.ts +2 -0
- package/dist/__tests__/security/ssrf.test.d.ts.map +1 -0
- package/dist/__tests__/security/ssrf.test.js +209 -0
- package/dist/__tests__/security/ssrf.test.js.map +1 -0
- package/dist/api/handler-factory.d.ts.map +1 -1
- package/dist/api/handler-factory.js +3 -0
- package/dist/api/handler-factory.js.map +1 -1
- package/dist/api/handlers.d.ts.map +1 -1
- package/dist/api/handlers.js +84 -1
- package/dist/api/handlers.js.map +1 -1
- package/dist/auth/oauth.d.ts +8 -0
- package/dist/auth/oauth.d.ts.map +1 -1
- package/dist/auth/oauth.js +39 -1
- package/dist/auth/oauth.js.map +1 -1
- package/dist/auth/password.d.ts +35 -2
- package/dist/auth/password.d.ts.map +1 -1
- package/dist/auth/password.js +97 -7
- package/dist/auth/password.js.map +1 -1
- package/dist/auth/session.d.ts +9 -0
- package/dist/auth/session.d.ts.map +1 -1
- package/dist/auth/session.js +54 -1
- package/dist/auth/session.js.map +1 -1
- package/dist/cron/index.d.ts +72 -0
- package/dist/cron/index.d.ts.map +1 -0
- package/dist/cron/index.js +222 -0
- package/dist/cron/index.js.map +1 -0
- package/dist/security/encrypted-fields.d.ts +9 -0
- package/dist/security/encrypted-fields.d.ts.map +1 -1
- package/dist/security/encrypted-fields.js +52 -1
- package/dist/security/encrypted-fields.js.map +1 -1
- package/dist/security/ip-canon.d.ts +71 -0
- package/dist/security/ip-canon.d.ts.map +1 -0
- package/dist/security/ip-canon.js +352 -0
- package/dist/security/ip-canon.js.map +1 -0
- package/dist/security/rate-limit.d.ts +0 -4
- package/dist/security/rate-limit.d.ts.map +1 -1
- package/dist/security/rate-limit.js +30 -0
- package/dist/security/rate-limit.js.map +1 -1
- package/dist/security/safe-fetch.d.ts +30 -8
- package/dist/security/safe-fetch.d.ts.map +1 -1
- package/dist/security/safe-fetch.js +32 -6
- package/dist/security/safe-fetch.js.map +1 -1
- package/dist/security/webhook.d.ts +20 -2
- package/dist/security/webhook.d.ts.map +1 -1
- package/dist/security/webhook.js +100 -30
- package/dist/security/webhook.js.map +1 -1
- package/package.json +1 -1
package/dist/security/webhook.js
CHANGED
|
@@ -1,37 +1,90 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
import { canonicalizeHostname, isPrivateAddress } from './ip-canon.js';
|
|
2
|
+
/**
|
|
3
|
+
* Validate that a webhook URL does not target private/internal networks
|
|
4
|
+
* (string-level SSRF prevention).
|
|
5
|
+
*
|
|
6
|
+
* Catches every IP-literal encoding `getaddrinfo` accepts: decimal
|
|
7
|
+
* (`http://2130706433`), octal (`http://0177.0.0.1`), hex
|
|
8
|
+
* (`http://0x7f.0.0.1`), short-form (`http://127.1`), IPv4-mapped IPv6
|
|
9
|
+
* (`http://[::ffff:127.0.0.1]`), bracketed IPv6, and the reserved
|
|
10
|
+
* `100.64.0.0/10` carrier-grade NAT range that previously slipped through.
|
|
11
|
+
*
|
|
12
|
+
* **Important caveat:** this only validates the URL **as written**. A hostname
|
|
13
|
+
* like `attacker-controlled.tld` that resolves to a public IP at validation
|
|
14
|
+
* time and to `127.0.0.1` at fetch time (DNS rebinding) still bypasses this
|
|
15
|
+
* check — `safeFetch` calls `resolveAndCheck()` on top to defend against that.
|
|
16
|
+
*/
|
|
13
17
|
export function validateWebhookUrl(url) {
|
|
18
|
+
let parsed;
|
|
14
19
|
try {
|
|
15
|
-
|
|
16
|
-
if (!['https:', 'http:'].includes(parsed.protocol)) {
|
|
17
|
-
return { valid: false, error: 'Only HTTP(S) protocols are allowed' };
|
|
18
|
-
}
|
|
19
|
-
if (parsed.hostname === 'localhost' || parsed.hostname === '0.0.0.0') {
|
|
20
|
-
return { valid: false, error: 'Localhost URLs are not allowed' };
|
|
21
|
-
}
|
|
22
|
-
for (const range of PRIVATE_RANGES) {
|
|
23
|
-
if (range.test(parsed.hostname)) {
|
|
24
|
-
return { valid: false, error: 'Private/internal IP addresses are not allowed' };
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return { valid: true };
|
|
20
|
+
parsed = new URL(url);
|
|
28
21
|
}
|
|
29
22
|
catch {
|
|
30
23
|
return { valid: false, error: 'Invalid URL' };
|
|
31
24
|
}
|
|
25
|
+
if (!['https:', 'http:'].includes(parsed.protocol)) {
|
|
26
|
+
return { valid: false, error: 'Only HTTP(S) protocols are allowed' };
|
|
27
|
+
}
|
|
28
|
+
// Special-case the well-known names — they're DNS hostnames so the
|
|
29
|
+
// canonicalizer can't catch them via numeric ranges.
|
|
30
|
+
const lowerHost = parsed.hostname.toLowerCase();
|
|
31
|
+
if (lowerHost === 'localhost' || lowerHost === 'ip6-localhost' || lowerHost === 'ip6-loopback') {
|
|
32
|
+
return { valid: false, error: 'Localhost hostnames are not allowed' };
|
|
33
|
+
}
|
|
34
|
+
// metadata.google.internal and AWS' internal DNS shadow real metadata IPs
|
|
35
|
+
// we already block — but the hostname can be redirected via /etc/hosts in
|
|
36
|
+
// self-hosted contexts, so block it explicitly.
|
|
37
|
+
if (lowerHost === 'metadata.google.internal' || lowerHost.endsWith('.internal')) {
|
|
38
|
+
return { valid: false, error: 'Cloud metadata hostnames are not allowed' };
|
|
39
|
+
}
|
|
40
|
+
const canonical = canonicalizeHostname(parsed.hostname);
|
|
41
|
+
if (!canonical.isHostname) {
|
|
42
|
+
// Three sub-cases for `!isHostname`:
|
|
43
|
+
// 1. Valid IP literal that's private → reject with the range reason.
|
|
44
|
+
// 2. Valid IP literal that's public → allow (fall through).
|
|
45
|
+
// 3. Malformed IP literal (e.g. `::1::1`, IPv6 with garbage) →
|
|
46
|
+
// `isValidIp` is false. Fail closed: the URL was clearly not a
|
|
47
|
+
// DNS hostname, so a `getaddrinfo` will reject it too — but
|
|
48
|
+
// worse, on some platforms it may resolve to *something*. Reject
|
|
49
|
+
// here rather than risk that ambiguity.
|
|
50
|
+
if (!canonical.isValidIp) {
|
|
51
|
+
return {
|
|
52
|
+
valid: false,
|
|
53
|
+
error: `Malformed IP literal in URL hostname: ${parsed.hostname}`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const denied = isPrivateAddress(canonical);
|
|
57
|
+
if (denied) {
|
|
58
|
+
return {
|
|
59
|
+
valid: false,
|
|
60
|
+
error: `Private/internal IP address rejected: ${denied.reason}`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { valid: true };
|
|
32
65
|
}
|
|
33
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* Resolve a hostname's A/AAAA records and verify none of them land in a
|
|
68
|
+
* private range. Pair with `validateWebhookUrl` to defend against DNS
|
|
69
|
+
* rebinding — see `safeFetch`.
|
|
70
|
+
*/
|
|
34
71
|
export async function resolveAndCheck(hostname) {
|
|
72
|
+
// If the hostname is already an IP literal, skip DNS and check directly.
|
|
73
|
+
const directCanonical = canonicalizeHostname(hostname);
|
|
74
|
+
if (!directCanonical.isHostname) {
|
|
75
|
+
// Malformed IP literal — treat as unsafe. Without this, a bad IPv6
|
|
76
|
+
// string (e.g. `::1::garbage`) would slip through the SSRF gate
|
|
77
|
+
// because `isPrivateAddress` legitimately returns null when there's
|
|
78
|
+
// no parsed IP to check.
|
|
79
|
+
if (!directCanonical.isValidIp) {
|
|
80
|
+
return { safe: false, resolvedIp: hostname, error: 'malformed IP literal' };
|
|
81
|
+
}
|
|
82
|
+
const denied = isPrivateAddress(directCanonical);
|
|
83
|
+
if (denied) {
|
|
84
|
+
return { safe: false, resolvedIp: hostname, error: denied.reason };
|
|
85
|
+
}
|
|
86
|
+
return { safe: true, resolvedIp: hostname };
|
|
87
|
+
}
|
|
35
88
|
const { resolve4, resolve6 } = await import('node:dns/promises');
|
|
36
89
|
const ips = [];
|
|
37
90
|
try {
|
|
@@ -51,11 +104,28 @@ export async function resolveAndCheck(hostname) {
|
|
|
51
104
|
if (ips.length === 0) {
|
|
52
105
|
return { safe: false, error: `DNS resolution failed for ${hostname}` };
|
|
53
106
|
}
|
|
107
|
+
// Check EVERY resolved IP, not just the first — a multi-A-record response
|
|
108
|
+
// could mix public + private IPs and we must reject if any single one is
|
|
109
|
+
// private to defeat round-robin DNS rebinding.
|
|
54
110
|
for (const ip of ips) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
111
|
+
const c = canonicalizeHostname(ip);
|
|
112
|
+
// DNS resolvers return canonical IP literals, so `isValidIp` should be
|
|
113
|
+
// true. If a resolver ever returns something we can't parse, treat it
|
|
114
|
+
// as unsafe rather than trusting it.
|
|
115
|
+
if (!c.isValidIp) {
|
|
116
|
+
return {
|
|
117
|
+
safe: false,
|
|
118
|
+
resolvedIp: ip,
|
|
119
|
+
error: `Unparseable IP returned from DNS for ${hostname}: ${ip}`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const denied = isPrivateAddress(c);
|
|
123
|
+
if (denied) {
|
|
124
|
+
return {
|
|
125
|
+
safe: false,
|
|
126
|
+
resolvedIp: ip,
|
|
127
|
+
error: `Resolved IP ${ip} is in a private range: ${denied.reason}`,
|
|
128
|
+
};
|
|
59
129
|
}
|
|
60
130
|
}
|
|
61
131
|
return { safe: true, resolvedIp: ips[0] };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhook.js","sourceRoot":"","sources":["../../src/security/webhook.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"webhook.js","sourceRoot":"","sources":["../../src/security/webhook.ts"],"names":[],"mappings":"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"}
|