@lunora/advisor 1.0.0-alpha.19 → 1.0.0-alpha.20

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 (22) hide show
  1. package/dist/index.mjs +12 -12
  2. package/dist/packem_shared/argument-derived-sink-C1xTTqAt.mjs +30 -0
  3. package/dist/packem_shared/browserUserUrlWithoutAllowlist-CvhA6w59.mjs +26 -0
  4. package/dist/packem_shared/containerInstanceKeyFromUserInput-BUXj2J4w.mjs +19 -0
  5. package/dist/packem_shared/{helpers-BhGmRI2n.mjs → helpers-BySnKhVB.mjs} +2 -1
  6. package/dist/packem_shared/imagesUrlSourceFromUserInput-YcDQ0b_Y.mjs +19 -0
  7. package/dist/packem_shared/{indexReferencesUnknownField-BfCCZfgW.mjs → indexReferencesUnknownField-BSWNngxX.mjs} +1 -1
  8. package/dist/packem_shared/kvUnscopedUserKeyIdor-YWwmfE8X.mjs +19 -0
  9. package/dist/packem_shared/{outputProjectionMissingOnPublicRead-Cjd885DW.mjs → outputProjectionMissingOnPublicRead-Bl5IMx0k.mjs} +1 -1
  10. package/dist/packem_shared/{publicMutationWithoutRatelimit-xBpJ6GWK.mjs → publicMutationWithoutRatelimit-DbIhgi7j.mjs} +2 -2
  11. package/dist/packem_shared/{publicTableRlsOptoutConfusion-DJ5GFSRD.mjs → publicTableRlsOptoutConfusion-Bn71yoD4.mjs} +1 -1
  12. package/dist/packem_shared/{relationReferencesUnknownField-D4hckqi8.mjs → relationReferencesUnknownField-CjbLScJ1.mjs} +1 -1
  13. package/dist/packem_shared/storageKeyFromUserArgs-B86elJgS.mjs +19 -0
  14. package/dist/packem_shared/{userCreatingMutationWithoutCaptcha-CH31YsUZ.mjs → userCreatingMutationWithoutCaptcha-2DZtWIPb.mjs} +2 -2
  15. package/dist/packem_shared/vectorsNamespaceFromUserInput-CQhr5bVn.mjs +19 -0
  16. package/package.json +3 -3
  17. package/dist/packem_shared/browserUserUrlWithoutAllowlist-B5DrK72J.mjs +0 -32
  18. package/dist/packem_shared/containerInstanceKeyFromUserInput-DLl-jpex.mjs +0 -26
  19. package/dist/packem_shared/imagesUrlSourceFromUserInput-BYq6I7VC.mjs +0 -26
  20. package/dist/packem_shared/kvUnscopedUserKeyIdor-wOv1sRL8.mjs +0 -26
  21. package/dist/packem_shared/storageKeyFromUserArgs-B7k0iiGb.mjs +0 -26
  22. package/dist/packem_shared/vectorsNamespaceFromUserInput-CCSl5SYq.mjs +0 -26
package/dist/index.mjs CHANGED
@@ -14,9 +14,9 @@ import authSecureCookiesDisabled from './packem_shared/authSecureCookiesDisabled
14
14
  import authSessionFreshageZero from './packem_shared/authSessionFreshageZero-CjufCc_Q.mjs';
15
15
  import authTrustedOriginsWildcard from './packem_shared/authTrustedOriginsWildcard-ylv4PKzC.mjs';
16
16
  import browserAllowPrivateTargets from './packem_shared/browserAllowPrivateTargets-CBvQxLAM.mjs';
17
- import browserUserUrlWithoutAllowlist from './packem_shared/browserUserUrlWithoutAllowlist-B5DrK72J.mjs';
17
+ import browserUserUrlWithoutAllowlist from './packem_shared/browserUserUrlWithoutAllowlist-CvhA6w59.mjs';
18
18
  import circularFk from './packem_shared/circularFk-B2freHrP.mjs';
19
- import containerInstanceKeyFromUserInput from './packem_shared/containerInstanceKeyFromUserInput-DLl-jpex.mjs';
19
+ import containerInstanceKeyFromUserInput from './packem_shared/containerInstanceKeyFromUserInput-BUXj2J4w.mjs';
20
20
  import containerOversizedInstance from './packem_shared/containerOversizedInstance-5U1VKPRM.mjs';
21
21
  import containerPublicInternet from './packem_shared/containerPublicInternet-CuNerJE5.mjs';
22
22
  import containerRuntimeEgressRelaxation from './packem_shared/containerRuntimeEgressRelaxation-B0LlfmDA.mjs';
@@ -30,10 +30,10 @@ import httpActionMissingAuthGuard from './packem_shared/httpActionMissingAuthGua
30
30
  import httpActionResponseHeaderInjection from './packem_shared/httpActionResponseHeaderInjection-DOFS7pFT.mjs';
31
31
  import hyperdriveOutsideAction from './packem_shared/hyperdriveOutsideAction-BgZqX7Xg.mjs';
32
32
  import identityUndeclaredClaimTrusted from './packem_shared/identityUndeclaredClaimTrusted-D8nXV2dd.mjs';
