@actuate-media/cms-core 0.11.2 → 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/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
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { encryptField, decryptField, InvalidEncryptionKeyError, } from '../../security/encrypted-fields.js';
|
|
3
|
+
const VALID_KEY = 'a'.repeat(64); // 64 hex chars = 32 bytes
|
|
4
|
+
const ALT_KEY = 'b'.repeat(64);
|
|
5
|
+
describe('encrypted-fields key validation', () => {
|
|
6
|
+
it('rejects empty key', async () => {
|
|
7
|
+
await expect(encryptField('hello', '')).rejects.toBeInstanceOf(InvalidEncryptionKeyError);
|
|
8
|
+
});
|
|
9
|
+
it('rejects undefined / non-string key', async () => {
|
|
10
|
+
await expect(encryptField('hello', undefined)).rejects.toBeInstanceOf(InvalidEncryptionKeyError);
|
|
11
|
+
});
|
|
12
|
+
it('rejects too-short key', async () => {
|
|
13
|
+
await expect(encryptField('hello', 'a'.repeat(32))).rejects.toBeInstanceOf(InvalidEncryptionKeyError);
|
|
14
|
+
});
|
|
15
|
+
it('rejects too-long key', async () => {
|
|
16
|
+
await expect(encryptField('hello', 'a'.repeat(128))).rejects.toBeInstanceOf(InvalidEncryptionKeyError);
|
|
17
|
+
});
|
|
18
|
+
it('rejects non-hex characters', async () => {
|
|
19
|
+
await expect(encryptField('hello', 'z'.repeat(63) + 'a')).rejects.toBeInstanceOf(InvalidEncryptionKeyError);
|
|
20
|
+
});
|
|
21
|
+
it('rejects the placeholder dev key from the .env example', async () => {
|
|
22
|
+
// "aes256-local-dev-key-change-in-prod" is 35 chars, not 64, and contains '-'.
|
|
23
|
+
await expect(encryptField('hello', 'aes256-local-dev-key-change-in-prod')).rejects.toBeInstanceOf(InvalidEncryptionKeyError);
|
|
24
|
+
});
|
|
25
|
+
it('error message points operators at the random-bytes generator', async () => {
|
|
26
|
+
try {
|
|
27
|
+
await encryptField('hello', 'short');
|
|
28
|
+
throw new Error('should have thrown');
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
expect(err).toBeInstanceOf(InvalidEncryptionKeyError);
|
|
32
|
+
expect(err.message).toContain('randomBytes(32)');
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe('encrypted-fields round-trip', () => {
|
|
37
|
+
it('encrypts and decrypts with the same key', async () => {
|
|
38
|
+
const plaintext = 'hello, world — with unicode ✓';
|
|
39
|
+
const encrypted = await encryptField(plaintext, VALID_KEY);
|
|
40
|
+
expect(encrypted).not.toBe(plaintext);
|
|
41
|
+
const decrypted = await decryptField(encrypted, VALID_KEY);
|
|
42
|
+
expect(decrypted).toBe(plaintext);
|
|
43
|
+
});
|
|
44
|
+
it('produces different ciphertext on every call (unique IV)', async () => {
|
|
45
|
+
const a = await encryptField('same input', VALID_KEY);
|
|
46
|
+
const b = await encryptField('same input', VALID_KEY);
|
|
47
|
+
expect(a).not.toBe(b);
|
|
48
|
+
});
|
|
49
|
+
it('rejects ciphertext encrypted with a different key', async () => {
|
|
50
|
+
const encrypted = await encryptField('secret', VALID_KEY);
|
|
51
|
+
await expect(decryptField(encrypted, ALT_KEY)).rejects.toThrow();
|
|
52
|
+
});
|
|
53
|
+
it('rejects tampered ciphertext (AES-GCM auth tag enforces integrity)', async () => {
|
|
54
|
+
const encrypted = await encryptField('secret', VALID_KEY);
|
|
55
|
+
// Flip one byte at the end (inside the auth tag region)
|
|
56
|
+
const tampered = encrypted.slice(0, -2) + (encrypted.slice(-2) === 'ff' ? '00' : 'ff');
|
|
57
|
+
await expect(decryptField(tampered, VALID_KEY)).rejects.toThrow();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
//# sourceMappingURL=encrypted-fields.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encrypted-fields.test.js","sourceRoot":"","sources":["../../../src/__tests__/security/encrypted-fields.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-fetch.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/security/safe-fetch.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { safeFetch, SsrfBlockedError } from '../../security/safe-fetch.js';
|
|
3
|
+
describe('safeFetch — DNS rebinding defense', () => {
|
|
4
|
+
let originalFetch;
|
|
5
|
+
let mockFetch;
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
originalFetch = globalThis.fetch;
|
|
8
|
+
mockFetch = vi.fn();
|
|
9
|
+
globalThis.fetch = mockFetch;
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
globalThis.fetch = originalFetch;
|
|
13
|
+
});
|
|
14
|
+
it('proceeds with public IP when DNS resolves to a public address', async () => {
|
|
15
|
+
mockFetch.mockResolvedValueOnce(new Response('ok', { status: 200 }));
|
|
16
|
+
const resolver = vi.fn().mockResolvedValue({ safe: true, resolvedIp: '8.8.8.8' });
|
|
17
|
+
const res = await safeFetch('https://example.com/x', { _resolver: resolver });
|
|
18
|
+
expect(res.status).toBe(200);
|
|
19
|
+
expect(resolver).toHaveBeenCalledWith('example.com');
|
|
20
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
21
|
+
});
|
|
22
|
+
it('rejects when DNS resolves to a private IP (DNS rebinding defeated)', async () => {
|
|
23
|
+
const resolver = vi.fn().mockResolvedValue({
|
|
24
|
+
safe: false,
|
|
25
|
+
resolvedIp: '127.0.0.1',
|
|
26
|
+
error: 'Resolved IP 127.0.0.1 is in a private range',
|
|
27
|
+
});
|
|
28
|
+
await expect(safeFetch('https://attacker-controlled.tld/probe', { _resolver: resolver })).rejects.toBeInstanceOf(SsrfBlockedError);
|
|
29
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
30
|
+
});
|
|
31
|
+
it('rejects when DNS resolves to AWS metadata IP', async () => {
|
|
32
|
+
const resolver = vi.fn().mockResolvedValue({
|
|
33
|
+
safe: false,
|
|
34
|
+
resolvedIp: '169.254.169.254',
|
|
35
|
+
error: 'Resolved IP 169.254.169.254 is in a private range',
|
|
36
|
+
});
|
|
37
|
+
await expect(safeFetch('https://looks-public.com/', { _resolver: resolver })).rejects.toBeInstanceOf(SsrfBlockedError);
|
|
38
|
+
});
|
|
39
|
+
it('best-effort mode: proceeds when resolver throws (default)', async () => {
|
|
40
|
+
mockFetch.mockResolvedValueOnce(new Response('ok'));
|
|
41
|
+
const resolver = vi.fn().mockRejectedValue(new Error('DNS module unavailable'));
|
|
42
|
+
const res = await safeFetch('https://example.com/', { _resolver: resolver });
|
|
43
|
+
expect(res.status).toBe(200);
|
|
44
|
+
});
|
|
45
|
+
it('strict mode: rejects when resolver throws and requireDnsCheck=true', async () => {
|
|
46
|
+
const resolver = vi.fn().mockRejectedValue(new Error('DNS module unavailable'));
|
|
47
|
+
await expect(safeFetch('https://example.com/', {
|
|
48
|
+
_resolver: resolver,
|
|
49
|
+
requireDnsCheck: true,
|
|
50
|
+
})).rejects.toBeInstanceOf(SsrfBlockedError);
|
|
51
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
it('rejects URL-string bypass forms before any DNS lookup', async () => {
|
|
54
|
+
const resolver = vi.fn();
|
|
55
|
+
await expect(safeFetch('http://2130706433/', { _resolver: resolver })).rejects.toBeInstanceOf(SsrfBlockedError);
|
|
56
|
+
await expect(safeFetch('http://[::ffff:127.0.0.1]/', { _resolver: resolver })).rejects.toBeInstanceOf(SsrfBlockedError);
|
|
57
|
+
expect(resolver).not.toHaveBeenCalled();
|
|
58
|
+
});
|
|
59
|
+
it('re-validates AND re-resolves every redirect hop', async () => {
|
|
60
|
+
// Hop 1: example.com (public) -> 302 to evil.com
|
|
61
|
+
mockFetch.mockResolvedValueOnce(new Response(null, { status: 302, headers: { Location: 'https://evil.com/inner' } }));
|
|
62
|
+
// Hop 2 should never be issued because resolver flags evil.com as private.
|
|
63
|
+
const resolver = vi
|
|
64
|
+
.fn()
|
|
65
|
+
.mockImplementationOnce(async () => ({ safe: true, resolvedIp: '8.8.8.8' }))
|
|
66
|
+
.mockImplementationOnce(async () => ({
|
|
67
|
+
safe: false,
|
|
68
|
+
resolvedIp: '10.0.0.5',
|
|
69
|
+
error: 'private',
|
|
70
|
+
}));
|
|
71
|
+
await expect(safeFetch('https://example.com/start', {
|
|
72
|
+
_resolver: resolver,
|
|
73
|
+
followRedirects: true,
|
|
74
|
+
})).rejects.toBeInstanceOf(SsrfBlockedError);
|
|
75
|
+
expect(resolver).toHaveBeenCalledTimes(2);
|
|
76
|
+
expect(mockFetch).toHaveBeenCalledTimes(1); // only the first hop went out
|
|
77
|
+
});
|
|
78
|
+
it('default (no follow) returns the redirect response unmodified', async () => {
|
|
79
|
+
mockFetch.mockResolvedValueOnce(new Response(null, { status: 302, headers: { Location: 'https://elsewhere.com/' } }));
|
|
80
|
+
const resolver = vi.fn().mockResolvedValue({ safe: true, resolvedIp: '8.8.8.8' });
|
|
81
|
+
const res = await safeFetch('https://example.com/', { _resolver: resolver });
|
|
82
|
+
expect(res.status).toBe(302);
|
|
83
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
84
|
+
// No re-resolve because we didn't follow the redirect.
|
|
85
|
+
expect(resolver).toHaveBeenCalledTimes(1);
|
|
86
|
+
});
|
|
87
|
+
it('honors maxRedirects bound', async () => {
|
|
88
|
+
mockFetch.mockResolvedValue(new Response(null, { status: 302, headers: { Location: 'https://example.com/loop' } }));
|
|
89
|
+
const resolver = vi.fn().mockResolvedValue({ safe: true, resolvedIp: '8.8.8.8' });
|
|
90
|
+
await expect(safeFetch('https://example.com/start', {
|
|
91
|
+
_resolver: resolver,
|
|
92
|
+
followRedirects: true,
|
|
93
|
+
maxRedirects: 2,
|
|
94
|
+
})).rejects.toBeInstanceOf(SsrfBlockedError);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
//# sourceMappingURL=safe-fetch.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-fetch.test.js","sourceRoot":"","sources":["../../../src/__tests__/security/safe-fetch.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssrf.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/security/ssrf.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { canonicalizeHostname, isPrivateAddress } from '../../security/ip-canon.js';
|
|
3
|
+
import { resolveAndCheck, validateWebhookUrl } from '../../security/webhook.js';
|
|
4
|
+
describe('canonicalizeHostname — IPv4 encodings', () => {
|
|
5
|
+
it.each([
|
|
6
|
+
['127.0.0.1', '127.0.0.1'],
|
|
7
|
+
['10.0.0.1', '10.0.0.1'],
|
|
8
|
+
['192.168.1.1', '192.168.1.1'],
|
|
9
|
+
// Decimal: 127*2^24 = 2130706432 → 127.0.0.0; +1 = 127.0.0.1
|
|
10
|
+
['2130706433', '127.0.0.1'],
|
|
11
|
+
// Octal (0177 = 127, 0 = 0, 0 = 0, 01 = 1)
|
|
12
|
+
['0177.0.0.1', '127.0.0.1'],
|
|
13
|
+
['0177.0.0.01', '127.0.0.1'],
|
|
14
|
+
// Hex
|
|
15
|
+
['0x7f.0.0.1', '127.0.0.1'],
|
|
16
|
+
['0x7f000001', '127.0.0.1'],
|
|
17
|
+
// Short forms
|
|
18
|
+
['127.1', '127.0.0.1'],
|
|
19
|
+
['10.1', '10.0.0.1'],
|
|
20
|
+
['127.0.1', '127.0.0.1'],
|
|
21
|
+
])('canonicalizes %s → %s', (input, expected) => {
|
|
22
|
+
const c = canonicalizeHostname(input);
|
|
23
|
+
expect(c.isHostname).toBe(false);
|
|
24
|
+
expect(c.ipv4).toBe(expected);
|
|
25
|
+
});
|
|
26
|
+
it('rejects malformed IPv4 (out-of-range octet)', () => {
|
|
27
|
+
const c = canonicalizeHostname('256.0.0.1');
|
|
28
|
+
expect(c.isHostname).toBe(true); // falls through to "treat as hostname"
|
|
29
|
+
});
|
|
30
|
+
it('rejects malformed octal (digit 8 is invalid)', () => {
|
|
31
|
+
const c = canonicalizeHostname('0888.0.0.1');
|
|
32
|
+
expect(c.isHostname).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
describe('canonicalizeHostname — IPv6 encodings', () => {
|
|
36
|
+
it.each([
|
|
37
|
+
['::1', '::ffff:127.0.0.1 OR plain'], // checked separately below
|
|
38
|
+
])('detects IPv6 forms', () => {
|
|
39
|
+
expect(canonicalizeHostname('::1').isHostname).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
it('canonicalizes ::ffff:127.0.0.1 to underlying IPv4', () => {
|
|
42
|
+
const c = canonicalizeHostname('::ffff:127.0.0.1');
|
|
43
|
+
expect(c.ipv4).toBe('127.0.0.1');
|
|
44
|
+
});
|
|
45
|
+
it('canonicalizes ::ffff:7f00:1 (hex form) to underlying IPv4', () => {
|
|
46
|
+
const c = canonicalizeHostname('::ffff:7f00:1');
|
|
47
|
+
expect(c.ipv4).toBe('127.0.0.1');
|
|
48
|
+
});
|
|
49
|
+
it('canonicalizes ::127.0.0.1 (deprecated IPv4-compatible) to underlying IPv4', () => {
|
|
50
|
+
const c = canonicalizeHostname('::127.0.0.1');
|
|
51
|
+
expect(c.ipv4).toBe('127.0.0.1');
|
|
52
|
+
});
|
|
53
|
+
it('handles bracketed IPv6 (URL form)', () => {
|
|
54
|
+
const c = canonicalizeHostname('[::1]');
|
|
55
|
+
expect(c.ipv6).toBeDefined();
|
|
56
|
+
});
|
|
57
|
+
it('rejects IPv6 with multiple :: compressions', () => {
|
|
58
|
+
const c = canonicalizeHostname('::1::1');
|
|
59
|
+
expect(c.isHostname).toBe(false);
|
|
60
|
+
expect(c.isValidIp).toBe(false); // <- Bugbot #3: explicit malformed flag
|
|
61
|
+
expect(c.ipv6).toBeUndefined();
|
|
62
|
+
expect(c.ipv4).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe('canonicalizeHostname — malformed IP literals', () => {
|
|
66
|
+
// Bugbot review #1/#2/#3 (PR #40): every input that looks like an IP
|
|
67
|
+
// literal but doesn't parse must be flagged with `isValidIp: false` so
|
|
68
|
+
// SSRF gates can fail closed instead of misreading "no IP returned" as
|
|
69
|
+
// "no private IP found".
|
|
70
|
+
it.each([
|
|
71
|
+
'::1::1', // multiple :: compressions
|
|
72
|
+
'::garbage', // non-hex hextet
|
|
73
|
+
'fe80:::1', // triple colon
|
|
74
|
+
'::1:gg::', // bad chars + multiple ::
|
|
75
|
+
'1:2:3:4:5:6:7:8:9', // too many groups
|
|
76
|
+
'abcd::1234::5678', // multiple ::
|
|
77
|
+
])('marks %s as a non-hostname AND non-valid IP', (input) => {
|
|
78
|
+
const c = canonicalizeHostname(input);
|
|
79
|
+
expect(c.isHostname).toBe(false);
|
|
80
|
+
expect(c.isValidIp).toBe(false);
|
|
81
|
+
expect(c.ipv4).toBeUndefined();
|
|
82
|
+
expect(c.ipv6).toBeUndefined();
|
|
83
|
+
});
|
|
84
|
+
it('isPrivateAddress fails closed on the malformed shape', () => {
|
|
85
|
+
// Defense in depth: even if a caller forgets to check `isValidIp`,
|
|
86
|
+
// calling `isPrivateAddress` on a malformed shape returns a non-null
|
|
87
|
+
// reason instead of `null` (which would mean "safe to fetch").
|
|
88
|
+
const c = canonicalizeHostname('::1::1');
|
|
89
|
+
const result = isPrivateAddress(c);
|
|
90
|
+
expect(result).not.toBeNull();
|
|
91
|
+
expect(result?.reason).toMatch(/malformed/i);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
describe('canonicalizeHostname — DNS hostnames pass through', () => {
|
|
95
|
+
it.each(['example.com', 'sub.example.com', 'localhost-but-not-quite.com', 'a-b-c.example'])('leaves %s as a hostname', (input) => {
|
|
96
|
+
const c = canonicalizeHostname(input);
|
|
97
|
+
expect(c.isHostname).toBe(true);
|
|
98
|
+
expect(c.ipv4).toBeUndefined();
|
|
99
|
+
expect(c.ipv6).toBeUndefined();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe('isPrivateAddress', () => {
|
|
103
|
+
it.each([
|
|
104
|
+
'127.0.0.1',
|
|
105
|
+
'10.0.0.1',
|
|
106
|
+
'172.16.0.1',
|
|
107
|
+
'172.31.255.254',
|
|
108
|
+
'192.168.1.1',
|
|
109
|
+
'169.254.169.254', // AWS metadata
|
|
110
|
+
'100.64.0.1', // CGNAT
|
|
111
|
+
'0.0.0.0',
|
|
112
|
+
'198.18.0.1', // benchmark range
|
|
113
|
+
'224.0.0.1', // multicast
|
|
114
|
+
'255.255.255.255', // broadcast
|
|
115
|
+
])('flags %s as private', (ip) => {
|
|
116
|
+
const c = canonicalizeHostname(ip);
|
|
117
|
+
expect(isPrivateAddress(c)).not.toBeNull();
|
|
118
|
+
});
|
|
119
|
+
it.each(['8.8.8.8', '1.1.1.1', '93.184.216.34', '203.0.0.1'])('allows public IP %s', (ip) => {
|
|
120
|
+
const c = canonicalizeHostname(ip);
|
|
121
|
+
expect(isPrivateAddress(c)).toBeNull();
|
|
122
|
+
});
|
|
123
|
+
it.each([
|
|
124
|
+
['::1', '::1 (IPv6 loopback)'],
|
|
125
|
+
['::', ':: (unspecified)'],
|
|
126
|
+
['fe80::1', 'fe80::/10 (IPv6 link-local)'],
|
|
127
|
+
['fc00::1', 'fc00::/7 (IPv6 unique local)'],
|
|
128
|
+
['fd12::1', 'fc00::/7 (IPv6 unique local)'],
|
|
129
|
+
['ff02::1', 'ff00::/8 (IPv6 multicast)'],
|
|
130
|
+
])('flags IPv6 %s as private', (ip) => {
|
|
131
|
+
const c = canonicalizeHostname(ip);
|
|
132
|
+
expect(isPrivateAddress(c)).not.toBeNull();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
describe('validateWebhookUrl — bypass forms', () => {
|
|
136
|
+
// These are the bypass forms the prior regex-based implementation missed.
|
|
137
|
+
// Each one would have happily POSTed to localhost / metadata IP / RFC1918.
|
|
138
|
+
it.each([
|
|
139
|
+
['decimal IP', 'http://2130706433/'],
|
|
140
|
+
['octal IP', 'http://0177.0.0.1/'],
|
|
141
|
+
['hex IP', 'http://0x7f.0.0.1/'],
|
|
142
|
+
['mixed hex flat', 'http://0x7f000001/'],
|
|
143
|
+
['short form', 'http://127.1/'],
|
|
144
|
+
['IPv4-mapped IPv6', 'http://[::ffff:127.0.0.1]/'],
|
|
145
|
+
['IPv4-compatible IPv6 (deprecated)', 'http://[::127.0.0.1]/'],
|
|
146
|
+
['bracketed IPv6 loopback', 'http://[::1]/'],
|
|
147
|
+
['IPv6 link-local', 'http://[fe80::1]/'],
|
|
148
|
+
['IPv6 unique-local fd00', 'http://[fd00::1]/'],
|
|
149
|
+
['CGNAT range', 'http://100.64.0.1/'],
|
|
150
|
+
['AWS metadata', 'http://169.254.169.254/latest/meta-data/'],
|
|
151
|
+
['benchmark range', 'http://198.18.0.1/'],
|
|
152
|
+
['localhost hostname', 'http://localhost/'],
|
|
153
|
+
['metadata.google.internal', 'http://metadata.google.internal/'],
|
|
154
|
+
['arbitrary .internal hostname', 'http://my-service.internal/'],
|
|
155
|
+
])('rejects %s (%s)', (_label, url) => {
|
|
156
|
+
const result = validateWebhookUrl(url);
|
|
157
|
+
expect(result.valid).toBe(false);
|
|
158
|
+
expect(result.error).toBeDefined();
|
|
159
|
+
});
|
|
160
|
+
it.each([
|
|
161
|
+
['regular https', 'https://example.com/webhook'],
|
|
162
|
+
['public IP literal', 'http://8.8.8.8/'],
|
|
163
|
+
['hostname with port', 'https://api.example.com:8443/hooks/123'],
|
|
164
|
+
])('allows %s (%s)', (_label, url) => {
|
|
165
|
+
const result = validateWebhookUrl(url);
|
|
166
|
+
expect(result.valid).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
it('rejects non-http(s) protocols', () => {
|
|
169
|
+
expect(validateWebhookUrl('file:///etc/passwd').valid).toBe(false);
|
|
170
|
+
expect(validateWebhookUrl('gopher://example.com/').valid).toBe(false);
|
|
171
|
+
expect(validateWebhookUrl('ftp://example.com/').valid).toBe(false);
|
|
172
|
+
expect(validateWebhookUrl('javascript:alert(1)').valid).toBe(false);
|
|
173
|
+
});
|
|
174
|
+
it('rejects malformed URLs', () => {
|
|
175
|
+
expect(validateWebhookUrl('not a url').valid).toBe(false);
|
|
176
|
+
expect(validateWebhookUrl('').valid).toBe(false);
|
|
177
|
+
});
|
|
178
|
+
// Bugbot review #1 (PR #40): malformed IP-shaped hostnames previously
|
|
179
|
+
// slipped through because canonicalizeHostname returned `{isHostname:
|
|
180
|
+
// false}` with no IP fields, and isPrivateAddress returned null —
|
|
181
|
+
// which the validator interpreted as "safe". They must fail closed.
|
|
182
|
+
it.each([
|
|
183
|
+
'http://[::1::1]/', // multi :: IPv6
|
|
184
|
+
'http://[fe80:::1]/', // triple colon
|
|
185
|
+
'http://[abcd::1234::5678]/', // multi ::
|
|
186
|
+
])('rejects malformed IP literal in URL: %s', (url) => {
|
|
187
|
+
const result = validateWebhookUrl(url);
|
|
188
|
+
expect(result.valid).toBe(false);
|
|
189
|
+
expect(result.error).toMatch(/malformed|invalid/i);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
describe('resolveAndCheck — IP literal short-circuit', () => {
|
|
193
|
+
it('rejects malformed IPv6 literal without DNS lookup', async () => {
|
|
194
|
+
// Bugbot review #2 (PR #40): the IP-literal short-circuit path also
|
|
195
|
+
// had the malformed-IP fail-open bug. Verify it now fails closed.
|
|
196
|
+
const result = await resolveAndCheck('::1::1');
|
|
197
|
+
expect(result.safe).toBe(false);
|
|
198
|
+
expect(result.error).toMatch(/malformed/i);
|
|
199
|
+
});
|
|
200
|
+
it('still allows public IP literals', async () => {
|
|
201
|
+
const result = await resolveAndCheck('8.8.8.8');
|
|
202
|
+
expect(result.safe).toBe(true);
|
|
203
|
+
});
|
|
204
|
+
it('still rejects private IP literals', async () => {
|
|
205
|
+
const result = await resolveAndCheck('127.0.0.1');
|
|
206
|
+
expect(result.safe).toBe(false);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
//# sourceMappingURL=ssrf.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssrf.test.js","sourceRoot":"","sources":["../../../src/__tests__/security/ssrf.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handler-factory.d.ts","sourceRoot":"","sources":["../../src/api/handler-factory.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"handler-factory.d.ts","sourceRoot":"","sources":["../../src/api/handler-factory.ts"],"names":[],"mappings":"AAyDA,MAAM,WAAW,uBAAuB;IACtC,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,kBAAkB,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;CAC5C;AAMD,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,uBAA4B,IAkFtC,SAAS,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAgHnE"}
|
|
@@ -22,6 +22,8 @@ import { setStorageAdapter } from '../storage/index.js';
|
|
|
22
22
|
* /auth/oauth/... — OAuth init + callback redirects
|
|
23
23
|
* /setup/create-admin — only reachable on a fresh install
|
|
24
24
|
* /forms/:id/submit — public form submissions (regex below)
|
|
25
|
+
* /cron/... — Vercel Cron / external schedulers; auth is via
|
|
26
|
+
* Authorization: Bearer ${CRON_SECRET} instead
|
|
25
27
|
*/
|
|
26
28
|
const CSRF_EXEMPT_PATHS = [
|
|
27
29
|
'/auth/login',
|
|
@@ -31,6 +33,7 @@ const CSRF_EXEMPT_PATHS = [
|
|
|
31
33
|
'/auth/reset-password',
|
|
32
34
|
'/auth/oauth/',
|
|
33
35
|
'/setup/create-admin',
|
|
36
|
+
'/cron/',
|
|
34
37
|
];
|
|
35
38
|
function isCsrfExemptPath(pathname) {
|
|
36
39
|
if (CSRF_EXEMPT_PATHS.some((p) => pathname === p ||
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handler-factory.js","sourceRoot":"","sources":["../../src/api/handler-factory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AACpE,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAEvD
|
|
1
|
+
{"version":3,"file":"handler-factory.js","sourceRoot":"","sources":["../../src/api/handler-factory.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/api/handlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/api/handlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAyW5C,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAS9E;AAkKD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAu6IzD"}
|
package/dist/api/handlers.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { listDocuments, getDocument, createDocument, updateDocument, deleteDocument, getGlobal, updateGlobal, } from '../actions.js';
|
|
2
|
-
import { verifyPassword } from '../auth/password.js';
|
|
2
|
+
import { verifyPassword, hashPassword, needsRehash, compareToDummyHash } from '../auth/password.js';
|
|
3
3
|
import { createSession, verifySession, revokeSession } from '../auth/session.js';
|
|
4
4
|
import { createPasswordReset, executePasswordReset } from '../auth/reset.js';
|
|
5
5
|
import { checkSetupRequired, createInitialAdmin } from '../setup/index.js';
|
|
@@ -12,6 +12,7 @@ import { logEvent } from '../security/audit.js';
|
|
|
12
12
|
import { applyFieldAccess } from '../security/access.js';
|
|
13
13
|
import { createPreviewAdapter } from '../preview/index.js';
|
|
14
14
|
import { schedulingCronHandler } from '../scheduling/index.js';
|
|
15
|
+
import { isAuthorizedCronRequest, processCleanup, processSeoScan } from '../cron/index.js';
|
|
15
16
|
import { verifyCaptcha, getCaptchaConfig } from '../security/captcha.js';
|
|
16
17
|
import { checkForUpdates } from '../upgrade/version-check.js';
|
|
17
18
|
import { createUpgradePR } from '../upgrade/upgrade-pr.js';
|
|
@@ -563,10 +564,18 @@ export function registerCMSRoutes(router) {
|
|
|
563
564
|
const user = await d.user.findFirst({
|
|
564
565
|
where: { email: email.toLowerCase().trim() },
|
|
565
566
|
});
|
|
567
|
+
// User-enumeration timing defense (H5): when the email doesn't exist,
|
|
568
|
+
// OR when the account is OAuth-only (no passwordHash), still spend
|
|
569
|
+
// the same ~100-200ms running PBKDF2 against a dummy hash before
|
|
570
|
+
// returning the error. Without this, attackers can distinguish
|
|
571
|
+
// "no such user" / "OAuth-only" / "wrong password" by the response
|
|
572
|
+
// delta, enabling targeted phishing & credential stuffing.
|
|
566
573
|
if (!user) {
|
|
574
|
+
await compareToDummyHash(password);
|
|
567
575
|
return errorResponse('Invalid email or password', 401);
|
|
568
576
|
}
|
|
569
577
|
if (!user.passwordHash) {
|
|
578
|
+
await compareToDummyHash(password);
|
|
570
579
|
return errorResponse('This account uses social login. Please sign in with your OAuth provider.', 400);
|
|
571
580
|
}
|
|
572
581
|
const passwordValid = await verifyPassword(password, user.passwordHash);
|
|
@@ -582,6 +591,20 @@ export function registerCMSRoutes(router) {
|
|
|
582
591
|
if (!user.isActive) {
|
|
583
592
|
return errorResponse('Account is deactivated', 403);
|
|
584
593
|
}
|
|
594
|
+
// H6: opportunistically upgrade stored hashes that use weaker
|
|
595
|
+
// PBKDF2 parameters than the current policy. We have the plaintext
|
|
596
|
+
// here (and we know it's correct) — re-hash and persist. Wrapped in
|
|
597
|
+
// try/catch so a transient DB failure never fails an otherwise-valid
|
|
598
|
+
// login; the user gets in, the upgrade just runs again next time.
|
|
599
|
+
if (needsRehash(user.passwordHash)) {
|
|
600
|
+
try {
|
|
601
|
+
const upgraded = await hashPassword(password);
|
|
602
|
+
await d.user.update({ where: { id: user.id }, data: { passwordHash: upgraded } });
|
|
603
|
+
}
|
|
604
|
+
catch (err) {
|
|
605
|
+
console.warn('[actuate][login] password rehash skipped:', err instanceof Error ? err.message : err);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
585
608
|
if (user.totpEnabled) {
|
|
586
609
|
// Hand back an opaque short-lived token instead of the raw userId.
|
|
587
610
|
// The /auth/totp/login endpoint will verify both this token and a
|
|
@@ -3472,6 +3495,66 @@ export function registerCMSRoutes(router) {
|
|
|
3472
3495
|
return internalError(err, 'scheduling run');
|
|
3473
3496
|
}
|
|
3474
3497
|
});
|
|
3498
|
+
// ---------------------------------------------------------------------------
|
|
3499
|
+
// Cron endpoints — auth via `Authorization: Bearer ${CRON_SECRET}`.
|
|
3500
|
+
//
|
|
3501
|
+
// **HTTP method:** registered as GET because Vercel Cron sends GET requests
|
|
3502
|
+
// (https://vercel.com/docs/cron-jobs). We also register POST aliases so
|
|
3503
|
+
// self-hosted schedulers that POST (k8s CronJob, GH Actions step running
|
|
3504
|
+
// `curl -X POST`, EventBridge HTTP target) keep working without changes.
|
|
3505
|
+
//
|
|
3506
|
+
// Vercel Cron sets the `Authorization: Bearer <CRON_SECRET>` header
|
|
3507
|
+
// automatically when `CRON_SECRET` is defined in the project environment.
|
|
3508
|
+
// When CRON_SECRET is unset, requests are rejected — fail-closed so a
|
|
3509
|
+
// misconfigured deploy never exposes these to the public.
|
|
3510
|
+
//
|
|
3511
|
+
// CSRF is NOT required: these endpoints have no session and the body carries
|
|
3512
|
+
// no admin intent — auth is exclusively via the Bearer token. They are also
|
|
3513
|
+
// listed in `CSRF_EXEMPT_PATHS` in handler-factory.ts so the global CSRF
|
|
3514
|
+
// gate doesn't block them.
|
|
3515
|
+
// ---------------------------------------------------------------------------
|
|
3516
|
+
const cronPublish = async (request) => {
|
|
3517
|
+
try {
|
|
3518
|
+
if (!isAuthorizedCronRequest(request.headers.get('authorization'))) {
|
|
3519
|
+
return errorResponse('Unauthorized', 401);
|
|
3520
|
+
}
|
|
3521
|
+
const result = await schedulingCronHandler(db());
|
|
3522
|
+
return json({ data: result });
|
|
3523
|
+
}
|
|
3524
|
+
catch (err) {
|
|
3525
|
+
return internalError(err, 'cron publish');
|
|
3526
|
+
}
|
|
3527
|
+
};
|
|
3528
|
+
router.get('/cron/publish', cronPublish);
|
|
3529
|
+
router.post('/cron/publish', cronPublish);
|
|
3530
|
+
const cronCleanup = async (request) => {
|
|
3531
|
+
try {
|
|
3532
|
+
if (!isAuthorizedCronRequest(request.headers.get('authorization'))) {
|
|
3533
|
+
return errorResponse('Unauthorized', 401);
|
|
3534
|
+
}
|
|
3535
|
+
const result = await processCleanup(db());
|
|
3536
|
+
return json({ data: result });
|
|
3537
|
+
}
|
|
3538
|
+
catch (err) {
|
|
3539
|
+
return internalError(err, 'cron cleanup');
|
|
3540
|
+
}
|
|
3541
|
+
};
|
|
3542
|
+
router.get('/cron/cleanup', cronCleanup);
|
|
3543
|
+
router.post('/cron/cleanup', cronCleanup);
|
|
3544
|
+
const cronSeoScan = async (request) => {
|
|
3545
|
+
try {
|
|
3546
|
+
if (!isAuthorizedCronRequest(request.headers.get('authorization'))) {
|
|
3547
|
+
return errorResponse('Unauthorized', 401);
|
|
3548
|
+
}
|
|
3549
|
+
const result = await processSeoScan(db());
|
|
3550
|
+
return json({ data: result });
|
|
3551
|
+
}
|
|
3552
|
+
catch (err) {
|
|
3553
|
+
return internalError(err, 'cron seo-scan');
|
|
3554
|
+
}
|
|
3555
|
+
};
|
|
3556
|
+
router.get('/cron/seo-scan', cronSeoScan);
|
|
3557
|
+
router.post('/cron/seo-scan', cronSeoScan);
|
|
3475
3558
|
router.get('/scheduling/calendar', async (request) => {
|
|
3476
3559
|
try {
|
|
3477
3560
|
const auth = await requireAuth(request);
|