@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.
Files changed (67) hide show
  1. package/dist/__tests__/api/cron-routes.test.d.ts +2 -0
  2. package/dist/__tests__/api/cron-routes.test.d.ts.map +1 -0
  3. package/dist/__tests__/api/cron-routes.test.js +67 -0
  4. package/dist/__tests__/api/cron-routes.test.js.map +1 -0
  5. package/dist/__tests__/auth/password.test.js +82 -3
  6. package/dist/__tests__/auth/password.test.js.map +1 -1
  7. package/dist/__tests__/auth/session.test.js +54 -1
  8. package/dist/__tests__/auth/session.test.js.map +1 -1
  9. package/dist/__tests__/cron/cron.test.d.ts +2 -0
  10. package/dist/__tests__/cron/cron.test.d.ts.map +1 -0
  11. package/dist/__tests__/cron/cron.test.js +262 -0
  12. package/dist/__tests__/cron/cron.test.js.map +1 -0
  13. package/dist/__tests__/security/encrypted-fields.test.d.ts +2 -0
  14. package/dist/__tests__/security/encrypted-fields.test.d.ts.map +1 -0
  15. package/dist/__tests__/security/encrypted-fields.test.js +60 -0
  16. package/dist/__tests__/security/encrypted-fields.test.js.map +1 -0
  17. package/dist/__tests__/security/safe-fetch.test.d.ts +2 -0
  18. package/dist/__tests__/security/safe-fetch.test.d.ts.map +1 -0
  19. package/dist/__tests__/security/safe-fetch.test.js +97 -0
  20. package/dist/__tests__/security/safe-fetch.test.js.map +1 -0
  21. package/dist/__tests__/security/ssrf.test.d.ts +2 -0
  22. package/dist/__tests__/security/ssrf.test.d.ts.map +1 -0
  23. package/dist/__tests__/security/ssrf.test.js +209 -0
  24. package/dist/__tests__/security/ssrf.test.js.map +1 -0
  25. package/dist/api/handler-factory.d.ts.map +1 -1
  26. package/dist/api/handler-factory.js +3 -0
  27. package/dist/api/handler-factory.js.map +1 -1
  28. package/dist/api/handlers.d.ts.map +1 -1
  29. package/dist/api/handlers.js +84 -1
  30. package/dist/api/handlers.js.map +1 -1
  31. package/dist/auth/oauth.d.ts +8 -0
  32. package/dist/auth/oauth.d.ts.map +1 -1
  33. package/dist/auth/oauth.js +39 -1
  34. package/dist/auth/oauth.js.map +1 -1
  35. package/dist/auth/password.d.ts +35 -2
  36. package/dist/auth/password.d.ts.map +1 -1
  37. package/dist/auth/password.js +97 -7
  38. package/dist/auth/password.js.map +1 -1
  39. package/dist/auth/session.d.ts +9 -0
  40. package/dist/auth/session.d.ts.map +1 -1
  41. package/dist/auth/session.js +54 -1
  42. package/dist/auth/session.js.map +1 -1
  43. package/dist/cron/index.d.ts +72 -0
  44. package/dist/cron/index.d.ts.map +1 -0
  45. package/dist/cron/index.js +222 -0
  46. package/dist/cron/index.js.map +1 -0
  47. package/dist/security/encrypted-fields.d.ts +9 -0
  48. package/dist/security/encrypted-fields.d.ts.map +1 -1
  49. package/dist/security/encrypted-fields.js +52 -1
  50. package/dist/security/encrypted-fields.js.map +1 -1
  51. package/dist/security/ip-canon.d.ts +71 -0
  52. package/dist/security/ip-canon.d.ts.map +1 -0
  53. package/dist/security/ip-canon.js +352 -0
  54. package/dist/security/ip-canon.js.map +1 -0
  55. package/dist/security/rate-limit.d.ts +0 -4
  56. package/dist/security/rate-limit.d.ts.map +1 -1
  57. package/dist/security/rate-limit.js +30 -0
  58. package/dist/security/rate-limit.js.map +1 -1
  59. package/dist/security/safe-fetch.d.ts +30 -8
  60. package/dist/security/safe-fetch.d.ts.map +1 -1
  61. package/dist/security/safe-fetch.js +32 -6
  62. package/dist/security/safe-fetch.js.map +1 -1
  63. package/dist/security/webhook.d.ts +20 -2
  64. package/dist/security/webhook.d.ts.map +1 -1
  65. package/dist/security/webhook.js +100 -30
  66. package/dist/security/webhook.js.map +1 -1
  67. package/package.json +1 -1