33
- import imagesUrlSourceFromUserInput from './packem_shared/imagesUrlSourceFromUserInput-BYq6I7VC.mjs';
34
- import indexReferencesUnknownField from './packem_shared/indexReferencesUnknownField-BfCCZfgW.mjs';
33
+ import imagesUrlSourceFromUserInput from './packem_shared/imagesUrlSourceFromUserInput-YcDQ0b_Y.mjs';
34
+ import indexReferencesUnknownField from './packem_shared/indexReferencesUnknownField-BSWNngxX.mjs';
35
35
  import insertManyUnsafeUserData from './packem_shared/insertManyUnsafeUserData-Dn77XpmX.mjs';
36
- import kvUnscopedUserKeyIdor from './packem_shared/kvUnscopedUserKeyIdor-wOv1sRL8.mjs';
36
+ import kvUnscopedUserKeyIdor from './packem_shared/kvUnscopedUserKeyIdor-YWwmfE8X.mjs';
37
37
  import mailInboundDispatchWithoutVerify from './packem_shared/mailInboundDispatchWithoutVerify-CHSRA8zz.mjs';
38
38
  import mailRecipientFromRequestInput from './packem_shared/mailRecipientFromRequestInput-Cka2qu5J.mjs';
39
39
  import maskUncoveredPiiColumn from './packem_shared/maskUncoveredPiiColumn-DjGIPG6M.mjs';
@@ -42,7 +42,7 @@ import maskedRelationLeakViaWith from './packem_shared/maskedRelationLeakViaWith
42
42
  import mutatorFullRowReplace from './packem_shared/mutatorFullRowReplace-BJnNDaIV.mjs';
43
43
  import nondeterministicQueryMutation from './packem_shared/nondeterministicQueryMutation-GXES1fLp.mjs';
44
44
  import normalizeIdUsedAsAuthorization from './packem_shared/normalizeIdUsedAsAuthorization-BVPtCpzT.mjs';
45
- import outputProjectionMissingOnPublicRead from './packem_shared/outputProjectionMissingOnPublicRead-Cjd885DW.mjs';
45
+ import outputProjectionMissingOnPublicRead from './packem_shared/outputProjectionMissingOnPublicRead-Bl5IMx0k.mjs';
46
46
  import ownerFieldFromArgsNotAuth from './packem_shared/ownerFieldFromArgsNotAuth-mOw3hE5z.mjs';
47
47
  import paymentCreateWithoutAuthorize from './packem_shared/paymentCreateWithoutAuthorize-BYm4JLxo.mjs';
48
48
  import paymentWebhookWideTolerance from './packem_shared/paymentWebhookWideTolerance-D9QQJMJq.mjs';
@@ -51,14 +51,14 @@ import policyReferencesUnknownTable from './packem_shared/policyReferencesUnknow
51
51
  import privilegedDispatchUnvalidatedPayload from './packem_shared/privilegedDispatchUnvalidatedPayload-5Forjckt.mjs';
52
52
  import privilegedFanoutFromPublicProcedure from './packem_shared/privilegedFanoutFromPublicProcedure-D3dL01B8.mjs';
53
53
  import publicArgumentUsesAny from './packem_shared/publicArgumentUsesAny-C71b2NCf.mjs';
54
- import publicMutationWithoutRatelimit from './packem_shared/publicMutationWithoutRatelimit-xBpJ6GWK.mjs';
55
- import publicTableRlsOptoutConfusion from './packem_shared/publicTableRlsOptoutConfusion-DJ5GFSRD.mjs';
54
+ import publicMutationWithoutRatelimit from './packem_shared/publicMutationWithoutRatelimit-DbIhgi7j.mjs';
55
+ import publicTableRlsOptoutConfusion from './packem_shared/publicTableRlsOptoutConfusion-Bn71yoD4.mjs';
56
56
  import queueWithoutDlq from './packem_shared/queueWithoutDlq-CSkGNb_0.mjs';
57
57
  import r2sqlOutsideAction from './packem_shared/r2sqlOutsideAction-CtqxvMuV.mjs';
58
58
  import ratelimitDefaultMemoryStore from './packem_shared/ratelimitDefaultMemoryStore-BISChG5C.mjs';
59
59
  import ratelimitKeySpoofableOrGlobal from './packem_shared/ratelimitKeySpoofableOrGlobal-DqlHYQQ3.mjs';
60
60
  import ratelimitMiddlewareFailOpen from './packem_shared/ratelimitMiddlewareFailOpen-CJgDCaUw.mjs';
61
- import relationReferencesUnknownField from './packem_shared/relationReferencesUnknownField-D4hckqi8.mjs';
61
+ import relationReferencesUnknownField from './packem_shared/relationReferencesUnknownField-CjbLScJ1.mjs';
62
62
  import relationReferencesUnknownTable from './packem_shared/relationReferencesUnknownTable-DrorpKYe.mjs';
63
63
  import rlsUncoveredTable from './packem_shared/rlsUncoveredTable-CxEfZ5eZ.mjs';
64
64
  import shapeTargetsGlobalTable from './packem_shared/shapeTargetsGlobalTable-DHrf4Koi.mjs';
@@ -66,7 +66,7 @@ import shapeUnknownTable from './packem_shared/shapeUnknownTable-C8aDWFoe.mjs';
66
66
  import softDeleteIncludeDeletedFromArgs from './packem_shared/softDeleteIncludeDeletedFromArgs-BLqDKrkM.mjs';
