@lunora/advisor 1.0.0-alpha.34 → 1.0.0-alpha.36

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 (201) hide show
  1. package/dist/index.d.mts +61 -1
  2. package/dist/index.d.ts +61 -1
  3. package/dist/index.mjs +1 -200
  4. package/dist/packem_shared/AE_METRIC_EVENTS-LP_0D7Uw.mjs +1 -0
  5. package/dist/packem_shared/actionFetchSsrf-BbWxy1Yf.mjs +1 -0
  6. package/dist/packem_shared/adminRouteWithoutGuard-DSo4t9j4.mjs +1 -0
  7. package/dist/packem_shared/aiRawRunEscapeHatch-DfhjYm43.mjs +1 -0
  8. package/dist/packem_shared/aiToolSideEffectPromptInjection-DnWQOoKi.mjs +1 -0
  9. package/dist/packem_shared/aiUnboundedGenerationPublic-D3HZrh5_.mjs +1 -0
  10. package/dist/packem_shared/allowUnauthenticatedShardAccessEnabled-D4aMNBb0.mjs +1 -0
  11. package/dist/packem_shared/argument-derived-sink-BpO3NPPM.mjs +1 -0
  12. package/dist/packem_shared/authApiCallWithoutHeaders-BItMMdZP.mjs +1 -0
  13. package/dist/packem_shared/authCsrfCheckDisabled-De9ammtV.mjs +1 -0
  14. package/dist/packem_shared/authEmailVerificationDisabled-BFKdJcx4.mjs +1 -0
  15. package/dist/packem_shared/authSecureCookiesDisabled-B4WIzvMm.mjs +1 -0
  16. package/dist/packem_shared/authSessionFreshageZero-Bpcaf_Pe.mjs +1 -0
  17. package/dist/packem_shared/authTrustedOriginsWildcard-DjU9NXEm.mjs +1 -0
  18. package/dist/packem_shared/browserAllowPrivateTargets-Dxyeh6lg.mjs +1 -0
  19. package/dist/packem_shared/browserUserUrlWithoutAllowlist-BHBS6b0N.mjs +1 -0
  20. package/dist/packem_shared/circularFk-BMuJKmuZ.mjs +1 -0
  21. package/dist/packem_shared/constraintValidator-N339mfyx.mjs +1 -0
  22. package/dist/packem_shared/containerInstanceKeyFromUserInput-C8S4T3ty.mjs +1 -0
  23. package/dist/packem_shared/containerOversizedInstance-B9wW_sU5.mjs +1 -0
  24. package/dist/packem_shared/containerPublicInternet-Bx2J7tha.mjs +1 -0
  25. package/dist/packem_shared/containerRuntimeEgressRelaxation-BQ2ZD1aV.mjs +1 -0
  26. package/dist/packem_shared/containerStartEnableInternetOverride-B62WUura.mjs +1 -0
  27. package/dist/packem_shared/dedupeCacheKeys-7JgxaQWV.mjs +1 -0
  28. package/dist/packem_shared/duplicateIndex-DFT7qLQD.mjs +1 -0
  29. package/dist/packem_shared/emptyIndex-BiXEfyfN.mjs +1 -0
  30. package/dist/packem_shared/exportSinkMisconfigured-DvpAaa6j.mjs +1 -0
  31. package/dist/packem_shared/externalSourceIncrementalNoDeletePath-DnoL64YY.mjs +1 -0
  32. package/dist/packem_shared/externalSourceOnGlobal-DNPLoNUg.mjs +1 -0
  33. package/dist/packem_shared/externalSourceUnscoped-DqBp5jYr.mjs +1 -0
  34. package/dist/packem_shared/filterWithoutIndex-ht2GZQWV.mjs +1 -0
  35. package/dist/packem_shared/finding-BAVgqnCF.mjs +1 -0
  36. package/dist/packem_shared/fk-index-Bmk19RvQ.mjs +1 -0
  37. package/dist/packem_shared/flagGatesSecurityWithUnsafeDefault-DPGFEHTt.mjs +1 -0
  38. package/dist/packem_shared/fromServerSchema-CJ-AQ2kw.mjs +1 -0
  39. package/dist/packem_shared/geoIndexFieldNotGeopoint-DAGLZtaH.mjs +1 -0
  40. package/dist/packem_shared/geoIndexUnused-BgDE6_AR.mjs +1 -0
  41. package/dist/packem_shared/hardcodedSecret-rIRc3aNk.mjs +1 -0
  42. package/dist/packem_shared/helpers-DwlvEUCh.mjs +1 -0
  43. package/dist/packem_shared/hotShard-DVs_U17R.mjs +1 -0
  44. package/dist/packem_shared/httpActionMissingAuthGuard-BZDdeW_D.mjs +1 -0
  45. package/dist/packem_shared/httpActionResponseHeaderInjection-D063rB7U.mjs +1 -0
  46. package/dist/packem_shared/hyperdriveOutsideAction-CRmOQz67.mjs +1 -0
  47. package/dist/packem_shared/identityUndeclaredClaimTrusted-BYRYGQ74.mjs +1 -0
  48. package/dist/packem_shared/imagesUrlSourceFromUserInput-Cy5pc_dA.mjs +1 -0
  49. package/dist/packem_shared/indexReferencesUnknownField-r3QHlaz6.mjs +1 -0
  50. package/dist/packem_shared/indexUtilization-QfbIH_w4.mjs +1 -0
  51. package/dist/packem_shared/insertManyUnsafeUserData-DrH7PdRs.mjs +1 -0
  52. package/dist/packem_shared/kvUnscopedUserKeyIdor-B4lGxRxh.mjs +1 -0
  53. package/dist/packem_shared/mailInboundDispatchWithoutVerify-BivYJ5F9.mjs +1 -0
  54. package/dist/packem_shared/mailRecipientFromRequestInput-BR_oSh65.mjs +1 -0
  55. package/dist/packem_shared/maskUncoveredPiiColumn-DqPfZM2O.mjs +1 -0
  56. package/dist/packem_shared/maskWeakHashStrategyOnPii-CEiiGgT7.mjs +1 -0
  57. package/dist/packem_shared/maskedRelationLeakViaWith-CxYk0MDW.mjs +1 -0
  58. package/dist/packem_shared/mutatorFullRowReplace-CEgecSGm.mjs +1 -0
  59. package/dist/packem_shared/nondeterministicQueryMutation-BaMfbAk_.mjs +1 -0
  60. package/dist/packem_shared/normalizeIdUsedAsAuthorization-FjISSXJm.mjs +1 -0
  61. package/dist/packem_shared/notifyMissingPushConfig-C_GbVw3S.mjs +1 -0
  62. package/dist/packem_shared/notifySendOutsideAction-B0jZuUP3.mjs +1 -0
  63. package/dist/packem_shared/outputProjectionMissingOnPublicRead-9Pm9766s.mjs +1 -0
  64. package/dist/packem_shared/ownerFieldFromArgsNotAuth-BoR9vvst.mjs +1 -0
  65. package/dist/packem_shared/paymentCreateWithoutAuthorize-CurtqN8K.mjs +1 -0
  66. package/dist/packem_shared/paymentWebhookWideTolerance-I7ingfR8.mjs +1 -0
  67. package/dist/packem_shared/plaintextSecretInWranglerVariables-DXjKIm8H.mjs +1 -0
  68. package/dist/packem_shared/policyReferencesUnknownTable-D3WelAQa.mjs +1 -0
  69. package/dist/packem_shared/privilegedDispatchUnvalidatedPayload-74GuGye1.mjs +1 -0
  70. package/dist/packem_shared/privilegedFanoutFromPublicProcedure-C2j2GSUN.mjs +1 -0
  71. package/dist/packem_shared/publicArgumentUsesAny-BJb4cTqL.mjs +1 -0
  72. package/dist/packem_shared/publicMutationWithoutRatelimit-D9phluHW.mjs +1 -0
  73. package/dist/packem_shared/publicTableRlsOptoutConfusion-CaWtPGAn.mjs +1 -0
  74. package/dist/packem_shared/queueWithoutDlq-BLY3iy9Q.mjs +1 -0
  75. package/dist/packem_shared/r2sqlOutsideAction-CKK_I1Ms.mjs +1 -0
  76. package/dist/packem_shared/ratelimitDefaultMemoryStore-mOdI3Jmb.mjs +1 -0
  77. package/dist/packem_shared/ratelimitKeySpoofableOrGlobal-DaGId_Fh.mjs +1 -0
  78. package/dist/packem_shared/ratelimitMiddlewareFailOpen-CTWVosEc.mjs +1 -0
  79. package/dist/packem_shared/relationReferencesUnknownField-clqyB69Y.mjs +1 -0
  80. package/dist/packem_shared/relationReferencesUnknownTable-CLdrhkmS.mjs +1 -0
  81. package/dist/packem_shared/rlsUncoveredTable-DYHgnoos.mjs +1 -0
  82. package/dist/packem_shared/shapeTargetsGlobalTable-Blz-aSkg.mjs +1 -0
  83. package/dist/packem_shared/shapeUnknownTable-DFxtyFio.mjs +1 -0
  84. package/dist/packem_shared/signupMutationWithoutDisposableGating-3ir808Vz.mjs +1 -0
  85. package/dist/packem_shared/softDeleteIncludeDeletedFromArgs-DgxpQHRA.mjs +1 -0
  86. package/dist/packem_shared/sqlInjectionRisk-D2NaT-5c.mjs +1 -0
  87. package/dist/packem_shared/storageGenerateUploadUrlNoContentTypePin-C9cjl6Jh.mjs +1 -0
  88. package/dist/packem_shared/storageKeyFromUserArgs-BeS5cp56.mjs +1 -0
  89. package/dist/packem_shared/storagePresignedUrlForPrivateContent-j7VhwIdu.mjs +1 -0
  90. package/dist/packem_shared/storageUploadWithoutContentTypeAllowlist-oxKLecuf.mjs +1 -0
  91. package/dist/packem_shared/storageUploadWithoutMaxSize-DUDxPutk.mjs +1 -0
  92. package/dist/packem_shared/tableWithoutInsert-D3AhaNzR.mjs +1 -0
  93. package/dist/packem_shared/ttlFieldNotTimestamp-BFiRkSZD.mjs +1 -0
  94. package/dist/packem_shared/unboundedStringArgument-AkkIcr-S.mjs +1 -0
  95. package/dist/packem_shared/unindexedForeignKey-DK4eWXkb.mjs +1 -0
  96. package/dist/packem_shared/unindexedRelationTarget-B5fFq1E8.mjs +1 -0
  97. package/dist/packem_shared/unrestrictedWhereBranch-YKiydie4.mjs +1 -0
  98. package/dist/packem_shared/userCreatingMutationWithoutCaptcha-iDjRkUFO.mjs +1 -0
  99. package/dist/packem_shared/vectorsNamespaceFromUserInput-Cyq5B2kF.mjs +1 -0
  100. package/dist/packem_shared/workflowDuplicateStepName-cXF6Rpvg.mjs +1 -0
  101. package/dist/packem_shared/workflowUnknownTarget-JF8rITLP.mjs +1 -0
  102. package/dist/packem_shared/workflowUnused-Dx_FQwGE.mjs +1 -0
  103. package/package.json +2 -2
  104. package/dist/packem_shared/AE_METRIC_EVENTS-BM14d0lm.mjs +0 -87
  105. package/dist/packem_shared/actionFetchSsrf-wbmYgzmz.mjs +0 -26
  106. package/dist/packem_shared/adminRouteWithoutGuard-UUGBkAjU.mjs +0 -33
  107. package/dist/packem_shared/aiRawRunEscapeHatch-C29jd32J.mjs +0 -26
  108. package/dist/packem_shared/aiToolSideEffectPromptInjection-BtD3alqB.mjs +0 -33
  109. package/dist/packem_shared/aiUnboundedGenerationPublic-CbAcSxLB.mjs +0 -26
  110. package/dist/packem_shared/allowUnauthenticatedShardAccessEnabled-BcASq-jP.mjs +0 -30
  111. package/dist/packem_shared/argument-derived-sink-C1xTTqAt.mjs +0 -30
  112. package/dist/packem_shared/authApiCallWithoutHeaders-BeJhCZaf.mjs +0 -38
  113. package/dist/packem_shared/authCsrfCheckDisabled-DCf9FSoD.mjs +0 -26
  114. package/dist/packem_shared/authEmailVerificationDisabled-Dk-Tc5oU.mjs +0 -26
  115. package/dist/packem_shared/authSecureCookiesDisabled-osJrHW9Y.mjs +0 -26
  116. package/dist/packem_shared/authSessionFreshageZero-CjufCc_Q.mjs +0 -26
  117. package/dist/packem_shared/authTrustedOriginsWildcard-ylv4PKzC.mjs +0 -26
  118. package/dist/packem_shared/browserAllowPrivateTargets-CBvQxLAM.mjs +0 -26
  119. package/dist/packem_shared/browserUserUrlWithoutAllowlist-CvhA6w59.mjs +0 -26
  120. package/dist/packem_shared/circularFk-CNcAVuYa.mjs +0 -127
  121. package/dist/packem_shared/constraintValidator-CxwtpJ6E.mjs +0 -206
  122. package/dist/packem_shared/containerInstanceKeyFromUserInput-BUXj2J4w.mjs +0 -19
  123. package/dist/packem_shared/containerOversizedInstance-5U1VKPRM.mjs +0 -36
  124. package/dist/packem_shared/containerPublicInternet-CuNerJE5.mjs +0 -30
  125. package/dist/packem_shared/containerRuntimeEgressRelaxation-B0LlfmDA.mjs +0 -26
  126. package/dist/packem_shared/containerStartEnableInternetOverride-DDaHZQ1L.mjs +0 -26
  127. package/dist/packem_shared/dedupeCacheKeys-r5B7u_yq.mjs +0 -13
  128. package/dist/packem_shared/duplicateIndex-BOublMSt.mjs +0 -57
  129. package/dist/packem_shared/emptyIndex-BX8EuEY7.mjs +0 -32
  130. package/dist/packem_shared/exportSinkMisconfigured-897sVMUa.mjs +0 -46
  131. package/dist/packem_shared/externalSourceIncrementalNoDeletePath-DvX9cRBD.mjs +0 -45
  132. package/dist/packem_shared/externalSourceOnGlobal-Bg-NfCX9.mjs +0 -30
  133. package/dist/packem_shared/externalSourceUnscoped-5vT-Bup3.mjs +0 -44
  134. package/dist/packem_shared/filterWithoutIndex-BYVeJaSs.mjs +0 -31
  135. package/dist/packem_shared/finding-Dm_zvzS1.mjs +0 -16
  136. package/dist/packem_shared/fk-index-IUK1ukgs.mjs +0 -7
  137. package/dist/packem_shared/flagGatesSecurityWithUnsafeDefault-BhIs0shr.mjs +0 -41
  138. package/dist/packem_shared/fromServerSchema-CVnbSf8j.mjs +0 -70
  139. package/dist/packem_shared/geoIndexFieldNotGeopoint-CcaInLhY.mjs +0 -40
  140. package/dist/packem_shared/geoIndexUnused-D7I0UyND.mjs +0 -40
  141. package/dist/packem_shared/hardcodedSecret-Be-pKVdn.mjs +0 -31
  142. package/dist/packem_shared/helpers-BySnKhVB.mjs +0 -31
  143. package/dist/packem_shared/hotShard-CkC7qpre.mjs +0 -60
  144. package/dist/packem_shared/httpActionMissingAuthGuard-CxipddNx.mjs +0 -35
  145. package/dist/packem_shared/httpActionResponseHeaderInjection-DOFS7pFT.mjs +0 -39
  146. package/dist/packem_shared/hyperdriveOutsideAction-BgZqX7Xg.mjs +0 -30
  147. package/dist/packem_shared/identityUndeclaredClaimTrusted-D8nXV2dd.mjs +0 -32
  148. package/dist/packem_shared/imagesUrlSourceFromUserInput-YcDQ0b_Y.mjs +0 -19
  149. package/dist/packem_shared/indexReferencesUnknownField-BSWNngxX.mjs +0 -36
  150. package/dist/packem_shared/indexUtilization-B5DMQ3bI.mjs +0 -45
  151. package/dist/packem_shared/insertManyUnsafeUserData-Dn77XpmX.mjs +0 -26
  152. package/dist/packem_shared/kvUnscopedUserKeyIdor-YWwmfE8X.mjs +0 -19
  153. package/dist/packem_shared/mailInboundDispatchWithoutVerify-CHSRA8zz.mjs +0 -28
  154. package/dist/packem_shared/mailRecipientFromRequestInput-Cka2qu5J.mjs +0 -26
  155. package/dist/packem_shared/maskUncoveredPiiColumn-DjGIPG6M.mjs +0 -61
  156. package/dist/packem_shared/maskWeakHashStrategyOnPii-1c4q8Opf.mjs +0 -35
  157. package/dist/packem_shared/maskedRelationLeakViaWith-CPI4s0sl.mjs +0 -76
  158. package/dist/packem_shared/mutatorFullRowReplace-BJnNDaIV.mjs +0 -26
  159. package/dist/packem_shared/nondeterministicQueryMutation-GXES1fLp.mjs +0 -35
  160. package/dist/packem_shared/normalizeIdUsedAsAuthorization-BVPtCpzT.mjs +0 -50
  161. package/dist/packem_shared/notifyMissingPushConfig-Ca9EzRzy.mjs +0 -27
  162. package/dist/packem_shared/notifySendOutsideAction-BUZSIZa4.mjs +0 -30
  163. package/dist/packem_shared/outputProjectionMissingOnPublicRead-Bl5IMx0k.mjs +0 -51
  164. package/dist/packem_shared/ownerFieldFromArgsNotAuth-mOw3hE5z.mjs +0 -26
  165. package/dist/packem_shared/paymentCreateWithoutAuthorize-BYm4JLxo.mjs +0 -26
  166. package/dist/packem_shared/paymentWebhookWideTolerance-D9QQJMJq.mjs +0 -35
  167. package/dist/packem_shared/plaintextSecretInWranglerVariables-NKO4YkKf.mjs +0 -35
  168. package/dist/packem_shared/policyReferencesUnknownTable-DtaIEovd.mjs +0 -38
  169. package/dist/packem_shared/privilegedDispatchUnvalidatedPayload-5Forjckt.mjs +0 -37
  170. package/dist/packem_shared/privilegedFanoutFromPublicProcedure-D3dL01B8.mjs +0 -26
  171. package/dist/packem_shared/publicArgumentUsesAny-C71b2NCf.mjs +0 -32
  172. package/dist/packem_shared/publicMutationWithoutRatelimit-DbIhgi7j.mjs +0 -36
  173. package/dist/packem_shared/publicTableRlsOptoutConfusion-Bn71yoD4.mjs +0 -38
  174. package/dist/packem_shared/queueWithoutDlq-CSkGNb_0.mjs +0 -41
  175. package/dist/packem_shared/r2sqlOutsideAction-CtqxvMuV.mjs +0 -30
  176. package/dist/packem_shared/ratelimitDefaultMemoryStore-BISChG5C.mjs +0 -26
  177. package/dist/packem_shared/ratelimitKeySpoofableOrGlobal-DqlHYQQ3.mjs +0 -26
  178. package/dist/packem_shared/ratelimitMiddlewareFailOpen-CJgDCaUw.mjs +0 -33
  179. package/dist/packem_shared/relationReferencesUnknownField-CjbLScJ1.mjs +0 -54
  180. package/dist/packem_shared/relationReferencesUnknownTable-DrorpKYe.mjs +0 -33
  181. package/dist/packem_shared/rlsUncoveredTable-CxEfZ5eZ.mjs +0 -56
  182. package/dist/packem_shared/shapeTargetsGlobalTable-DHrf4Koi.mjs +0 -34
  183. package/dist/packem_shared/shapeUnknownTable-C8aDWFoe.mjs +0 -34
  184. package/dist/packem_shared/signupMutationWithoutDisposableGating-Dnbol2CS.mjs +0 -39
  185. package/dist/packem_shared/softDeleteIncludeDeletedFromArgs-BLqDKrkM.mjs +0 -38
  186. package/dist/packem_shared/sqlInjectionRisk-zwytYGLt.mjs +0 -26
  187. package/dist/packem_shared/storageGenerateUploadUrlNoContentTypePin-Da4L9Ge8.mjs +0 -26
  188. package/dist/packem_shared/storageKeyFromUserArgs-B86elJgS.mjs +0 -19
  189. package/dist/packem_shared/storagePresignedUrlForPrivateContent-yGmb8uqz.mjs +0 -47
  190. package/dist/packem_shared/storageUploadWithoutContentTypeAllowlist-BV-gF1lT.mjs +0 -27
  191. package/dist/packem_shared/storageUploadWithoutMaxSize-DpxO59wU.mjs +0 -27
  192. package/dist/packem_shared/tableWithoutInsert-CbbaYIP4.mjs +0 -34
  193. package/dist/packem_shared/ttlFieldNotTimestamp-Dpaj63Z6.mjs +0 -36
  194. package/dist/packem_shared/unboundedStringArgument-DThg2-wt.mjs +0 -32
  195. package/dist/packem_shared/unindexedForeignKey-BgJbKyqK.mjs +0 -45
  196. package/dist/packem_shared/unindexedRelationTarget-D6eyj6Xx.mjs +0 -53
  197. package/dist/packem_shared/userCreatingMutationWithoutCaptcha-2DZtWIPb.mjs +0 -42
  198. package/dist/packem_shared/vectorsNamespaceFromUserInput-CQhr5bVn.mjs +0 -19
  199. package/dist/packem_shared/workflowDuplicateStepName-ioBxPBCy.mjs +0 -48
  200. package/dist/packem_shared/workflowUnknownTarget-Cdd7WhKQ.mjs +0 -34
  201. package/dist/packem_shared/workflowUnused-D0jHxdz9.mjs +0 -38
