@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
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cron handlers for scheduled platform jobs.
|
|
3
|
+
*
|
|
4
|
+
* The endpoints in `api/handlers.ts` validate `Authorization: Bearer ${CRON_SECRET}`
|
|
5
|
+
* before invoking these — Vercel Cron sends that header automatically when
|
|
6
|
+
* `CRON_SECRET` is defined in the project's environment. See:
|
|
7
|
+
* https://vercel.com/docs/cron-jobs/manage-cron-jobs#securing-cron-jobs
|
|
8
|
+
*
|
|
9
|
+
* Each handler is **idempotent** and **bounded** — safe to invoke from any
|
|
10
|
+
* scheduler (Vercel Cron, GitHub Actions, EventBridge, k8s CronJob, etc.) and
|
|
11
|
+
* safe to invoke twice in the same window.
|
|
12
|
+
*/
|
|
13
|
+
import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
14
|
+
import { schedulingCronHandler } from '../scheduling/index.js';
|
|
15
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
16
|
+
const DEFAULT_CLEANUP = {
|
|
17
|
+
sessionRetentionMs: 7 * DAY,
|
|
18
|
+
auditLogRetentionMs: 90 * DAY,
|
|
19
|
+
trashRetentionMs: 30 * DAY,
|
|
20
|
+
passwordResetRetentionMs: 1 * DAY,
|
|
21
|
+
};
|
|
22
|
+
function modelExists(db, name) {
|
|
23
|
+
if (db == null || typeof db !== 'object' || !(name in db))
|
|
24
|
+
return false;
|
|
25
|
+
const delegate = db[name];
|
|
26
|
+
// `typeof null === 'object'` in JavaScript, so we have to check `!== null`
|
|
27
|
+
// explicitly. Without this guard, `{ session: null }` would pass the check
|
|
28
|
+
// and `db.session.deleteMany(...)` would throw a TypeError that the outer
|
|
29
|
+
// try/catch only papers over.
|
|
30
|
+
return delegate !== null && typeof delegate === 'object';
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Delete stale rows from session, audit log, trash, and password-reset tables.
|
|
34
|
+
*
|
|
35
|
+
* Each deletion is wrapped in its own try/catch so a missing model (e.g. an
|
|
36
|
+
* older Prisma schema without `passwordResetToken`) doesn't fail the entire
|
|
37
|
+
* job — partial cleanup is still useful and the caller logs the count.
|
|
38
|
+
*/
|
|
39
|
+
export async function processCleanup(db, options = {}) {
|
|
40
|
+
const opts = { ...DEFAULT_CLEANUP, ...options };
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
const result = {
|
|
43
|
+
sessionsDeleted: 0,
|
|
44
|
+
auditLogsDeleted: 0,
|
|
45
|
+
documentsDeleted: 0,
|
|
46
|
+
passwordResetTokensDeleted: 0,
|
|
47
|
+
};
|
|
48
|
+
if (modelExists(db, 'session')) {
|
|
49
|
+
try {
|
|
50
|
+
const cutoff = new Date(now - opts.sessionRetentionMs);
|
|
51
|
+
const r = await db.session.deleteMany({
|
|
52
|
+
where: {
|
|
53
|
+
OR: [{ revokedAt: { lt: cutoff } }, { expiresAt: { lt: cutoff } }],
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
result.sessionsDeleted = r?.count ?? 0;
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
console.warn('[actuate][cron] session cleanup failed:', errMsg(err));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (modelExists(db, 'auditLog')) {
|
|
63
|
+
try {
|
|
64
|
+
const cutoff = new Date(now - opts.auditLogRetentionMs);
|
|
65
|
+
const r = await db.auditLog.deleteMany({ where: { timestamp: { lt: cutoff } } });
|
|
66
|
+
result.auditLogsDeleted = r?.count ?? 0;
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
console.warn('[actuate][cron] audit log cleanup failed:', errMsg(err));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (modelExists(db, 'document')) {
|
|
73
|
+
try {
|
|
74
|
+
const cutoff = new Date(now - opts.trashRetentionMs);
|
|
75
|
+
const r = await db.document.deleteMany({ where: { deletedAt: { lt: cutoff } } });
|
|
76
|
+
result.documentsDeleted = r?.count ?? 0;
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
console.warn('[actuate][cron] trash cleanup failed:', errMsg(err));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (modelExists(db, 'passwordResetToken')) {
|
|
83
|
+
try {
|
|
84
|
+
const cutoff = new Date(now - opts.passwordResetRetentionMs);
|
|
85
|
+
const r = await db.passwordResetToken.deleteMany({
|
|
86
|
+
where: {
|
|
87
|
+
OR: [{ usedAt: { lt: cutoff } }, { expiresAt: { lt: cutoff } }],
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
result.passwordResetTokensDeleted = r?.count ?? 0;
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
console.warn('[actuate][cron] password reset token cleanup failed:', errMsg(err));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Run the same SEO checks as `POST /seo/scan`, but headlessly so a cron
|
|
100
|
+
* (or a CLI / external scheduler) can invoke it without an admin session.
|
|
101
|
+
*
|
|
102
|
+
* Bounded by `maxDocuments` to avoid OOM / timeout on large catalogs —
|
|
103
|
+
* defaults to 5000, which is well above the typical Vercel function memory
|
|
104
|
+
* envelope while staying safely under the 60s cron execution limit.
|
|
105
|
+
*/
|
|
106
|
+
export async function processSeoScan(db, options = {}) {
|
|
107
|
+
const maxDocuments = options.maxDocuments ?? 5000;
|
|
108
|
+
if (!modelExists(db, 'document')) {
|
|
109
|
+
return { total: 0, pagesWithIssues: 0, totalProblems: 0, issues: [] };
|
|
110
|
+
}
|
|
111
|
+
const documents = await db.document.findMany({
|
|
112
|
+
where: { status: 'PUBLISHED', deletedAt: null },
|
|
113
|
+
select: {
|
|
114
|
+
id: true,
|
|
115
|
+
title: true,
|
|
116
|
+
slug: true,
|
|
117
|
+
collection: true,
|
|
118
|
+
data: true,
|
|
119
|
+
plainText: true,
|
|
120
|
+
},
|
|
121
|
+
take: maxDocuments,
|
|
122
|
+
});
|
|
123
|
+
const issues = [];
|
|
124
|
+
for (const doc of documents) {
|
|
125
|
+
const data = (doc.data ?? {});
|
|
126
|
+
const problems = [];
|
|
127
|
+
if (!data.metaTitle && !data.seoTitle)
|
|
128
|
+
problems.push('Missing meta title');
|
|
129
|
+
if (!data.metaDescription && !data.seoDescription)
|
|
130
|
+
problems.push('Missing meta description');
|
|
131
|
+
if (!data.canonical)
|
|
132
|
+
problems.push('No canonical URL set');
|
|
133
|
+
if (!data.schemaType)
|
|
134
|
+
problems.push('No Schema.org type');
|
|
135
|
+
const plainText = (doc.plainText ?? '');
|
|
136
|
+
if (plainText.length > 0 && plainText.length < 300) {
|
|
137
|
+
problems.push('Content is too short (< 300 characters)');
|
|
138
|
+
}
|
|
139
|
+
const content = typeof data.body === 'string'
|
|
140
|
+
? data.body
|
|
141
|
+
: typeof data.content === 'string'
|
|
142
|
+
? data.content
|
|
143
|
+
: '';
|
|
144
|
+
if (content) {
|
|
145
|
+
const imgMatches = content.match(/<img\b[^>]*>/gi) ?? [];
|
|
146
|
+
// Case-insensitive: `<IMG ALT="text">` is valid HTML and should not be
|
|
147
|
+
// flagged as missing. Tests the literal attribute name with `\balt\s*=`
|
|
148
|
+
// to avoid matching unrelated tokens that happen to contain `alt=`.
|
|
149
|
+
const missingAlt = imgMatches.filter((img) => !/\balt\s*=/i.test(img)).length;
|
|
150
|
+
if (missingAlt > 0)
|
|
151
|
+
problems.push(`${missingAlt} image(s) missing alt text`);
|
|
152
|
+
// Case-insensitive to match the HTML spec — `<H1>`, `<h1>`, `<h1 class="...">`
|
|
153
|
+
// and `<h1\n>` all count.
|
|
154
|
+
if (!/<h1\b/i.test(content)) {
|
|
155
|
+
problems.push('No H1 heading found in content');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (problems.length > 0) {
|
|
159
|
+
issues.push({
|
|
160
|
+
documentId: doc.id,
|
|
161
|
+
title: doc.title ?? 'Untitled',
|
|
162
|
+
slug: doc.slug ?? '',
|
|
163
|
+
problems,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
total: documents.length,
|
|
169
|
+
pagesWithIssues: issues.length,
|
|
170
|
+
totalProblems: issues.reduce((sum, i) => sum + i.problems.length, 0),
|
|
171
|
+
issues,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/** Re-export for convenience so the route handler can import everything from one module. */
|
|
175
|
+
export { schedulingCronHandler };
|
|
176
|
+
/**
|
|
177
|
+
* Validate a Vercel-style cron Authorization header against `CRON_SECRET`.
|
|
178
|
+
*
|
|
179
|
+
* Returns `false` if the env var is missing — that's deliberately fail-closed
|
|
180
|
+
* so a misconfigured deploy can't silently expose cron endpoints to the public
|
|
181
|
+
* internet. Comparison is constant-time to defeat byte-by-byte timing attacks.
|
|
182
|
+
*/
|
|
183
|
+
export function isAuthorizedCronRequest(authHeader) {
|
|
184
|
+
const secret = process.env.CRON_SECRET;
|
|
185
|
+
if (!secret || secret.length === 0)
|
|
186
|
+
return false;
|
|
187
|
+
if (!authHeader)
|
|
188
|
+
return false;
|
|
189
|
+
// Vercel sends `Authorization: Bearer <CRON_SECRET>`. Some self-hosted
|
|
190
|
+
// schedulers send the bare value; accept both.
|
|
191
|
+
const trimmed = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : authHeader;
|
|
192
|
+
return constantTimeEqual(trimmed, secret);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Per-process random HMAC key. Using a per-process key (not a fixed string)
|
|
196
|
+
* means an attacker can't precompute HMACs offline to compare against — the
|
|
197
|
+
* comparison is between two HMACs they cannot directly observe.
|
|
198
|
+
*/
|
|
199
|
+
const HMAC_COMPARE_KEY = randomBytes(32);
|
|
200
|
+
/**
|
|
201
|
+
* Compare two strings in constant time, including across length differences.
|
|
202
|
+
*
|
|
203
|
+
* The naive approach — early-return on length mismatch then per-character
|
|
204
|
+
* XOR — leaks the secret length: an attacker can probe `Authorization`
|
|
205
|
+
* header values of varying lengths and time the response to learn how many
|
|
206
|
+
* bytes `CRON_SECRET` actually has, reducing the brute-force search space.
|
|
207
|
+
*
|
|
208
|
+
* Instead, HMAC-SHA256 both inputs (yielding fixed 32-byte digests
|
|
209
|
+
* regardless of input length) and compare those with `timingSafeEqual`.
|
|
210
|
+
* `timingSafeEqual` requires equal-length buffers, which the HMAC step
|
|
211
|
+
* guarantees, and it's the only constant-time comparison primitive in
|
|
212
|
+
* `node:crypto` that's safe across all V8 optimisations.
|
|
213
|
+
*/
|
|
214
|
+
function constantTimeEqual(a, b) {
|
|
215
|
+
const hmacA = createHmac('sha256', HMAC_COMPARE_KEY).update(a).digest();
|
|
216
|
+
const hmacB = createHmac('sha256', HMAC_COMPARE_KEY).update(b).digest();
|
|
217
|
+
return timingSafeEqual(hmacA, hmacB);
|
|
218
|
+
}
|
|
219
|
+
function errMsg(err) {
|
|
220
|
+
return err instanceof Error ? err.message : String(err);
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cron/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown for invalid `CMS_ENCRYPTION_KEY` shape. We use a named class so the
|
|
3
|
+
* caller (typically an API handler) can map it to a clear 500 with operator
|
|
4
|
+
* guidance rather than the opaque `OperationError` that some Web Crypto
|
|
5
|
+
* implementations throw on malformed keys.
|
|
6
|
+
*/
|
|
7
|
+
export declare class InvalidEncryptionKeyError extends Error {
|
|
8
|
+
constructor(reason: string);
|
|
9
|
+
}
|
|
1
10
|
/** Encrypt a field value using AES-256-GCM. */
|
|
2
11
|
export declare function encryptField(value: string, keyHex: string): Promise<string>;
|
|
3
12
|
/** Decrypt a field value encrypted with AES-256-GCM. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"encrypted-fields.d.ts","sourceRoot":"","sources":["../../src/security/encrypted-fields.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"encrypted-fields.d.ts","sourceRoot":"","sources":["../../src/security/encrypted-fields.ts"],"names":[],"mappings":"AAQA;;;;;GAKG;AACH,qBAAa,yBAA0B,SAAQ,KAAK;gBACtC,MAAM,EAAE,MAAM;CAQ3B;AAyBD,+CAA+C;AAC/C,wBAAsB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAiBjF;AAED,wDAAwD;AACxD,wBAAsB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAcrF"}
|
|
@@ -1,8 +1,46 @@
|
|
|
1
1
|
const ALGORITHM = 'AES-GCM';
|
|
2
2
|
const IV_LENGTH = 12;
|
|
3
3
|
const TAG_LENGTH = 128;
|
|
4
|
+
const KEY_BYTES = 32; // AES-256 requires exactly 32 bytes
|
|
5
|
+
const KEY_HEX_LENGTH = KEY_BYTES * 2;
|
|
6
|
+
const HEX_RE = /^[0-9a-fA-F]+$/;
|
|
7
|
+
/**
|
|
8
|
+
* Thrown for invalid `CMS_ENCRYPTION_KEY` shape. We use a named class so the
|
|
9
|
+
* caller (typically an API handler) can map it to a clear 500 with operator
|
|
10
|
+
* guidance rather than the opaque `OperationError` that some Web Crypto
|
|
11
|
+
* implementations throw on malformed keys.
|
|
12
|
+
*/
|
|
13
|
+
export class InvalidEncryptionKeyError extends Error {
|
|
14
|
+
constructor(reason) {
|
|
15
|
+
super(`[Actuate CMS] CMS_ENCRYPTION_KEY is invalid: ${reason}. ` +
|
|
16
|
+
`Generate a valid key with: ` +
|
|
17
|
+
`node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`);
|
|
18
|
+
this.name = 'InvalidEncryptionKeyError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Validate that `keyHex` is exactly 64 hex chars (32 bytes for AES-256).
|
|
23
|
+
*
|
|
24
|
+
* Without this guard, WebCrypto's `importKey` either throws an opaque
|
|
25
|
+
* `OperationError` (short keys), or in some non-spec runtimes silently
|
|
26
|
+
* truncates / pads — both produce ciphertext that the matching `decrypt`
|
|
27
|
+
* call can't reverse, surfaced to the user as a generic "decryption failed"
|
|
28
|
+
* weeks after the bad key was first deployed.
|
|
29
|
+
*/
|
|
30
|
+
function validateKey(keyHex) {
|
|
31
|
+
if (typeof keyHex !== 'string' || keyHex.length === 0) {
|
|
32
|
+
throw new InvalidEncryptionKeyError('key is empty or not a string');
|
|
33
|
+
}
|
|
34
|
+
if (keyHex.length !== KEY_HEX_LENGTH) {
|
|
35
|
+
throw new InvalidEncryptionKeyError(`key must be ${KEY_HEX_LENGTH} hex characters (${KEY_BYTES} bytes), got ${keyHex.length}`);
|
|
36
|
+
}
|
|
37
|
+
if (!HEX_RE.test(keyHex)) {
|
|
38
|
+
throw new InvalidEncryptionKeyError('key must contain only hex characters [0-9a-fA-F]');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
4
41
|
/** Encrypt a field value using AES-256-GCM. */
|
|
5
42
|
export async function encryptField(value, keyHex) {
|
|
43
|
+
validateKey(keyHex);
|
|
6
44
|
const key = await importKey(keyHex);
|
|
7
45
|
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
|
|
8
46
|
const encoded = new TextEncoder().encode(value);
|
|
@@ -14,6 +52,7 @@ export async function encryptField(value, keyHex) {
|
|
|
14
52
|
}
|
|
15
53
|
/** Decrypt a field value encrypted with AES-256-GCM. */
|
|
16
54
|
export async function decryptField(encrypted, keyHex) {
|
|
55
|
+
validateKey(keyHex);
|
|
17
56
|
const key = await importKey(keyHex);
|
|
18
57
|
const data = hexToBuffer(encrypted);
|
|
19
58
|
const iv = data.slice(0, IV_LENGTH);
|
|
@@ -33,10 +72,22 @@ function bufferToHex(buffer) {
|
|
|
33
72
|
.map((b) => b.toString(16).padStart(2, '0'))
|
|
34
73
|
.join('');
|
|
35
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Decode a hex string to bytes. Caller is expected to have validated shape
|
|
77
|
+
* via `validateKey` for keys; this function tolerates already-validated input
|
|
78
|
+
* and rejects odd-length hex (which previously silently dropped trailing chars).
|
|
79
|
+
*/
|
|
36
80
|
function hexToBuffer(hex) {
|
|
81
|
+
if (hex.length % 2 !== 0) {
|
|
82
|
+
throw new Error('hex string must have an even number of characters');
|
|
83
|
+
}
|
|
37
84
|
const bytes = new Uint8Array(hex.length / 2);
|
|
38
85
|
for (let i = 0; i < hex.length; i += 2) {
|
|
39
|
-
|
|
86
|
+
const byte = parseInt(hex.slice(i, i + 2), 16);
|
|
87
|
+
if (Number.isNaN(byte)) {
|
|
88
|
+
throw new Error(`invalid hex character at offset ${i}`);
|
|
89
|
+
}
|
|
90
|
+
bytes[i / 2] = byte;
|
|
40
91
|
}
|
|
41
92
|
return bytes;
|
|
42
93
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"encrypted-fields.js","sourceRoot":"","sources":["../../src/security/encrypted-fields.ts"],"names":[],"mappings":"AAAA,MAAM,SAAS,GAAG,SAAS,CAAA;AAC3B,MAAM,SAAS,GAAG,EAAE,CAAA;AACpB,MAAM,UAAU,GAAG,GAAG,CAAA;
|
|
1
|
+
{"version":3,"file":"encrypted-fields.js","sourceRoot":"","sources":["../../src/security/encrypted-fields.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IP canonicalization for SSRF defense.
|
|
3
|
+
*
|
|
4
|
+
* The previous regex-based approach in `webhook.ts` only matched the literal
|
|
5
|
+
* dotted-quad form. It missed every encoding that `getaddrinfo` (and therefore
|
|
6
|
+
* `fetch`) accepts:
|
|
7
|
+
*
|
|
8
|
+
* - Decimal: http://2130706433/ → 127.0.0.1
|
|
9
|
+
* - Octal: http://0177.0.0.1/ → 127.0.0.1
|
|
10
|
+
* - Hex: http://0x7f.0.0.1/ → 127.0.0.1
|
|
11
|
+
* - Short-form: http://127.1/ → 127.0.0.1
|
|
12
|
+
* - IPv4-mapped v6: http://[::ffff:127.0.0.1] → 127.0.0.1
|
|
13
|
+
* - IPv4-compat v6: http://[::127.0.0.1] → 127.0.0.1
|
|
14
|
+
*
|
|
15
|
+
* This module canonicalizes any of those forms to a 4-octet IPv4 string (or
|
|
16
|
+
* a normalized 16-byte IPv6 address), then range-checks against a complete
|
|
17
|
+
* private/internal block list including AWS/GCP/Azure metadata endpoints and
|
|
18
|
+
* the carrier-grade NAT range.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Result of {@link canonicalizeHostname}. There are exactly three valid
|
|
22
|
+
* shapes:
|
|
23
|
+
*
|
|
24
|
+
* - **Valid IPv4 literal** — `isHostname=false`, `isValidIp=true`, `ipv4` set.
|
|
25
|
+
* - **Valid IPv6 literal** — `isHostname=false`, `isValidIp=true`, `ipv6` set.
|
|
26
|
+
* - **Hostname (DNS name)** — `isHostname=true`, `isValidIp=false`, neither
|
|
27
|
+
* `ipv4` nor `ipv6` set.
|
|
28
|
+
* - **Malformed IP literal** — `isHostname=false`, `isValidIp=false`, neither
|
|
29
|
+
* IP field set. (Inputs that look like IP literals — e.g. contain `:` or
|
|
30
|
+
* only digits and dots — but fail to parse cleanly.)
|
|
31
|
+
*
|
|
32
|
+
* SSRF gates MUST treat the malformed shape as "reject" rather than as
|
|
33
|
+
* "validated IP literal". Use `isValidIp` to distinguish the two `false`
|
|
34
|
+
* `isHostname` cases.
|
|
35
|
+
*/
|
|
36
|
+
export interface CanonicalizedHost {
|
|
37
|
+
/** Original hostname as it appeared in the URL. */
|
|
38
|
+
raw: string;
|
|
39
|
+
/** Canonical IPv4 dotted-quad string when the literal was IPv4 (in any form). */
|
|
40
|
+
ipv4?: string;
|
|
41
|
+
/** Canonical IPv6 hextet string when the literal was IPv6. */
|
|
42
|
+
ipv6?: string;
|
|
43
|
+
/** True when the input was a DNS name (not an IP literal in any form). */
|
|
44
|
+
isHostname: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* True when the input parsed cleanly into a canonical IP address (`ipv4`
|
|
47
|
+
* or `ipv6` is set). False for both DNS hostnames and malformed IP-shaped
|
|
48
|
+
* strings. Callers performing a security check on the parsed IP MUST
|
|
49
|
+
* verify `isValidIp` before reading `ipv4` / `ipv6`.
|
|
50
|
+
*/
|
|
51
|
+
isValidIp: boolean;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Canonicalize a URL hostname into either a normalized IP address or a
|
|
55
|
+
* passthrough hostname. Handles every IPv4 encoding accepted by `inet_aton`
|
|
56
|
+
* (decimal, octal, hex, short-form), IPv4-mapped IPv6, and bracketed IPv6.
|
|
57
|
+
*/
|
|
58
|
+
export declare function canonicalizeHostname(rawHostname: string): CanonicalizedHost;
|
|
59
|
+
/**
|
|
60
|
+
* Returns the matching private/internal range when the canonicalized host is
|
|
61
|
+
* one, `null` when it's a safe public IP, or a "malformed" reason when the
|
|
62
|
+
* input claims to be an IP literal but didn't parse cleanly.
|
|
63
|
+
*
|
|
64
|
+
* Defense-in-depth: even if a caller forgets to gate on `isValidIp`, this
|
|
65
|
+
* function still fails closed on the malformed shape (`isHostname=false`
|
|
66
|
+
* with no `ipv4`/`ipv6`).
|
|
67
|
+
*/
|
|
68
|
+
export declare function isPrivateAddress(host: CanonicalizedHost): {
|
|
69
|
+
reason: string;
|
|
70
|
+
} | null;
|
|
71
|
+
//# sourceMappingURL=ip-canon.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ip-canon.d.ts","sourceRoot":"","sources":["../../src/security/ip-canon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAiCH;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,iBAAiB;IAChC,mDAAmD;IACnD,GAAG,EAAE,MAAM,CAAA;IACX,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,8DAA8D;IAC9D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,0EAA0E;IAC1E,UAAU,EAAE,OAAO,CAAA;IACnB;;;;;OAKG;IACH,SAAS,EAAE,OAAO,CAAA;CACnB;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,iBAAiB,CAmC3E;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA4BnF"}
|