67
67
  import sqlInjectionRisk from './packem_shared/sqlInjectionRisk-zwytYGLt.mjs';
68
68
  import storageGenerateUploadUrlNoContentTypePin from './packem_shared/storageGenerateUploadUrlNoContentTypePin-Da4L9Ge8.mjs';
69
- import storageKeyFromUserArgs from './packem_shared/storageKeyFromUserArgs-B7k0iiGb.mjs';
69
+ import storageKeyFromUserArgs from './packem_shared/storageKeyFromUserArgs-B86elJgS.mjs';
70
70
  import storagePresignedUrlForPrivateContent from './packem_shared/storagePresignedUrlForPrivateContent-yGmb8uqz.mjs';
71
71
  import storageUploadWithoutContentTypeAllowlist from './packem_shared/storageUploadWithoutContentTypeAllowlist-BV-gF1lT.mjs';
72
72
  import storageUploadWithoutMaxSize from './packem_shared/storageUploadWithoutMaxSize-DpxO59wU.mjs';
@@ -74,8 +74,8 @@ import tableWithoutInsert from './packem_shared/tableWithoutInsert-CbbaYIP4.mjs'
74
74
  import unboundedStringArgument from './packem_shared/unboundedStringArgument-DThg2-wt.mjs';
75
75
  import unindexedForeignKey from './packem_shared/unindexedForeignKey-BgJbKyqK.mjs';
76
76
  import unindexedRelationTarget from './packem_shared/unindexedRelationTarget-D6eyj6Xx.mjs';
77
- import userCreatingMutationWithoutCaptcha from './packem_shared/userCreatingMutationWithoutCaptcha-CH31YsUZ.mjs';
78
- import vectorsNamespaceFromUserInput from './packem_shared/vectorsNamespaceFromUserInput-CCSl5SYq.mjs';
77
+ import userCreatingMutationWithoutCaptcha from './packem_shared/userCreatingMutationWithoutCaptcha-2DZtWIPb.mjs';
78
+ import vectorsNamespaceFromUserInput from './packem_shared/vectorsNamespaceFromUserInput-CQhr5bVn.mjs';
79
79
  import workflowDuplicateStepName from './packem_shared/workflowDuplicateStepName-ioBxPBCy.mjs';
80
80
  import workflowUnknownTarget from './packem_shared/workflowUnknownTarget-Cdd7WhKQ.mjs';
81
81
  import workflowUnused from './packem_shared/workflowUnused-D0jHxdz9.mjs';