@@ -1,37 +1,90 @@
1
- const PRIVATE_RANGES = [
2
- /^10\./,
3
- /^172\.(1[6-9]|2\d|3[01])\./,
4
- /^192\.168\./,
5
- /^127\./,
6
- /^0\./,
7
- /^169\.254\./,
8
- /^::1$/,
9
- /^fc00:/i,
10
- /^fe80:/i,
11
- ];
12
- /** Validate that a webhook URL does not target private/internal networks (SSRF prevention). */
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
- const parsed = new URL(url);
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
- /** Resolve a hostname and verify the resulting IP isn't in a private range. */
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
- for (const range of PRIVATE_RANGES) {
56
- if (range.test(ip)) {
57
- return { safe: false, resolvedIp: ip, error: `Resolved IP ${ip} is in a private range` };
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,MAAM,cAAc,GAAG;IACrB,OAAO;IACP,4BAA4B;IAC5B,aAAa;IACb,QAAQ;IACR,MAAM;IACN,aAAa;IACb,OAAO;IACP,SAAS;IACT,SAAS;CACV,CAAA;AAED,+FAA+F;AAC/F,MAAM,UAAU,kBAAkB,CAAC,GAAW;IAC5C,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAA;QAE3B,IAAI,CAAC,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;YACnD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,oCAAoC,EAAE,CAAA;QACtE,CAAC;QAED,IAAI,MAAM,CAAC,QAAQ,KAAK,WAAW,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YACrE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,gCAAgC,EAAE,CAAA;QAClE,CAAC;QAED,KAAK,MAAM,KAAK,IAAI,cAAc,EAAE,CAAC;YACnC,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAChC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,+CAA+C,EAAE,CAAA;YACjF,CAAC;QACH,CAAC;QAED,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAA;IACxB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,CAAA;IAC/C,CAAC;AACH,CAAC;AAED,+EAA+E;AAC/E,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,QAAgB;IAEhB,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,CAAA;IAEhE,MAAM,GAAG,GAAa,EAAE,CAAA;IACxB,IAAI,CAAC;QACH,MAAM,EAAE,GAAG,MAAM,QAAQ,CAAC,QAAQ,CAAC,CAAA;QACnC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAA;IACjB,CAAC;IAAC,MAAM,CAAC;QACP,kBAAkB;IACpB,CAAC;IACD,IAAI,CAAC;QACH,MAAM,EAAE,GAAG,MAAM,QAAQ,CAAC,QAAQ,CAAC,CAAA;QACnC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAA;IACjB,CAAC;IAAC,MAAM,CAAC;QACP,qBAAqB;IACvB,CAAC;IAED,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrB,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,6BAA6B,QAAQ,EAAE,EAAE,CAAA;IACxE,CAAC;IAED,KAAK,MAAM,EAAE,IAAI,GAAG,EAAE,CAAC;QACrB,KAAK,MAAM,KAAK,IAAI,cAAc,EAAE,CAAC;YACnC,IAAI,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;gBACnB,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,wBAAwB,EAAE,CAAA;YAC1F,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,CAAA;AAC3C,CAAC"}
1
+ {"version":3,"file":"webhook.js","sourceRoot":"","sources":["../../src/security/webhook.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@actuate-media/cms-core",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/actuate-media/actuatecms.git",