@@ -1,28 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const mailInboundDispatchWithoutVerify = {
4
- categories: ["SECURITY"],
5
- description: "`createInboundEmailHandler({...})` has no `verify` hook, so it trusts every inbound message — the `from` address is spoofable and DKIM/SPF/DMARC verdicts go unchecked. If the handler dispatches into a Lunora function (admin bearer, RLS disabled), a forged sender can drive privileged writes (a confused-deputy escalation).",
6
- facing: "EXTERNAL",
7
- level: "ERROR",
8
- // eslint-disable-next-line no-secrets/no-secrets -- the lint's rule id, not a credential
9
- name: "mail_inbound_dispatch_without_verify",
10
- remediation: "Pass a `verify` hook to `createInboundEmailHandler({...})` that rejects the message unless its DKIM/SPF/DMARC authentication verdicts pass and the sender is expected — before any dispatch into a Lunora function. Never trust the `from` header alone.",
11
- run: (context) => {
12
- if (context.configCalls === void 0) {
13
- return [];
14
- }
15
- return context.configCalls.filter((call) => call.callee === "createInboundEmailHandler" && call.analyzable && !call.presentKeys.includes("verify")).map(
16
- (call) => emit(mailInboundDispatchWithoutVerify, {
17
- // eslint-disable-next-line no-secrets/no-secrets -- the lint's rule id, not a credential
18
- cacheKey: `mail_inbound_dispatch_without_verify:${call.file}:${call.line.toString()}`,
19
- detail: `\`createInboundEmailHandler({...})\` in ${call.file}:${call.line.toString()} has no \`verify\` hook — it trusts the spoofable \`from\` address and unchecked DKIM/SPF/DMARC verdicts. A forged sender can then reach whatever the handler dispatches to (a Lunora function runs with the admin bearer and RLS disabled). Add a \`verify\` gate that enforces the authentication verdicts before dispatch.`,
20
- metadata: { callee: call.callee, file: call.file, line: call.line }
21
- })
22
- );
23
- },
24
- source: "static",
25
- title: "Inbound email handler without a verify hook"
26
- };
27
-
28
- export { mailInboundDispatchWithoutVerify as default };
@@ -1,26 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const mailRecipientFromRequestInput = {
4
- categories: ["SECURITY"],
5
- description: "A `ctx.mail`/`ctx.email` `send`/`queue` call sets a recipient field (to/cc/bcc) from the handler's `args` with no server-side scoping. Sending to an address taken straight from request input turns the deployment into an open relay / spam amplifier.",
6
- facing: "EXTERNAL",
7
- level: "WARN",
8
- name: "mail_recipient_from_request_input",
9
- remediation: "Derive recipients from server-trusted state (e.g. the authenticated user's own record via `ctx.auth`), never straight from `args`. If user-chosen recipients are intended, gate the path behind authentication and rate limiting.",
10
- run: (context) => {
11
- if (context.mailRecipientAccesses === void 0) {
12
- return [];
13
- }
14
- return context.mailRecipientAccesses.map(
15
- (access) => emit(mailRecipientFromRequestInput, {
16
- cacheKey: `mail_recipient_from_request_input:${access.file}:${access.line.toString()}`,
17
- detail: `\`ctx.mail.${access.method}\` in \`${access.exportName}\` (${access.file}:${access.line.toString()}) sets a recipient field (to/cc/bcc) derived from \`args\` with no server-side scoping — any caller can direct mail to an arbitrary address (open relay / spam amplifier). Derive the recipient from server-trusted state instead (e.g. \`ctx.auth.user.email\`).`,
18
- metadata: { exportName: access.exportName, file: access.file, line: access.line, method: access.method }
19
- })
20
- );
21
- },
22
- source: "static",
23
- title: "Mail recipient derived from request input"
24
- };
25
-
26
- export { mailRecipientFromRequestInput as default };
@@ -1,61 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const maskUncoveredPiiColumn = {
4
- categories: ["SECURITY"],
5
- description: "A public procedure reads a table whose columns are masked by `.use(mask(...))` elsewhere in the app, but this procedure's builder chain does not include `.use(mask(...))`. It returns those columns in the clear while a sibling procedure redacts them.",
6
- facing: "EXTERNAL",
7
- level: "WARN",
8
- name: "mask_uncovered_pii_column",
9
- remediation: "Add `.use(mask(policies))` to the procedure's builder chain — e.g. `c.use(mask(myMasks)).query(...)` — so its returned rows redact the same columns as the rest of the app. If this procedure is intentionally privileged (e.g. an admin export), declare it with `internalQuery` / `internalMutation` / `internalAction` instead of the public builder so the intent is explicit.",
10
- // eslint-disable-next-line sonarjs/cognitive-complexity -- the procedure/table/column cross-reference reads clearest as one linear pass; splitting it would obscure the flow.
11
- run: (context) => {
12
- if (context.maskProcedures === void 0) {
13
- return [];
14
- }
15
- const maskedColumnsByTable = /* @__PURE__ */ new Map();
16
- for (const procedure of context.maskProcedures) {
17
- for (const { column, table } of procedure.maskColumns) {
18
- if (table === "" || column === "") {
19
- continue;
20
- }
21
- const columns = maskedColumnsByTable.get(table) ?? /* @__PURE__ */ new Set();
22
- columns.add(column);
23
- maskedColumnsByTable.set(table, columns);
24
- }
25
- }
26
- if (maskedColumnsByTable.size === 0) {
27
- return [];
28
- }
29
- const findings = [];
30
- for (const procedure of context.maskProcedures) {
31
- if (procedure.usesMask) {
32
- continue;
33
- }
34
- if (procedure.visibility === "internal") {
35
- continue;
36
- }
37
- const touched = /* @__PURE__ */ new Set();
38
- for (const table of procedure.tablesRead) {
39
- if (table !== "" && maskedColumnsByTable.has(table)) {
40
- touched.add(table);
41
- }
42
- }
43
- for (const table of touched) {
44
- const columns = [...maskedColumnsByTable.get(table) ?? /* @__PURE__ */ new Set()].toSorted((a, b) => a.localeCompare(b));
45
- const columnList = columns.map((column) => `\`${column}\``).join(", ");
46
- findings.push(
47
- emit(maskUncoveredPiiColumn, {
48
- cacheKey: `mask_uncovered_pii_column:${procedure.file}:${procedure.exportName}:${table}`,
49
- detail: `\`${procedure.exportName}\` in ${procedure.file} reads table \`${table}\` without \`.use(mask(...))\` — its column${columns.length === 1 ? "" : "s"} ${columnList} ${columns.length === 1 ? "is" : "are"} masked elsewhere in the app but returned in the clear here.`,
50
- metadata: { columns, exportName: procedure.exportName, file: procedure.file, table }
51
- })
52
- );
53
- }
54
- }
55
- return findings;
56
- },
57
- source: "static",
58
- title: "Maskable column returned without mask() middleware"
59
- };
60
-
61
- export { maskUncoveredPiiColumn as default };
@@ -1,35 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const PII_COLUMN_RE = /address|birthdate|creditcard|dateofbirth|dob|driverslicense|email|firstname|fullname|lastname|nationalid|passport|phone|socialsecurity|ssn|taxid/u;
4
- const isPiiColumn = (column) => PII_COLUMN_RE.test(column.replaceAll(/[^a-z0-9]/giu, "").toLowerCase());
5
- const maskWeakHashStrategyOnPii = {
6
- categories: ["SECURITY"],
7
- description: 'A `mask(policies)` column named like PII (email, SSN, phone, …) uses the `"hash"` strategy. `"hash"` is an unsalted 32-bit FNV-1a digest — brute-force-recoverable and cross-row-correlatable — not a confidentiality control, so it does not actually hide the value.',
8
- facing: "EXTERNAL",
9
- level: "WARN",
10
- name: "mask_weak_hash_strategy_on_pii",
11
- remediation: 'Use `"redact"` instead of `"hash"` for this column — e.g. `mask({ <table>: { <column>: "redact" } })`. Reserve `"hash"` for columns where a stable, joinable pseudonym is the goal and the value itself is not sensitive.',
12
- run: (context) => {
13
- if (context.maskStrategies === void 0) {
14
- return [];
15
- }
16
- const findings = [];
17
- for (const strategy of context.maskStrategies) {
18
- if (strategy.strategy !== "hash" || !isPiiColumn(strategy.column)) {
19
- continue;
20
- }
21
- findings.push(
22
- emit(maskWeakHashStrategyOnPii, {
23
- cacheKey: `mask_weak_hash_strategy_on_pii:${strategy.file}:${strategy.line.toString()}`,
24
- detail: `\`${strategy.exportName}\` in ${strategy.file} masks \`${strategy.table === "" ? strategy.column : `${strategy.table}.${strategy.column}`}\` with \`"hash"\` — its name suggests PII, and \`"hash"\` is brute-force-recoverable and leaks cross-row equality. Use \`"redact"\` instead.`,
25
- metadata: { column: strategy.column, exportName: strategy.exportName, file: strategy.file, table: strategy.table }
26
- })
27
- );
28
- }
29
- return findings;
30
- },
31
- source: "static",
32
- title: 'Weak "hash" mask strategy on a PII column'
33
- };
34
-
35
- export { maskWeakHashStrategyOnPii as default };
@@ -1,76 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const maskedTablesOf = (context) => {
4
- const tables = /* @__PURE__ */ new Set();
5
- for (const procedure of context.maskProcedures ?? []) {
6
- for (const { table } of procedure.maskColumns) {
7
- if (table !== "") {
8
- tables.add(table);
9
- }
10
- }
11
- }
12
- for (const strategy of context.maskStrategies ?? []) {
13
- if (strategy.table !== "") {
14
- tables.add(strategy.table);
15
- }
16
- }
17
- return tables;
18
- };
19
- const relationTargetsOf = (context) => {
20
- const targets = /* @__PURE__ */ new Map();
21
- for (const table of context.schema.tables) {
22
- for (const relation of table.relations) {
23
- targets.set(`${table.name} ${relation.name}`, relation.table);
24
- }
25
- }
26
- return targets;
27
- };
28
- const maskedRelationLeakViaWith = {
29
- categories: ["SECURITY"],
30
- description: "A public read hydrates a masked table through a `with` relation. Column masking does not descend into `with`-hydrated relations, so the related table's masked columns are returned in the clear.",
31
- facing: "EXTERNAL",
32
- level: "INFO",
33
- name: "masked_relation_leak_via_with",
34
- remediation: "Don't hydrate a masked table through `with` on an unprotected read: drop the relation, project only non-sensitive columns, or route the parent read through a procedure that re-applies masking to the joined table. Masking is per-procedure and top-level only — it never reaches `with`-loaded relations.",
35
- run: (context) => {
36
- if (context.relationLoads === void 0) {
37
- return [];
38
- }
39
- const maskedTables = maskedTablesOf(context);
40
- if (maskedTables.size === 0) {
41
- return [];
42
- }
43
- const relationTargets = relationTargetsOf(context);
44
- const findings = [];
45
- for (const row of context.relationLoads) {
46
- if (row.visibility !== "public" || row.parentTable === "") {
47
- continue;
48
- }
49
- for (const relation of row.relations) {
50
- const target = relationTargets.get(`${row.parentTable} ${relation}`);
51
- if (target === void 0 || !maskedTables.has(target)) {
52
- continue;
53
- }
54
- findings.push(
55
- emit(maskedRelationLeakViaWith, {
56
- cacheKey: `masked_relation_leak_via_with:${row.file}:${row.line.toString()}:${row.parentTable}:${relation}`,
57
- detail: `The public read in \`${row.exportName}\` (${row.file}:${row.line.toString()}) hydrates relation \`${relation}\` (masked table \`${target}\`) via \`with\` on \`${row.parentTable}\`. Masking is top-level only and does not descend into \`with\`, so \`${target}\`'s masked columns are returned in the clear.`,
58
- metadata: {
59
- exportName: row.exportName,
60
- file: row.file,
61
- line: row.line,
62
- parentTable: row.parentTable,
63
- relation,
64
- relationTable: target
65
- }
66
- })
67
- );
68
- }
69
- }
70
- return findings;
71
- },
72
- source: "static",
73
- title: "Masked table surfaced unmasked through a with-relation on a public read"
74
- };
75
-
76
- export { maskedRelationLeakViaWith as default };
@@ -1,26 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const mutatorFullRowReplace = {
4
- categories: ["SCHEMA"],
5
- description: "A custom mutator's `server` impl writes with `ctx.db.replace(id, document)` — a whole-row overwrite. On a synced table this clobbers a concurrent edit to a different column; `ctx.db.patch(id, { field })` merges at the column level so independent field edits coexist.",
6
- facing: "EXTERNAL",
7
- level: "WARN",
8
- name: "mutator_full_row_replace",
9
- remediation: "Prefer `ctx.db.patch(id, { onlyTheChangedField })` so a concurrent edit to another column isn't lost. Keep `replace` only when the mutator genuinely owns the entire row (e.g. a full-form save).",
10
- run: (context) => {
11
- if (context.mutatorWrites === void 0) {
12
- return [];
13
- }
14
- return context.mutatorWrites.map(
15
- (write) => emit(mutatorFullRowReplace, {
16
- cacheKey: `mutator_full_row_replace:${write.exportName}:${String(write.line)}`,
17
- detail: `Mutator \`${write.exportName}\` (${write.file}:${String(write.line)}) writes with \`ctx.db.replace(...)\`, overwriting the whole row. A concurrent edit to a different column is clobbered — use \`ctx.db.patch(id, { field })\` to merge at the column level.`,
18
- metadata: { exportName: write.exportName, file: write.file, line: write.line }
19
- })
20
- );
21
- },
22
- source: "static",
23
- title: "Mutator overwrites the whole row (clobbers concurrent column edits)"
24
- };
25
-
26
- export { mutatorFullRowReplace as default };
@@ -1,35 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const nondeterministicQueryMutation = {
4
- categories: ["SCHEMA"],
5
- description: "A `query`/`mutation` handler calls a non-deterministic API (`Date.now`, `Math.random`, `crypto.randomUUID`, `crypto.getRandomValues`, or `fetch`). These handlers may be re-run on OCC retry / subscription re-evaluation, so they must be deterministic — time, randomness, and network belong in an `action`.",
6
- facing: "EXTERNAL",
7
- level: "WARN",
8
- name: "nondeterministic_query_mutation",
9
- remediation: "Move the non-deterministic call into an `action(...)` (which runs once and may use ambient APIs), then pass the computed value into the mutation as an argument — e.g. compute `Date.now()` in the action and call `ctx.runMutation(api.…, { now })`. Take generated ids/timestamps as args instead of producing them in the handler.",
10
- run: (context) => {
11
- if (context.nondeterministicCalls === void 0) {
12
- return [];
13
- }
14
- const findings = [];
15
- const occurrenceCount = /* @__PURE__ */ new Map();
16
- for (const call of context.nondeterministicCalls) {
17
- const baseKey = `${call.file}:${call.line.toString()}:${call.callee}`;
18
- const occurrence = (occurrenceCount.get(baseKey) ?? 0) + 1;
19
- occurrenceCount.set(baseKey, occurrence);
20
- const occurrenceSuffix = occurrence > 1 ? `:${occurrence.toString()}` : "";
21
- findings.push(
22
- emit(nondeterministicQueryMutation, {
23
- cacheKey: `nondeterministic_query_mutation:${baseKey}${occurrenceSuffix}`,
24
- detail: `\`${call.callee}(…)\` in ${call.exportName} (${call.file}:${call.line.toString()}) runs inside a ${call.kind} handler — query/mutation handlers must be deterministic. Compute it in an \`action\` and pass the value into the mutation as an argument.`,
25
- metadata: { callee: call.callee, exportName: call.exportName, file: call.file, kind: call.kind, line: call.line }
26
- })
27
- );
28
- }
29
- return findings;
30
- },
31
- source: "static",
32
- title: "Non-deterministic call in query/mutation handler"
33
- };
34
-
35
- export { nondeterministicQueryMutation as default };
@@ -1,50 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const SINK_LABELS = {
4
- delete: "deletes the row",
5
- get: "reads the row",
6
- patch: "patches the row"
7
- };
8
- const normalizeIdUsedAsAuthorization = {
9
- categories: ["SECURITY"],
10
- description: "A `.public()` `query`/`mutation` gates a `ctx.db.get`/`patch`/`delete` on a null-checked `ctx.db.normalizeId(table, id)` result with no ownership predicate and no RLS. `normalizeId` validates id *shape* only — it never reads the database — so a non-null result proves the id is well-formed, never that the caller owns the row (an IDOR).",
11
- facing: "EXTERNAL",
12
- level: "INFO",
13
- name: "normalize_id_used_as_authorization",
14
- remediation: 'Don\'t treat a non-null `normalizeId` result as authorization — it only validates id shape. After loading the row, compare its ownership column (`userId`/`ownerId`/…) against the server-trusted identity (`ctx.auth.userId`), or protect the procedure with `.use(rls(...))` / put the schema in `.rls("required")` mode so the row policy runs on every access.',
15
- run: (context) => {
16
- if (context.normalizeIdAuthorizations === void 0) {
17
- return [];
18
- }
19
- const rlsRequired = context.schema.rlsMode === "required";
20
- const rlsOptOutTables = new Set(context.schema.tables.filter((table) => table.isPublic).map((table) => table.name));
21
- const findings = [];
22
- for (const row of context.normalizeIdAuthorizations) {
23
- if (row.visibility !== "public" || row.usesRls || row.mentionsOwnership) {
24
- continue;
25
- }
26
- if (rlsRequired && (row.table === "" || !rlsOptOutTables.has(row.table))) {
27
- continue;
28
- }
29
- const target = row.table === "" ? "the row" : `the \`${row.table}\` row`;
30
- findings.push(
31
- emit(normalizeIdUsedAsAuthorization, {
32
- cacheKey: `normalize_id_used_as_authorization:${row.file}:${row.line.toString()}`,
33
- detail: `Public procedure \`${row.exportName}\` (${row.file}:${row.line.toString()}) gates access on \`ctx.db.normalizeId(...)\` and then ${SINK_LABELS[row.sinkMethod]} — but \`normalizeId\` only validates id shape, it never reads the database or checks ownership. Any caller supplying a well-formed id of ${target} reaches it (IDOR). Compare the loaded row's ownership column against \`ctx.auth\`, or cover the procedure with \`.use(rls(...))\`.`,
34
- metadata: {
35
- exportName: row.exportName,
36
- file: row.file,
37
- line: row.line,
38
- sinkMethod: row.sinkMethod,
39
- table: row.table
40
- }
41
- })
42
- );
43
- }
44
- return findings;
45
- },
46
- source: "static",
47
- title: "normalizeId result used as authorization (validates id shape, not ownership)"
48
- };
49
-
50
- export { normalizeIdUsedAsAuthorization as default };
@@ -1,27 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const notifyMissingPushConfig = {
4
- categories: ["SCHEMA"],
5
- description: "The app sends push notifications via `ctx.push` but `defineNotify(...)` configures neither a Web Push nor an FCM channel, so every send fails at runtime. Wire at least one push channel (`webPush` and/or `fcm`) and provide its secrets (`VAPID_*` / `FCM_*`).",
6
- facing: "INTERNAL",
7
- level: "WARN",
8
- name: "notify_missing_push_config",
9
- remediation: "In `lunora/notify.ts`, wire a push channel: `defineNotify({ webPush: (env) => webPushFromEnv(env) })` and set `VAPID_PUBLIC_KEY` / `VAPID_PRIVATE_KEY` / `VAPID_SUBJECT` in `.dev.vars` (or configure `fcm` with `FCM_PROJECT_ID`). Generate a VAPID keypair with `npx web-push generate-vapid-keys`.",
10
- run: (context) => {
11
- const config = context.notifyConfig;
12
- if (config === void 0 || !config.usesPush || config.hasWebPush || config.hasFcm) {
13
- return [];
14
- }
15
- return [
16
- emit(notifyMissingPushConfig, {
17
- cacheKey: "notify_missing_push_config",
18
- detail: "`ctx.push` is used to send notifications, but `defineNotify(...)` wires neither `webPush` nor `fcm` — every push send will fail at runtime. Configure at least one push channel and its secrets.",
19
- metadata: { hasFcm: config.hasFcm, hasWebPush: config.hasWebPush, usesPush: config.usesPush }
20
- })
21
- ];
22
- },
23
- source: "static",
24
- title: "Push used with no Web Push / FCM channel configured"
25
- };
26
-
27
- export { notifyMissingPushConfig as default };
@@ -1,30 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const notifySendOutsideAction = {
4
- categories: ["SCHEMA"],
5
- description: "A `query`/`mutation` handler sends a notification via `ctx.notify`/`ctx.push`. A send is external I/O (a `fetch` to a push service / FCM): it is non-deterministic like `fetch`, and a mutation re-run on OCC retry would re-send it (duplicate pushes). These facades are available on `ActionCtx` only and must be confined to `action` handlers.",
6
- facing: "EXTERNAL",
7
- level: "WARN",
8
- name: "notify_send_outside_action",
9
- remediation: "Move the `ctx.notify`/`ctx.push` send into an `action(...)`, where external I/O is allowed. If a query/mutation must trigger a notification, have it enqueue the send (`enqueuePushBroadcast` via `@lunora/queue`) or schedule an action — the queue/scheduler runs the send exactly once, off the transactional path.",
10
- run: (context) => {
11
- if (context.notifyCalls === void 0) {
12
- return [];
13
- }
14
- const findings = [];
15
- for (const call of context.notifyCalls) {
16
- findings.push(
17
- emit(notifySendOutsideAction, {
18
- cacheKey: `notify_send_outside_action:${call.file}:${call.line.toString()}:${call.callee}`,
19
- detail: `\`${call.callee}(…)\` in ${call.exportName} (${call.file}:${call.line.toString()}) runs inside a ${call.kind} handler — a notification send is non-deterministic external I/O and a retried ${call.kind} would re-send it. Move it into an \`action\`, or enqueue/schedule the send.`,
20
- metadata: { callee: call.callee, exportName: call.exportName, file: call.file, kind: call.kind, line: call.line }
21
- })
22
- );
23
- }
24
- return findings;
25
- },
26
- source: "static",
27
- title: "Notification send used outside an action"
28
- };
29
-
30
- export { notifySendOutsideAction as default };
@@ -1,51 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
- import { P as PII_FIELD_NAMES } from './helpers-BySnKhVB.mjs';
3
-
4
- const outputProjectionMissingOnPublicRead = {
5
- categories: ["SECURITY"],
6
- description: "A `.public()` `query` returns raw table rows (no `.output(...)` projection, no `.use(mask(...))`) from a table carrying PII-named columns (`email`, `phone`, `ssn`, …). Every column ships to the caller, and a column added to the table later leaks by default.",
7
- facing: "EXTERNAL",
8
- level: "INFO",
9
- name: "output_projection_missing_on_public_read",
10
- remediation: "Add an explicit return projection to the public query — `.output(v.object({ … }))` listing only the fields a client needs — or apply a `.use(mask(...))` policy to the PII columns. This makes the exposed shape intentional and stops a newly-added column from leaking through this query by default.",
11
- run: (context) => {
12
- if (context.rawRowReturns === void 0) {
13
- return [];
14
- }
15
- const piiColumnsByTable = /* @__PURE__ */ new Map();
16
- for (const table of context.schema.tables) {
17
- const piiColumns = table.fields.filter((field) => PII_FIELD_NAMES.has(field));
18
- if (piiColumns.length > 0) {
19
- piiColumnsByTable.set(table.name, piiColumns);
20
- }
21
- }
22
- const findings = [];
23
- for (const row of context.rawRowReturns) {
24
- if (row.visibility !== "public" || row.usesOutput || row.usesMask || row.table === "") {
25
- continue;
26
- }
27
- const piiColumns = piiColumnsByTable.get(row.table);
28
- if (piiColumns === void 0) {
29
- continue;
30
- }
31
- findings.push(
32
- emit(outputProjectionMissingOnPublicRead, {
33
- cacheKey: `output_projection_missing_on_public_read:${row.file}:${row.line.toString()}`,
34
- detail: `Public query \`${row.exportName}\` (${row.file}:${row.line.toString()}) returns raw \`${row.table}\` rows with no \`.output(...)\` projection — shipping PII column(s) ${piiColumns.join(", ")} to every caller, and any column added to \`${row.table}\` later leaks by default. Project the return with \`.output(v.object({ … }))\` or mask the PII columns.`,
35
- metadata: {
36
- columns: piiColumns,
37
- exportName: row.exportName,
38
- file: row.file,
39
- line: row.line,
40
- table: row.table
41
- }
42
- })
43
- );
44
- }
45
- return findings;
46
- },
47
- source: "static",
48
- title: "Public query returns raw rows with PII and no output projection"
49
- };
50
-
51
- export { outputProjectionMissingOnPublicRead as default };
@@ -1,26 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const ownerFieldFromArgsNotAuth = {
4
- categories: ["SECURITY"],
5
- description: "A `ctx.db` write sets an ownership/identity column (`userId`, `ownerId`, `tenantId`, …) from the handler's `args`. The caller controls who the row belongs to, so any caller can write rows owned by another user or tenant — an act-as-any-user / cross-tenant IDOR.",
6
- facing: "EXTERNAL",
7
- level: "ERROR",
8
- name: "owner_field_from_args_not_auth",
9
- remediation: "Stamp the ownership column from the server-trusted identity (`ctx.auth.userId` / `ctx.identity`), never from request input. Drop the field from the accepted `args` so a caller cannot supply it.",
10
- run: (context) => {
11
- if (context.ownerFieldWrites === void 0) {
12
- return [];
13
- }
14
- return context.ownerFieldWrites.map(
15
- (write) => emit(ownerFieldFromArgsNotAuth, {
16
- cacheKey: `owner_field_from_args_not_auth:${write.file}:${write.line.toString()}:${write.field}`,
17
- detail: `\`${write.method}\` in \`${write.exportName}\` (${write.file}:${write.line.toString()}) sets the ownership field \`${write.field}\` from \`args\` instead of the server-trusted identity — any caller can write rows owned by another user/tenant (IDOR). Stamp \`${write.field}\` from \`ctx.auth\`/\`ctx.identity\`, never from request input.`,
18
- metadata: { exportName: write.exportName, field: write.field, file: write.file, line: write.line, method: write.method }
19
- })
20
- );
21
- },
22
- source: "static",
23
- title: "Ownership field written from args, not server identity"
24
- };
25
-
26
- export { ownerFieldFromArgsNotAuth as default };
@@ -1,26 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const paymentCreateWithoutAuthorize = {
4
- categories: ["SECURITY"],
5
- description: "`createPayment({...})` was constructed without an `authorize` callback, so its access-control gate never runs — any caller that reaches a payment reference id can drive a charge, refund, or subscription change against it (a broken-access-control / IDOR risk on money movement).",
6
- facing: "EXTERNAL",
7
- level: "ERROR",
8
- name: "payment_create_without_authorize",
9
- remediation: "Pass an `authorize(referenceId)` callback to `createPayment({...})` that verifies the current caller (via `ctx.auth`) owns the payment/subscription reference before the operation runs. Prefer the `ctx.payments` accessor, which is authorization-scoped by default.",
10
- run: (context) => {
11
- if (context.configCalls === void 0) {
12
- return [];
13
- }
14
- return context.configCalls.filter((call) => call.callee === "createPayment" && call.analyzable && !call.presentKeys.includes("authorize")).map(
15
- (call) => emit(paymentCreateWithoutAuthorize, {
16
- cacheKey: `payment_create_without_authorize:${call.file}:${call.line.toString()}`,
17
- detail: `\`createPayment({...})\` in ${call.file}:${call.line.toString()} has no \`authorize\` callback — its access-control gate never runs, so any caller reaching the payment reference can move money against it. Add an \`authorize(referenceId)\` gate that checks \`ctx.auth\` ownership before the operation.`,
18
- metadata: { callee: call.callee, file: call.file, line: call.line }
19
- })
20
- );
21
- },
22
- source: "static",
23
- title: "createPayment without an authorize gate"
24
- };
25
-
26
- export { paymentCreateWithoutAuthorize as default };
@@ -1,35 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const WIDE_TOLERANCE_THRESHOLD_SECONDS = 60 * 60;
4
- const paymentWebhookWideTolerance = {
5
- categories: ["SECURITY"],
6
- description: "A payment webhook adapter configured with an implausibly wide `webhookToleranceSeconds` (over an hour). The timestamp tolerance is the webhook replay window; widening it far past the 300s default keeps captured signed events replayable long after capture, defeating the replay check.",
7
- facing: "EXTERNAL",
8
- level: "WARN",
9
- name: "payment_webhook_wide_tolerance",
10
- remediation: "Keep `webhookToleranceSeconds` at or near the 300s default — just enough for clock skew. If a wide window was set to work around retries or backfills, handle those with idempotency on the event id instead, not by loosening the replay-window guard.",
11
- run: (context) => {
12
- if (context.paymentWebhooks === void 0) {
13
- return [];
14
- }
15
- const isWide = (row) => row.toleranceSeconds !== void 0 && row.toleranceSeconds >= WIDE_TOLERANCE_THRESHOLD_SECONDS;
16
- return context.paymentWebhooks.filter((row) => isWide(row)).map((row) => {
17
- const location = `\`${row.exportName}\` (${row.file}:${row.line.toString()})`;
18
- return emit(paymentWebhookWideTolerance, {
19
- cacheKey: `payment_webhook_wide_tolerance:${row.file}:${row.line.toString()}`,
20
- detail: `\`${row.callee}\` in ${location} sets \`webhookToleranceSeconds\` to ${(row.toleranceSeconds ?? 0).toString()}s (default 300s). That replay window keeps captured signed events valid long after capture.`,
21
- metadata: {
22
- callee: row.callee,
23
- exportName: row.exportName,
24
- file: row.file,
25
- line: row.line,
26
- toleranceSeconds: row.toleranceSeconds
27
- }
28
- });
29
- });
30
- },
31
- source: "static",
32
- title: "Payment webhook replay-tolerance window is implausibly wide"
33
- };
34
-
35
- export { paymentWebhookWideTolerance as default };
@@ -1,35 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const plaintextSecretInWranglerVariables = {
4
- categories: ["SECURITY"],
5
- description: "A plaintext secret is committed to `wrangler.jsonc`'s `vars` block. `vars` are cleartext — baked into the deployed Worker and checked into source control — so the secret leaks via the repo and the bundle, and rotating it needs a config edit plus a redeploy.",
6
- facing: "INTERNAL",
7
- level: "ERROR",
8
- name: "plaintext_secret_in_wrangler_vars",
9
- remediation: "Move the value out of `vars`: bind it through a Secrets Store (`ctx.secrets.get(<NAME>)`) or set it with `wrangler secret put <NAME>` and read it from `env.<NAME>`. Delete the `vars` entry and rotate the exposed value — assume it is already compromised. Public/publishable keys (name them `*_PUBLIC` / `*_PUBLISHABLE`) are exempt.",
10
- run: (context) => {
11
- if (context.wranglerVariables === void 0) {
12
- return [];
13
- }
14
- const findings = [];
15
- const occurrenceCount = /* @__PURE__ */ new Map();
16
- for (const wranglerVariable of context.wranglerVariables) {
17
- const baseKey = `${wranglerVariable.file}:${wranglerVariable.key}`;
18
- const occurrence = (occurrenceCount.get(baseKey) ?? 0) + 1;
19
- occurrenceCount.set(baseKey, occurrence);
20
- const occurrenceSuffix = occurrence > 1 ? `:${occurrence.toString()}` : "";
21
- findings.push(
22
- emit(plaintextSecretInWranglerVariables, {
23
- cacheKey: `plaintext_secret_in_wrangler_vars:${baseKey}${occurrenceSuffix}`,
24
- detail: `The \`vars\` entry \`${wranglerVariable.key}\` in ${wranglerVariable.file} holds a plaintext ${wranglerVariable.kind.replaceAll("_", " ")} (${wranglerVariable.preview}). \`vars\` ship in cleartext to the deployed Worker and are committed to source control — move it to a Secrets Store binding or \`wrangler secret put\` and rotate the exposed value.`,
25
- metadata: { file: wranglerVariable.file, key: wranglerVariable.key, kind: wranglerVariable.kind, preview: wranglerVariable.preview }
26
- })
27
- );
28
- }
29
- return findings;
30
- },
31
- source: "static",
32
- title: "Plaintext secret in wrangler vars"
33
- };
34
-
35
- export { plaintextSecretInWranglerVariables as default };
@@ -1,38 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const policyReferencesUnknownTable = {
4
- categories: ["SECURITY"],
5
- description: "An RLS policy is bound to a `table` name that does not exist in the schema. The `rls()` middleware never matches it, so the table the policy was meant to protect is left ungated — reads return unrestricted rows and writes are allowed.",
6
- facing: "EXTERNAL",
7
- level: "WARN",
8
- name: "policy_references_unknown_table",
9
- remediation: "Fix the policy's `table` to a real table name (check for a typo or a table that was renamed/removed). If the table is gone, delete the dead policy.",
10
- run: (context) => {
11
- if (context.rlsProcedures === void 0) {
12
- return [];
13
- }
14
- const knownTables = new Set(context.schema.tables.map((table) => table.name));
15
- const reported = /* @__PURE__ */ new Set();
16
- const findings = [];
17
- for (const procedure of context.rlsProcedures) {
18
- for (const table of procedure.rlsTables) {
19
- if (table === "" || knownTables.has(table) || reported.has(table)) {
20
- continue;
21
- }
22
- reported.add(table);
23
- findings.push(
24
- emit(policyReferencesUnknownTable, {
25
- cacheKey: `policy_references_unknown_table:${table}`,
26
- detail: `An RLS policy in \`${procedure.exportName}\` (${procedure.file}) is bound to table \`${table}\`, which is not declared in the schema. The policy never matches, so \`${table}\` — if it exists under another name — is left ungated.`,
27
- metadata: { exportName: procedure.exportName, file: procedure.file, table }
28
- })
29
- );
30
- }
31
- }
32
- return findings;
33
- },
34
- source: "static",
35
- title: "RLS policy bound to an unknown table"
36
- };
37
-
38
- export { policyReferencesUnknownTable as default };