@@ -0,0 +1,30 @@
1
+ import { e as emit } from './finding-Dm_zvzS1.mjs';
2
+
3
+ const makeArgumentDerivedSinkLint = (config) => {
4
+ const lint = {
5
+ categories: config.categories,
6
+ description: config.description,
7
+ facing: config.facing,
8
+ level: config.level,
9
+ name: config.name,
10
+ remediation: config.remediation,
11
+ run: (context) => {
12
+ const accesses = config.getAccesses(context);
13
+ if (accesses === void 0 || config.suppressWhen?.(context) === true) {
14
+ return [];
15
+ }
16
+ return accesses.map(
17
+ (access) => emit(lint, {
18
+ cacheKey: config.cacheKey(access),
19
+ detail: config.detail(access),
20
+ metadata: config.metadata(access)
21
+ })
22
+ );
23
+ },
24
+ source: "static",
25
+ title: config.title
26
+ };
27
+ return lint;
28
+ };
29
+
30
+ export { makeArgumentDerivedSinkLint as m };
@@ -0,0 +1,26 @@
1
+ import { m as makeArgumentDerivedSinkLint } from './argument-derived-sink-C1xTTqAt.mjs';
2
+
3
+ const browserUserUrlWithoutAllowlist = makeArgumentDerivedSinkLint({
4
+ cacheKey: (access) => `browser_user_url_without_allowlist:${access.file}:${access.line.toString()}`,
5
+ categories: ["SECURITY"],
6
+ description: "A `ctx.browser.<method>(url, …)` call navigates to a URL derived from the handler's `args` with no server-side scoping, and no `createBrowser` allowlist contains it. The default guard blocks private targets but not arbitrary public URLs, so any caller can make the headless browser an open-proxy / SSRF tool (fetch arbitrary third-party URLs, DNS-rebind to internal hosts).",
7
+ detail: (access) => `\`ctx.browser.${access.method}\` in \`${access.exportName}\` (${access.file}:${access.line.toString()}) navigates to a URL derived from \`args\` with no server-side scoping, and no \`createBrowser\` allowlist contains it — the default guard blocks private targets but not arbitrary public URLs, so any caller can turn the headless browser into an open-proxy / SSRF tool. Pin \`allowedHosts\` (and/or \`resolveDns\`) on \`createBrowser({...})\`, and derive the URL from server-trusted state where possible.`,
8
+ facing: "EXTERNAL",
9
+ getAccesses: (context) => context.browserUrlAccesses,
10
+ level: "WARN",
11
+ metadata: (access) => {
12
+ return { exportName: access.exportName, file: access.file, line: access.line, method: access.method };
13
+ },
14
+ name: "browser_user_url_without_allowlist",
15
+ remediation: "Pin the browser with an `allowedHosts` allowlist (and/or `resolveDns`) on `createBrowser({...})`, and derive the navigation URL from server-trusted state where possible rather than passing `args` straight to `ctx.browser`.",
16
+ // A `createBrowser` hardened with an `allowedHosts` allowlist or a pinned
17
+ // `resolveDns` contains the SSRF surface — suppress every finding when one is
18
+ // visible. Only an analyzable (non-spread, static object-literal) config call
19
+ // counts; an opaque config could set the key elsewhere but can't be relied on.
20
+ suppressWhen: (context) => (context.configCalls ?? []).some(
21
+ (call) => call.callee === "createBrowser" && call.analyzable && (call.presentKeys.includes("allowedHosts") || call.presentKeys.includes("resolveDns"))
22
+ ),
23
+ title: "Browser navigates to arg-derived URL with no allowlist"
24
+ });
25
+
26
+ export { browserUserUrlWithoutAllowlist as default };
@@ -0,0 +1,19 @@
1
+ import { m as makeArgumentDerivedSinkLint } from './argument-derived-sink-C1xTTqAt.mjs';
2
+
3
+ const containerInstanceKeyFromUserInput = makeArgumentDerivedSinkLint({
4
+ cacheKey: (access) => `container_instance_key_from_user_input:${access.file}:${access.line.toString()}`,
5
+ categories: ["SECURITY"],
6
+ description: "A `ctx.containers.<name>.get` call routes to a container instance using a key derived from the handler's `args` with no server-side scoping. Any caller can supply another tenant's key and reach that tenant's container instance — a cross-tenant insecure direct object reference (IDOR).",
7
+ detail: (access) => `\`ctx.containers.*.${access.method}\` in \`${access.exportName}\` (${access.file}:${access.line.toString()}) routes to a container instance using a key derived from \`args\` with no server-side scoping — any caller can reach another tenant's container (IDOR). Derive the key from a server-trusted identity (e.g. \`\${ctx.auth.userId}\`).`,
8
+ facing: "EXTERNAL",
9
+ getAccesses: (context) => context.containerKeyAccesses,
10
+ level: "WARN",
11
+ metadata: (access) => {
12
+ return { exportName: access.exportName, file: access.file, line: access.line, method: access.method };
13
+ },
14
+ name: "container_instance_key_from_user_input",
15
+ remediation: `Derive the container instance key from a server-trusted identity (e.g. \`\${ctx.auth.userId}\`) or a record the caller owns — never pass request input straight to \`ctx.containers.<name>.get\`.`,
16
+ title: "Possible cross-tenant IDOR from arg-derived unscoped container key"
17
+ });
18
+
19
+ export { containerInstanceKeyFromUserInput as default };
@@ -26,5 +26,6 @@ const PII_FIELD_NAMES = /* @__PURE__ */ new Set([
26
26
  const SYSTEM_FIELDS = /* @__PURE__ */ new Set(["_creationTime", "_id"]);
27
27
  const ownershipOrPiiColumns = (table) => table.fields.filter((field) => OWNERSHIP_FIELD_NAMES.has(field) || PII_FIELD_NAMES.has(field));
28
28
  const tableColumnSet = (table) => /* @__PURE__ */ new Set([...SYSTEM_FIELDS, ...table.fields]);
29
+ const isPublicWrite = (procedure) => procedure.visibility === "public" && (procedure.kind === "mutation" || procedure.kind === "action");
29
30
 
30
- export { PII_FIELD_NAMES as P, ownershipOrPiiColumns as o, tableColumnSet as t };
31
+ export { PII_FIELD_NAMES as P, isPublicWrite as i, ownershipOrPiiColumns as o, tableColumnSet as t };
@@ -0,0 +1,19 @@
1
+ import { m as makeArgumentDerivedSinkLint } from './argument-derived-sink-C1xTTqAt.mjs';
2
+
3
+ const imagesUrlSourceFromUserInput = makeArgumentDerivedSinkLint({
4
+ cacheKey: (access) => `images_url_source_from_user_input:${access.file}:${access.line.toString()}`,
5
+ categories: ["SECURITY"],
6
+ description: "A `buildImageDeliveryUrl({ key, … })` call's `key` — the CDN transform's source image, an absolute URL or an origin-relative key — is derived from the handler's `args` with no server-side scoping, so any caller can point the CDN's on-the-fly transform at an attacker-chosen origin (SSRF / open proxy) or an arbitrary key under the account's own store.",
7
+ detail: (access) => `\`buildImageDeliveryUrl\` in \`${access.exportName}\` (${access.file}:${access.line.toString()}) builds its delivery URL from a \`key\` derived from \`args\` with no server-side scoping — any caller can point the CDN's on-the-fly transform at an attacker-chosen origin or store key. Validate \`key\` against an allowlist, or derive it from server-trusted state.`,
8
+ facing: "EXTERNAL",
9
+ getAccesses: (context) => context.imageDeliveryUrlAccesses,
10
+ level: "WARN",
11
+ metadata: (access) => {
12
+ return { exportName: access.exportName, file: access.file, line: access.line };
13
+ },
14
+ name: "images_url_source_from_user_input",
15
+ remediation: "Validate `key` against an allowlist of known origins/prefixes before calling `buildImageDeliveryUrl`, or derive it from server-trusted state (a stored record's own image key) rather than passing `args` straight through.",
16
+ title: "Image delivery URL built from unscoped user input"
17
+ });
18
+
19
+ export { imagesUrlSourceFromUserInput as default };
@@ -1,5 +1,5 @@
1
1
  import { e as emit } from './finding-Dm_zvzS1.mjs';
2
- import { t as tableColumnSet } from './helpers-BhGmRI2n.mjs';
2
+ import { t as tableColumnSet } from './helpers-BySnKhVB.mjs';
3
3
 
4
4
  const indexReferencesUnknownField = {
5
5
  categories: ["SCHEMA"],
@@ -0,0 +1,19 @@
1
+ import { m as makeArgumentDerivedSinkLint } from './argument-derived-sink-C1xTTqAt.mjs';
2
+
3
+ const kvUnscopedUserKeyIdor = makeArgumentDerivedSinkLint({
4
+ cacheKey: (access) => `kv_unscoped_user_key_idor:${access.file}:${access.line.toString()}`,
5
+ categories: ["SECURITY"],
6
+ description: "A `ctx.kv` read/write uses a namespace key derived from the handler's `args` with no server-side scoping. Workers KV is a flat namespace, so an unscoped key lets any caller read, overwrite, or delete another user's entry — an insecure direct object reference (IDOR).",
7
+ detail: (access) => `\`ctx.kv.${access.method}\` in \`${access.exportName}\` (${access.file}:${access.line.toString()}) uses a KV key derived from \`args\` with no server-side scoping — any caller can read/overwrite/delete another user's entry (IDOR). Prefix the key with a server-trusted identity (e.g. \`\${ctx.auth.userId}:…\`).`,
8
+ facing: "EXTERNAL",
9
+ getAccesses: (context) => context.kvKeyAccesses,
10
+ level: "ERROR",
11
+ metadata: (access) => {
12
+ return { exportName: access.exportName, file: access.file, line: access.line, method: access.method };
13
+ },
14
+ name: "kv_unscoped_user_key_idor",
15
+ remediation: `Prefix every \`ctx.kv\` key with a server-trusted identity (e.g. \`\${ctx.auth.userId}:\${args.id}\`) so a caller can only address their own entries. Never pass request input straight to \`ctx.kv.get\`/\`put\`/\`delete\`.`,
16
+ title: "Possible IDOR from arg-derived unscoped KV key"
17
+ });
18
+
19
+ export { kvUnscopedUserKeyIdor as default };
@@ -1,5 +1,5 @@
1
1
  import { e as emit } from './finding-Dm_zvzS1.mjs';
2
- import { P as PII_FIELD_NAMES } from './helpers-BhGmRI2n.mjs';
2
+ import { P as PII_FIELD_NAMES } from './helpers-BySnKhVB.mjs';
3
3
 
4
4
  const outputProjectionMissingOnPublicRead = {
5
5
  categories: ["SECURITY"],
@@ -1,4 +1,5 @@
1
1
  import { e as emit } from './finding-Dm_zvzS1.mjs';
2
+ import { i as isPublicWrite } from './helpers-BySnKhVB.mjs';
2
3
 
3
4
  const SENSITIVE_NAME_RE = /contact|forgot|login|magic|otp|register|reset|signin|signup|subscribe|verify/iu;
4
5
  const publicMutationWithoutRatelimit = {
@@ -14,8 +15,7 @@ const publicMutationWithoutRatelimit = {
14
15
  }
15
16
  const findings = [];
16
17
  for (const procedure of context.procedureProtections) {
17
- const isPublicWrite = procedure.visibility === "public" && (procedure.kind === "mutation" || procedure.kind === "action");
18
- if (!isPublicWrite || procedure.usesRateLimit) {
18
+ if (!isPublicWrite(procedure) || procedure.usesRateLimit) {
19
19
  continue;
20
20
  }
21
21
  const sensitive = SENSITIVE_NAME_RE.test(procedure.exportName);
@@ -1,5 +1,5 @@
1
1
  import { e as emit } from './finding-Dm_zvzS1.mjs';
2
- import { o as ownershipOrPiiColumns } from './helpers-BhGmRI2n.mjs';
2
+ import { o as ownershipOrPiiColumns } from './helpers-BySnKhVB.mjs';
3
3
 
4
4
  const publicTableRlsOptoutConfusion = {
5
5
  categories: ["SECURITY"],
@@ -1,5 +1,5 @@
1
1
  import { e as emit } from './finding-Dm_zvzS1.mjs';
2
- import { t as tableColumnSet } from './helpers-BhGmRI2n.mjs';
2
+ import { t as tableColumnSet } from './helpers-BySnKhVB.mjs';
3
3
 
4
4
  const missingColumns = (byName, columnsOf, holder, relation) => {
5
5
  const target = byName.get(relation.table);
@@ -0,0 +1,19 @@
1
+ import { m as makeArgumentDerivedSinkLint } from './argument-derived-sink-C1xTTqAt.mjs';
2
+
3
+ const storageKeyFromUserArgs = makeArgumentDerivedSinkLint({
4
+ cacheKey: (access) => `storage_key_from_user_args:${access.file}:${access.line.toString()}`,
5
+ categories: ["SECURITY"],
6
+ description: "A `ctx.storage.*` call uses an R2 object key taken directly from the handler's `args` with no server-side scoping. The bucket methods key by the caller-supplied string, so any caller can read, overwrite, or delete another user's object — object-level IDOR.",
7
+ detail: (access) => `\`ctx.storage.*.${access.method}\` in \`${access.exportName}\` (${access.file}:${access.line.toString()}) uses an object key derived from \`args\` with no server-side scoping — any caller can read/overwrite/delete another user's object (IDOR). Prefix the key with a server-trusted identity (e.g. \`\${ctx.auth.userId}/…\`) or resolve the object through an owned record.`,
8
+ facing: "EXTERNAL",
9
+ getAccesses: (context) => context.storageKeyAccesses,
10
+ level: "ERROR",
11
+ metadata: (access) => {
12
+ return { exportName: access.exportName, file: access.file, line: access.line, method: access.method };
13
+ },
14
+ name: "storage_key_from_user_args",
15
+ remediation: `Prefix the object key with a server-trusted identity (e.g. \`\${ctx.auth.userId}/…\`) or resolve the object through a record the caller is known to own. Never pass request input straight through as an R2 object key.`,
16
+ title: "R2 object key taken directly from user args (IDOR)"
17
+ });
18
+
19
+ export { storageKeyFromUserArgs as default };
@@ -1,4 +1,5 @@
1
1
  import { e as emit } from './finding-Dm_zvzS1.mjs';
2
+ import { i as isPublicWrite } from './helpers-BySnKhVB.mjs';
2
3
 
3
4
  const userCreatingMutationWithoutCaptcha = {
4
5
  categories: ["SECURITY"],
@@ -13,9 +14,8 @@ const userCreatingMutationWithoutCaptcha = {
13
14
  }
14
15
  const findings = [];
15
16
  for (const procedure of context.procedureProtections) {
16
- const isPublicWrite = procedure.visibility === "public" && (procedure.kind === "mutation" || procedure.kind === "action");
17
17
  const sensitive = procedure.writesUserTable || procedure.callsMail;
18
- if (!isPublicWrite || !sensitive || procedure.usesCaptcha) {
18
+ if (!isPublicWrite(procedure) || !sensitive || procedure.usesCaptcha) {
19
19
  continue;
20
20
  }
21
21
  const reason = procedure.writesUserTable ? "writes a user/session table" : "sends mail";
@@ -0,0 +1,19 @@
1
+ import { m as makeArgumentDerivedSinkLint } from './argument-derived-sink-C1xTTqAt.mjs';
2
+
3
+ const vectorsNamespaceFromUserInput = makeArgumentDerivedSinkLint({
4
+ cacheKey: (access) => `vectors_namespace_from_user_input:${access.file}:${access.line.toString()}`,
5
+ categories: ["SECURITY"],
6
+ description: "A `ctx.vectors.query`/`upsert`/`upsertMany` call uses a `namespace` derived from the handler's `args` with no server-side scoping. A Vectorize namespace partitions one index into isolated sub-collections, so an unscoped namespace lets any caller read or poison another tenant's vectors.",
7
+ detail: (access) => `\`ctx.vectors.${access.method}\` in \`${access.exportName}\` (${access.file}:${access.line.toString()}) uses a Vectorize namespace derived from \`args\` with no server-side scoping — any caller can read or poison another tenant's vectors. Derive the namespace from a server-trusted identity (e.g. \`\${ctx.auth.orgId}\`), never from \`args\`.`,
8
+ facing: "EXTERNAL",
9
+ getAccesses: (context) => context.vectorNamespaceAccesses,
10
+ level: "WARN",
11
+ metadata: (access) => {
12
+ return { exportName: access.exportName, file: access.file, line: access.line, method: access.method };
13
+ },
14
+ name: "vectors_namespace_from_user_input",
15
+ remediation: `Derive the \`namespace\` from a server-trusted identity (e.g. \`\${ctx.auth.orgId}\`), never from \`args\`.`,
16
+ title: "Vectorize namespace derived from unscoped user input"
17
+ });
18
+
19
+ export { vectorsNamespaceFromUserInput as default };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/advisor",
3
- "version": "1.0.0-alpha.19",
3
+ "version": "1.0.0-alpha.20",
4
4
  "description": "Schema & query lints (splinter-style advisors) for Lunora, feeding the Studio Advisors view",
5
5
  "keywords": [
6
6
  "advisor",
@@ -46,8 +46,8 @@
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
- "@lunora/errors": "1.0.0-alpha.1",
50
- "@lunora/server": "1.0.0-alpha.16"
49
+ "@lunora/errors": "1.0.0-alpha.2",
50
+ "@lunora/server": "1.0.0-alpha.17"
51
51
  },
52
52
  "engines": {
53
53
  "node": "^22.15.0 || >=24.11.0"
@@ -1,32 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const browserUserUrlWithoutAllowlist = {
4
- categories: ["SECURITY"],
5
- description: "A `ctx.browser.<method>(url, …)` call navigates to a URL derived from the handler's `args` with no server-side scoping, and no `createBrowser` allowlist contains it. The default guard blocks private targets but not arbitrary public URLs, so any caller can make the headless browser an open-proxy / SSRF tool (fetch arbitrary third-party URLs, DNS-rebind to internal hosts).",
6
- facing: "EXTERNAL",
7
- level: "WARN",
8
- name: "browser_user_url_without_allowlist",
9
- remediation: "Pin the browser with an `allowedHosts` allowlist (and/or `resolveDns`) on `createBrowser({...})`, and derive the navigation URL from server-trusted state where possible rather than passing `args` straight to `ctx.browser`.",
10
- run: (context) => {
11
- if (context.browserUrlAccesses === void 0) {
12
- return [];
13
- }
14
- const hardened = (context.configCalls ?? []).some(
15
- (call) => call.callee === "createBrowser" && call.analyzable && (call.presentKeys.includes("allowedHosts") || call.presentKeys.includes("resolveDns"))
16
- );
17
- if (hardened) {
18
- return [];
19
- }
20
- return context.browserUrlAccesses.map(
21
- (access) => emit(browserUserUrlWithoutAllowlist, {
22
- cacheKey: `browser_user_url_without_allowlist:${access.file}:${access.line.toString()}`,
23
- detail: `\`ctx.browser.${access.method}\` in \`${access.exportName}\` (${access.file}:${access.line.toString()}) navigates to a URL derived from \`args\` with no server-side scoping, and no \`createBrowser\` allowlist contains it — the default guard blocks private targets but not arbitrary public URLs, so any caller can turn the headless browser into an open-proxy / SSRF tool. Pin \`allowedHosts\` (and/or \`resolveDns\`) on \`createBrowser({...})\`, and derive the URL from server-trusted state where possible.`,
24
- metadata: { exportName: access.exportName, file: access.file, line: access.line, method: access.method }
25
- })
26
- );
27
- },
28
- source: "static",
29
- title: "Browser navigates to arg-derived URL with no allowlist"
30
- };
31
-
32
- export { browserUserUrlWithoutAllowlist as default };
@@ -1,26 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const containerInstanceKeyFromUserInput = {
4
- categories: ["SECURITY"],
5
- description: "A `ctx.containers.<name>.get` call routes to a container instance using a key derived from the handler's `args` with no server-side scoping. Any caller can supply another tenant's key and reach that tenant's container instance — a cross-tenant insecure direct object reference (IDOR).",
6
- facing: "EXTERNAL",
7
- level: "WARN",
8
- name: "container_instance_key_from_user_input",
9
- remediation: `Derive the container instance key from a server-trusted identity (e.g. \`\${ctx.auth.userId}\`) or a record the caller owns — never pass request input straight to \`ctx.containers.<name>.get\`.`,
10
- run: (context) => {
11
- if (context.containerKeyAccesses === void 0) {
12
- return [];
13
- }
14
- return context.containerKeyAccesses.map(
15
- (access) => emit(containerInstanceKeyFromUserInput, {
16
- cacheKey: `container_instance_key_from_user_input:${access.file}:${access.line.toString()}`,
17
- detail: `\`ctx.containers.*.${access.method}\` in \`${access.exportName}\` (${access.file}:${access.line.toString()}) routes to a container instance using a key derived from \`args\` with no server-side scoping — any caller can reach another tenant's container (IDOR). Derive the key from a server-trusted identity (e.g. \`\${ctx.auth.userId}\`).`,
18
- metadata: { exportName: access.exportName, file: access.file, line: access.line, method: access.method }
19
- })
20
- );
21
- },
22
- source: "static",
23
- title: "Possible cross-tenant IDOR from arg-derived unscoped container key"
24
- };
25
-
26
- export { containerInstanceKeyFromUserInput as default };
@@ -1,26 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const imagesUrlSourceFromUserInput = {
4
- categories: ["SECURITY"],
5
- description: "A `buildImageDeliveryUrl({ key, … })` call's `key` — the CDN transform's source image, an absolute URL or an origin-relative key — is derived from the handler's `args` with no server-side scoping, so any caller can point the CDN's on-the-fly transform at an attacker-chosen origin (SSRF / open proxy) or an arbitrary key under the account's own store.",
6
- facing: "EXTERNAL",
7
- level: "WARN",
8
- name: "images_url_source_from_user_input",
9
- remediation: "Validate `key` against an allowlist of known origins/prefixes before calling `buildImageDeliveryUrl`, or derive it from server-trusted state (a stored record's own image key) rather than passing `args` straight through.",
10
- run: (context) => {
11
- if (context.imageDeliveryUrlAccesses === void 0) {
12
- return [];
13
- }
14
- return context.imageDeliveryUrlAccesses.map(
15
- (access) => emit(imagesUrlSourceFromUserInput, {
16
- cacheKey: `images_url_source_from_user_input:${access.file}:${access.line.toString()}`,
17
- detail: `\`buildImageDeliveryUrl\` in \`${access.exportName}\` (${access.file}:${access.line.toString()}) builds its delivery URL from a \`key\` derived from \`args\` with no server-side scoping — any caller can point the CDN's on-the-fly transform at an attacker-chosen origin or store key. Validate \`key\` against an allowlist, or derive it from server-trusted state.`,
18
- metadata: { exportName: access.exportName, file: access.file, line: access.line }
19
- })
20
- );
21
- },
22
- source: "static",
23
- title: "Image delivery URL built from unscoped user input"
24
- };
25
-
26
- export { imagesUrlSourceFromUserInput as default };
@@ -1,26 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const kvUnscopedUserKeyIdor = {
4
- categories: ["SECURITY"],
5
- description: "A `ctx.kv` read/write uses a namespace key derived from the handler's `args` with no server-side scoping. Workers KV is a flat namespace, so an unscoped key lets any caller read, overwrite, or delete another user's entry — an insecure direct object reference (IDOR).",
6
- facing: "EXTERNAL",
7
- level: "ERROR",
8
- name: "kv_unscoped_user_key_idor",
9
- remediation: `Prefix every \`ctx.kv\` key with a server-trusted identity (e.g. \`\${ctx.auth.userId}:\${args.id}\`) so a caller can only address their own entries. Never pass request input straight to \`ctx.kv.get\`/\`put\`/\`delete\`.`,
10
- run: (context) => {
11
- if (context.kvKeyAccesses === void 0) {
12
- return [];
13
- }
14
- return context.kvKeyAccesses.map(
15
- (access) => emit(kvUnscopedUserKeyIdor, {
16
- cacheKey: `kv_unscoped_user_key_idor:${access.file}:${access.line.toString()}`,
17
- detail: `\`ctx.kv.${access.method}\` in \`${access.exportName}\` (${access.file}:${access.line.toString()}) uses a KV key derived from \`args\` with no server-side scoping — any caller can read/overwrite/delete another user's entry (IDOR). Prefix the key with a server-trusted identity (e.g. \`\${ctx.auth.userId}:…\`).`,
18
- metadata: { exportName: access.exportName, file: access.file, line: access.line, method: access.method }
19
- })
20
- );
21
- },
22
- source: "static",
23
- title: "Possible IDOR from arg-derived unscoped KV key"
24
- };
25
-
26
- export { kvUnscopedUserKeyIdor as default };
@@ -1,26 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const storageKeyFromUserArgs = {
4
- categories: ["SECURITY"],
5
- description: "A `ctx.storage.*` call uses an R2 object key taken directly from the handler's `args` with no server-side scoping. The bucket methods key by the caller-supplied string, so any caller can read, overwrite, or delete another user's object — object-level IDOR.",
6
- facing: "EXTERNAL",
7
- level: "ERROR",
8
- name: "storage_key_from_user_args",
9
- remediation: `Prefix the object key with a server-trusted identity (e.g. \`\${ctx.auth.userId}/…\`) or resolve the object through a record the caller is known to own. Never pass request input straight through as an R2 object key.`,
10
- run: (context) => {
11
- if (context.storageKeyAccesses === void 0) {
12
- return [];
13
- }
14
- return context.storageKeyAccesses.map(
15
- (access) => emit(storageKeyFromUserArgs, {
16
- cacheKey: `storage_key_from_user_args:${access.file}:${access.line.toString()}`,
17
- detail: `\`ctx.storage.*.${access.method}\` in \`${access.exportName}\` (${access.file}:${access.line.toString()}) uses an object key derived from \`args\` with no server-side scoping — any caller can read/overwrite/delete another user's object (IDOR). Prefix the key with a server-trusted identity (e.g. \`\${ctx.auth.userId}/…\`) or resolve the object through an owned record.`,
18
- metadata: { exportName: access.exportName, file: access.file, line: access.line, method: access.method }
19
- })
20
- );
21
- },
22
- source: "static",
23
- title: "R2 object key taken directly from user args (IDOR)"
24
- };
25
-
26
- export { storageKeyFromUserArgs as default };
@@ -1,26 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const vectorsNamespaceFromUserInput = {
4
- categories: ["SECURITY"],
5
- description: "A `ctx.vectors.query`/`upsert`/`upsertMany` call uses a `namespace` derived from the handler's `args` with no server-side scoping. A Vectorize namespace partitions one index into isolated sub-collections, so an unscoped namespace lets any caller read or poison another tenant's vectors.",
6
- facing: "EXTERNAL",
7
- level: "WARN",
8
- name: "vectors_namespace_from_user_input",
9
- remediation: `Derive the \`namespace\` from a server-trusted identity (e.g. \`\${ctx.auth.orgId}\`), never from \`args\`.`,
10
- run: (context) => {
11
- if (context.vectorNamespaceAccesses === void 0) {
12
- return [];
13
- }
14
- return context.vectorNamespaceAccesses.map(
15
- (access) => emit(vectorsNamespaceFromUserInput, {
16
- cacheKey: `vectors_namespace_from_user_input:${access.file}:${access.line.toString()}`,
17
- detail: `\`ctx.vectors.${access.method}\` in \`${access.exportName}\` (${access.file}:${access.line.toString()}) uses a Vectorize namespace derived from \`args\` with no server-side scoping — any caller can read or poison another tenant's vectors. Derive the namespace from a server-trusted identity (e.g. \`\${ctx.auth.orgId}\`), never from \`args\`.`,
18
- metadata: { exportName: access.exportName, file: access.file, line: access.line, method: access.method }
19
- })
20
- );
21
- },
22
- source: "static",
23
- title: "Vectorize namespace derived from unscoped user input"
24
- };
25
-
26
- export { vectorsNamespaceFromUserInput as default };