@lunora/advisor 1.0.0-alpha.21 → 1.0.0-alpha.211
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/README.md +64 -7
- package/dist/index.d.mts +4107 -2575
- package/dist/index.d.ts +4107 -2575
- package/dist/index.mjs +1 -181
- package/dist/packem_shared/MAP_VERSION-MQ5W2B6n.mjs +1 -0
- package/dist/packem_shared/actionFetchSsrf-BK-kvOPO.mjs +1 -0
- package/dist/packem_shared/actionWithoutErrorHandling-4GCBT0_z.mjs +1 -0
- package/dist/packem_shared/adminRouteWithoutGuard-DPE7LuNh.mjs +1 -0
- package/dist/packem_shared/aiRawRunEscapeHatch-BW7a8kZC.mjs +1 -0
- package/dist/packem_shared/aiRunWithoutLogging-CvHHtEN9.mjs +1 -0
- package/dist/packem_shared/aiToolSideEffectPromptInjection-9eJXRlKt.mjs +1 -0
- package/dist/packem_shared/aiUnboundedGenerationPublic-C17h6sMV.mjs +1 -0
- package/dist/packem_shared/allowUnauthenticatedShardAccessEnabled-BQufF5Kb.mjs +1 -0
- package/dist/packem_shared/argument-derived-sink-DLXFC79t.mjs +1 -0
- package/dist/packem_shared/authApiCallWithoutHeaders-CReJRsGW.mjs +1 -0
- package/dist/packem_shared/authCsrfCheckDisabled-CRIVsvb9.mjs +1 -0
- package/dist/packem_shared/authEmailVerificationDisabled-BjFlnHr2.mjs +1 -0
- package/dist/packem_shared/authScimWithoutTransactions-B5ATiz5A.mjs +1 -0
- package/dist/packem_shared/authSecureCookiesDisabled-By8PVum3.mjs +1 -0
- package/dist/packem_shared/authSessionFreshageZero-DuxX3Ln9.mjs +1 -0
- package/dist/packem_shared/authTrustedOriginsWildcard-BKIh6788.mjs +1 -0
- package/dist/packem_shared/browserAllowPrivateTargets-Cj5sizhv.mjs +1 -0
- package/dist/packem_shared/browserUserUrlWithoutAllowlist-CzDEraJB.mjs +1 -0
- package/dist/packem_shared/callSiteCallers-DArHfQSU.mjs +1 -0
- package/dist/packem_shared/circularFk-DtcWFJxK.mjs +1 -0
- package/dist/packem_shared/classifySensitivity-JnjaTGYi.mjs +1 -0
- package/dist/packem_shared/commitOrderedHardDelete-BPdwOKA7.mjs +1 -0
- package/dist/packem_shared/compareToBaseline-CUG59IOy.mjs +1 -0
- package/dist/packem_shared/containerInstanceKeyFromUserInput-Bl-tAzKV.mjs +1 -0
- package/dist/packem_shared/containerOversizedInstance-Bx89uR7E.mjs +1 -0
- package/dist/packem_shared/containerPublicInternet-BFfZf_P4.mjs +1 -0
- package/dist/packem_shared/containerRuntimeEgressRelaxation-4tMrhq1k.mjs +1 -0
- package/dist/packem_shared/containerStartEnableInternetOverride-mN_kuRtk.mjs +1 -0
- package/dist/packem_shared/crossModuleTableWrite-DjiVTAwK.mjs +1 -0
- package/dist/packem_shared/dedupeCacheKeys-DtBOHffV.mjs +1 -0
- package/dist/packem_shared/dispatch-namespace-BE_LxN7Z.mjs +1 -0
- package/dist/packem_shared/dispatchCycle-CWc5Q0cd.mjs +1 -0
- package/dist/packem_shared/duplicateIndex-Cip6-Rpu.mjs +1 -0
- package/dist/packem_shared/emptyIndex-BnHDcXza.mjs +1 -0
- package/dist/packem_shared/errorWithoutCatalog-BTfvaXHR.mjs +1 -0
- package/dist/packem_shared/exportSinkMisconfigured-JfbAx9AI.mjs +1 -0
- package/dist/packem_shared/externalSourceIncrementalNoDeletePath-BCzm3HzF.mjs +1 -0
- package/dist/packem_shared/externalSourceOnGlobal-CH7xbJ49.mjs +1 -0
- package/dist/packem_shared/externalSourceUnscoped-BxU2uSXk.mjs +1 -0
- package/dist/packem_shared/fanOutBreadth-CBtmZnoh.mjs +1 -0
- package/dist/packem_shared/filterAfterIndexScan-CV8aPq7v.mjs +1 -0
- package/dist/packem_shared/filterOnPrimaryKey-BeHVg4w3.mjs +1 -0
- package/dist/packem_shared/filterWithoutIndex-BGv3jbgg.mjs +1 -0
- package/dist/packem_shared/finding-NrKO8idM.mjs +1 -0
- package/dist/packem_shared/fk-index-DXYS17cc.mjs +1 -0
- package/dist/packem_shared/flagGatesSecurityWithUnsafeDefault-B5FBDF8c.mjs +1 -0
- package/dist/packem_shared/flagReadInSubscription-DabIhYGD.mjs +1 -0
- package/dist/packem_shared/fromServerSchema-KWJd4PLE.mjs +1 -0
- package/dist/packem_shared/geoIndexFieldNotGeopoint-BMQuok2-.mjs +1 -0
- package/dist/packem_shared/geoIndexUnused-D7C9Qr4U.mjs +1 -0
- package/dist/packem_shared/globalTableNearColumnLimit-BFbBBd6A.mjs +1 -0
- package/dist/packem_shared/gradeFromScore-KSt58rj1.mjs +1 -0
- package/dist/packem_shared/hardcodedSecret-Bw_4FYrs.mjs +1 -0
- package/dist/packem_shared/helpers-DdZ_pyR3.mjs +1 -0
- package/dist/packem_shared/hotShard-BwGYZ3Tq.mjs +1 -0
- package/dist/packem_shared/httpActionMissingAuthGuard-DhMIs1EI.mjs +1 -0
- package/dist/packem_shared/httpActionResponseHeaderInjection-mcRYd3Zw.mjs +1 -0
- package/dist/packem_shared/hyperdriveOutsideAction-CPDdGP2g.mjs +1 -0
- package/dist/packem_shared/identityUndeclaredClaimTrusted-D1sEd5lr.mjs +1 -0
- package/dist/packem_shared/imagesUrlSourceFromUserInput-BJXotuyO.mjs +1 -0
- package/dist/packem_shared/indexReferencesUnknownField-DKz4-bla.mjs +1 -0
- package/dist/packem_shared/indexUtilization-CkVPZcVe.mjs +1 -0
- package/dist/packem_shared/insertManyUnsafeUserData-DWE8d_DF.mjs +1 -0
- package/dist/packem_shared/kvUnscopedUserKeyIdor-DhymglIq.mjs +1 -0
- package/dist/packem_shared/mailInboundDispatchWithoutVerify-CWwqXyPX.mjs +1 -0
- package/dist/packem_shared/mailRecipientFromRequestInput-BVXsZ4Xf.mjs +1 -0
- package/dist/packem_shared/maskUncoveredPiiColumn-axEka4nd.mjs +1 -0
- package/dist/packem_shared/maskWeakHashStrategyOnPii-DJasagzX.mjs +1 -0
- package/dist/packem_shared/maskedRelationLeakViaWith-CIFij6vE.mjs +1 -0
- package/dist/packem_shared/mutatorFullRowReplace-BzRpZ47r.mjs +1 -0
- package/dist/packem_shared/mutatorWithoutOwnerScope-BoS81ns0.mjs +1 -0
- package/dist/packem_shared/nondeterministicQueryMutation-Bbb-AGOS.mjs +1 -0
- package/dist/packem_shared/normalizeIdUsedAsAuthorization-BXN-Can6.mjs +1 -0
- package/dist/packem_shared/notifyMissingPushConfig-DeQMoNwm.mjs +1 -0
- package/dist/packem_shared/notifySendOutsideAction-BJrDkr4h.mjs +1 -0
- package/dist/packem_shared/outputProjectionMissingOnPublicRead-DWSeo7oC.mjs +1 -0
- package/dist/packem_shared/ownerFieldFromArgsNotAuth-J8Xbz5uI.mjs +1 -0
- package/dist/packem_shared/paymentCreateWithoutAuthorize-CdenWpXD.mjs +1 -0
- package/dist/packem_shared/paymentWebhookWideTolerance-DVhJB0Ip.mjs +1 -0
- package/dist/packem_shared/plaintextSecretInWranglerVariables-bNkqmNqV.mjs +1 -0
- package/dist/packem_shared/policyReferencesUnknownTable-CIKuRZ5Y.mjs +1 -0
- package/dist/packem_shared/privilegedDispatchUnvalidatedPayload-BM3NtT57.mjs +1 -0
- package/dist/packem_shared/privilegedFanoutFromPublicProcedure-DV188gHQ.mjs +1 -0
- package/dist/packem_shared/procedure-protections-DRQotu9I.mjs +1 -0
- package/dist/packem_shared/procedureWithoutStructuredEvent-CZ_B23nM.mjs +1 -0
- package/dist/packem_shared/publicArgumentUsesAny-BDEvMA8Q.mjs +1 -0
- package/dist/packem_shared/publicMutationWithoutRatelimit-Bd-rRxnz.mjs +1 -0
- package/dist/packem_shared/publicTableRlsOptoutConfusion-CbNk75Xo.mjs +1 -0
- package/dist/packem_shared/queueWithoutDlq-BvHz3Opg.mjs +1 -0
- package/dist/packem_shared/r2sqlOutsideAction-BdLiLOAX.mjs +1 -0
- package/dist/packem_shared/ratelimitDefaultMemoryStore-Dj0Kk7t9.mjs +1 -0
- package/dist/packem_shared/ratelimitKeySpoofableOrGlobal-DHMl74hR.mjs +1 -0
- package/dist/packem_shared/ratelimitMiddlewareFailOpen-Di2t8pDc.mjs +1 -0
- package/dist/packem_shared/relationReferencesUnknownField-Cl6mik8w.mjs +1 -0
- package/dist/packem_shared/relationReferencesUnknownTable-vYjirSxf.mjs +1 -0
- package/dist/packem_shared/rlsUncoveredTable-CZ4ie4gX.mjs +1 -0
- package/dist/packem_shared/selfRescheduleWithoutProgress-Be9d1bQ0.mjs +1 -0
- package/dist/packem_shared/shapeTargetsGlobalTable-Bu3eEDic.mjs +1 -0
- package/dist/packem_shared/shapeUnknownTable-CREfNnWi.mjs +1 -0
- package/dist/packem_shared/signupMutationWithoutDisposableGating-BdKwtxDP.mjs +1 -0
- package/dist/packem_shared/softDeleteIncludeDeletedFromArgs-C7Ugg4zb.mjs +1 -0
- package/dist/packem_shared/sqlInjectionRisk-BYRllgPV.mjs +1 -0
- package/dist/packem_shared/storageGenerateUploadUrlNoContentTypePin-CrwFuo83.mjs +1 -0
- package/dist/packem_shared/storageKeyFromUserArgs-3vkxmI3v.mjs +1 -0
- package/dist/packem_shared/storagePresignedUrlForPrivateContent-C3KeV5tV.mjs +1 -0
- package/dist/packem_shared/storageUploadWithoutContentTypeAllowlist-BXoK_QWE.mjs +1 -0
- package/dist/packem_shared/storageUploadWithoutMaxSize-CW7uKvSi.mjs +1 -0
- package/dist/packem_shared/sweeps-JWMwcakB.mjs +1 -0
- package/dist/packem_shared/tableWithoutInsert-ClJp_jDr.mjs +1 -0
- package/dist/packem_shared/throughRelationMissingUniqueIndex-Cg5e3GkS.mjs +1 -0
- package/dist/packem_shared/topicTooManySubscriptions-CiZhFm9r.mjs +1 -0
- package/dist/packem_shared/ttlFieldNotTimestamp-lnb5nmM6.mjs +1 -0
- package/dist/packem_shared/unboundedCollectOnOpenRange-DHy2RUL7.mjs +1 -0
- package/dist/packem_shared/unboundedLoop-DxN3_apu.mjs +1 -0
- package/dist/packem_shared/unboundedStringArgument-jZnCbwB0.mjs +1 -0
- package/dist/packem_shared/unindexedForeignKey-Dypgn8uH.mjs +1 -0
- package/dist/packem_shared/unindexedRelationTarget-Dj2_NOCm.mjs +1 -0
- package/dist/packem_shared/unrestrictedWhereBranch-CcIBmHik.mjs +1 -0
- package/dist/packem_shared/userCreatingMutationWithoutCaptcha-D4TZEpgz.mjs +1 -0
- package/dist/packem_shared/vectorsNamespaceFromUserInput-BWvGjJzp.mjs +1 -0
- package/dist/packem_shared/workflowDuplicateStepName-B6eAJzwA.mjs +1 -0
- package/dist/packem_shared/workflowUnknownTarget-DEKB5H0g.mjs +1 -0
- package/dist/packem_shared/workflowUnused-DHwwE-1P.mjs +1 -0
- package/package.json +3 -3
- package/dist/packem_shared/AE_METRIC_EVENTS-BM14d0lm.mjs +0 -87
- package/dist/packem_shared/actionFetchSsrf-wbmYgzmz.mjs +0 -26
- package/dist/packem_shared/adminRouteWithoutGuard-UUGBkAjU.mjs +0 -33
- package/dist/packem_shared/aiRawRunEscapeHatch-C29jd32J.mjs +0 -26
- package/dist/packem_shared/aiToolSideEffectPromptInjection-BtD3alqB.mjs +0 -33
- package/dist/packem_shared/aiUnboundedGenerationPublic-CbAcSxLB.mjs +0 -26
- package/dist/packem_shared/allowUnauthenticatedShardAccessEnabled-BcASq-jP.mjs +0 -30
- package/dist/packem_shared/argument-derived-sink-C1xTTqAt.mjs +0 -30
- package/dist/packem_shared/authApiCallWithoutHeaders-BeJhCZaf.mjs +0 -38
- package/dist/packem_shared/authCsrfCheckDisabled-DCf9FSoD.mjs +0 -26
- package/dist/packem_shared/authEmailVerificationDisabled-Dk-Tc5oU.mjs +0 -26
- package/dist/packem_shared/authSecureCookiesDisabled-osJrHW9Y.mjs +0 -26
- package/dist/packem_shared/authSessionFreshageZero-CjufCc_Q.mjs +0 -26
- package/dist/packem_shared/authTrustedOriginsWildcard-ylv4PKzC.mjs +0 -26
- package/dist/packem_shared/browserAllowPrivateTargets-CBvQxLAM.mjs +0 -26
- package/dist/packem_shared/browserUserUrlWithoutAllowlist-CvhA6w59.mjs +0 -26
- package/dist/packem_shared/circularFk-B2freHrP.mjs +0 -84
- package/dist/packem_shared/constraintValidator-Dr9Py3FD.mjs +0 -186
- package/dist/packem_shared/containerInstanceKeyFromUserInput-BUXj2J4w.mjs +0 -19
- package/dist/packem_shared/containerOversizedInstance-5U1VKPRM.mjs +0 -36
- package/dist/packem_shared/containerPublicInternet-CuNerJE5.mjs +0 -30
- package/dist/packem_shared/containerRuntimeEgressRelaxation-B0LlfmDA.mjs +0 -26
- package/dist/packem_shared/containerStartEnableInternetOverride-DDaHZQ1L.mjs +0 -26
- package/dist/packem_shared/duplicateIndex-BOublMSt.mjs +0 -57
- package/dist/packem_shared/emptyIndex-BX8EuEY7.mjs +0 -32
- package/dist/packem_shared/externalSourceOnGlobal-Bg-NfCX9.mjs +0 -30
- package/dist/packem_shared/externalSourceUnscoped-5vT-Bup3.mjs +0 -44
- package/dist/packem_shared/filterWithoutIndex-BYVeJaSs.mjs +0 -31
- package/dist/packem_shared/finding-Dm_zvzS1.mjs +0 -16
- package/dist/packem_shared/fk-index-IUK1ukgs.mjs +0 -7
- package/dist/packem_shared/flagGatesSecurityWithUnsafeDefault-BhIs0shr.mjs +0 -41
- package/dist/packem_shared/fromServerSchema-BiB6JSiW.mjs +0 -59
- package/dist/packem_shared/hardcodedSecret-W2pz1UZB.mjs +0 -35
- package/dist/packem_shared/helpers-BySnKhVB.mjs +0 -31
- package/dist/packem_shared/hotShard-Ir5D0B6J.mjs +0 -48
- package/dist/packem_shared/httpActionMissingAuthGuard-CxipddNx.mjs +0 -35
- package/dist/packem_shared/httpActionResponseHeaderInjection-DOFS7pFT.mjs +0 -39
- package/dist/packem_shared/hyperdriveOutsideAction-BgZqX7Xg.mjs +0 -30
- package/dist/packem_shared/identityUndeclaredClaimTrusted-D8nXV2dd.mjs +0 -32
- package/dist/packem_shared/imagesUrlSourceFromUserInput-YcDQ0b_Y.mjs +0 -19
- package/dist/packem_shared/indexReferencesUnknownField-BSWNngxX.mjs +0 -36
- package/dist/packem_shared/indexUtilization-B5DMQ3bI.mjs +0 -45
- package/dist/packem_shared/insertManyUnsafeUserData-Dn77XpmX.mjs +0 -26
- package/dist/packem_shared/kvUnscopedUserKeyIdor-YWwmfE8X.mjs +0 -19
- package/dist/packem_shared/mailInboundDispatchWithoutVerify-CHSRA8zz.mjs +0 -28
- package/dist/packem_shared/mailRecipientFromRequestInput-Cka2qu5J.mjs +0 -26
- package/dist/packem_shared/maskUncoveredPiiColumn-DjGIPG6M.mjs +0 -61
- package/dist/packem_shared/maskWeakHashStrategyOnPii-1c4q8Opf.mjs +0 -35
- package/dist/packem_shared/maskedRelationLeakViaWith-CPI4s0sl.mjs +0 -76
- package/dist/packem_shared/mutatorFullRowReplace-BJnNDaIV.mjs +0 -26
- package/dist/packem_shared/nondeterministicQueryMutation-GXES1fLp.mjs +0 -35
- package/dist/packem_shared/normalizeIdUsedAsAuthorization-BVPtCpzT.mjs +0 -50
- package/dist/packem_shared/outputProjectionMissingOnPublicRead-Bl5IMx0k.mjs +0 -51
- package/dist/packem_shared/ownerFieldFromArgsNotAuth-mOw3hE5z.mjs +0 -26
- package/dist/packem_shared/paymentCreateWithoutAuthorize-BYm4JLxo.mjs +0 -26
- package/dist/packem_shared/paymentWebhookWideTolerance-D9QQJMJq.mjs +0 -35
- package/dist/packem_shared/plaintextSecretInWranglerVariables-NKO4YkKf.mjs +0 -35
- package/dist/packem_shared/policyReferencesUnknownTable-DtaIEovd.mjs +0 -38
- package/dist/packem_shared/privilegedDispatchUnvalidatedPayload-5Forjckt.mjs +0 -37
- package/dist/packem_shared/privilegedFanoutFromPublicProcedure-D3dL01B8.mjs +0 -26
- package/dist/packem_shared/publicArgumentUsesAny-C71b2NCf.mjs +0 -32
- package/dist/packem_shared/publicMutationWithoutRatelimit-DbIhgi7j.mjs +0 -36
- package/dist/packem_shared/publicTableRlsOptoutConfusion-Bn71yoD4.mjs +0 -38
- package/dist/packem_shared/queueWithoutDlq-CSkGNb_0.mjs +0 -41
- package/dist/packem_shared/r2sqlOutsideAction-CtqxvMuV.mjs +0 -30
- package/dist/packem_shared/ratelimitDefaultMemoryStore-BISChG5C.mjs +0 -26
- package/dist/packem_shared/ratelimitKeySpoofableOrGlobal-DqlHYQQ3.mjs +0 -26
- package/dist/packem_shared/ratelimitMiddlewareFailOpen-CJgDCaUw.mjs +0 -33
- package/dist/packem_shared/relationReferencesUnknownField-CjbLScJ1.mjs +0 -54
- package/dist/packem_shared/relationReferencesUnknownTable-DrorpKYe.mjs +0 -33
- package/dist/packem_shared/rlsUncoveredTable-CxEfZ5eZ.mjs +0 -56
- package/dist/packem_shared/shapeTargetsGlobalTable-DHrf4Koi.mjs +0 -34
- package/dist/packem_shared/shapeUnknownTable-C8aDWFoe.mjs +0 -34
- package/dist/packem_shared/softDeleteIncludeDeletedFromArgs-BLqDKrkM.mjs +0 -38
- package/dist/packem_shared/sqlInjectionRisk-zwytYGLt.mjs +0 -26
- package/dist/packem_shared/storageGenerateUploadUrlNoContentTypePin-Da4L9Ge8.mjs +0 -26
- package/dist/packem_shared/storageKeyFromUserArgs-B86elJgS.mjs +0 -19
- package/dist/packem_shared/storagePresignedUrlForPrivateContent-yGmb8uqz.mjs +0 -47
- package/dist/packem_shared/storageUploadWithoutContentTypeAllowlist-BV-gF1lT.mjs +0 -27
- package/dist/packem_shared/storageUploadWithoutMaxSize-DpxO59wU.mjs +0 -27
- package/dist/packem_shared/tableWithoutInsert-CbbaYIP4.mjs +0 -34
- package/dist/packem_shared/unboundedStringArgument-DThg2-wt.mjs +0 -32
- package/dist/packem_shared/unindexedForeignKey-BgJbKyqK.mjs +0 -45
- package/dist/packem_shared/unindexedRelationTarget-D6eyj6Xx.mjs +0 -53
- package/dist/packem_shared/userCreatingMutationWithoutCaptcha-2DZtWIPb.mjs +0 -42
- package/dist/packem_shared/vectorsNamespaceFromUserInput-CQhr5bVn.mjs +0 -19
- package/dist/packem_shared/workflowDuplicateStepName-ioBxPBCy.mjs +0 -48
- package/dist/packem_shared/workflowUnknownTarget-Cdd7WhKQ.mjs +0 -34
- package/dist/packem_shared/workflowUnused-D0jHxdz9.mjs +0 -38
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const SENSITIVE_TOKENS = ["signin", "signup", "login", "register", "reset", "password", "otp", "verify", "payment", "checkout"];
|
|
4
|
-
const normalize = (value) => value.toLowerCase().replaceAll(/[^a-z0-9]+/gu, "");
|
|
5
|
-
const matchesSensitiveToken = (value) => {
|
|
6
|
-
const normalized = normalize(value);
|
|
7
|
-
return SENSITIVE_TOKENS.some((token) => normalized.includes(token));
|
|
8
|
-
};
|
|
9
|
-
const guardsSensitiveFlow = (row) => matchesSensitiveToken(row.exportName) || matchesSensitiveToken(row.limitName);
|
|
10
|
-
const ratelimitMiddlewareFailOpen = {
|
|
11
|
-
categories: ["SECURITY"],
|
|
12
|
-
description: "A rate-limit or Turnstile middleware guarding a sensitive auth/payment procedure is configured `failOpen: true`, so a limiter store outage or a failed Turnstile siteverify silently admits every request — turning a transient failure into an unthrottled brute-force / abuse window.",
|
|
13
|
-
facing: "EXTERNAL",
|
|
14
|
-
level: "WARN",
|
|
15
|
-
name: "ratelimit_middleware_fail_open",
|
|
16
|
-
remediation: "Drop `failOpen: true` (the default is fail-closed) on the rate-limit / Turnstile guard for auth and payment flows, or move the escape hatch to a non-sensitive procedure. Prefer rejecting traffic with a 503 over admitting unlimited unauthenticated attempts when the limiter or siteverify is unavailable.",
|
|
17
|
-
run: (context) => {
|
|
18
|
-
if (context.failOpenGuards === void 0) {
|
|
19
|
-
return [];
|
|
20
|
-
}
|
|
21
|
-
return context.failOpenGuards.filter((row) => row.failOpen && guardsSensitiveFlow(row)).map(
|
|
22
|
-
(row) => emit(ratelimitMiddlewareFailOpen, {
|
|
23
|
-
cacheKey: `ratelimit_middleware_fail_open:${row.file}:${row.line.toString()}`,
|
|
24
|
-
detail: `\`${row.callee}(...)\` in \`${row.exportName}\` (${row.file}:${row.line.toString()}) sets \`failOpen: true\` while guarding a sensitive flow — a limiter/siteverify outage then admits every request instead of rejecting it.`,
|
|
25
|
-
metadata: { callee: row.callee, exportName: row.exportName, file: row.file, limitName: row.limitName, line: row.line }
|
|
26
|
-
})
|
|
27
|
-
);
|
|
28
|
-
},
|
|
29
|
-
source: "static",
|
|
30
|
-
title: "Fail-open rate-limit / CAPTCHA guard on a sensitive procedure"
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export { ratelimitMiddlewareFailOpen as default };
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
import { t as tableColumnSet } from './helpers-BySnKhVB.mjs';
|
|
3
|
-
|
|
4
|
-
const missingColumns = (byName, columnsOf, holder, relation) => {
|
|
5
|
-
const target = byName.get(relation.table);
|
|
6
|
-
if (!target) {
|
|
7
|
-
return [];
|
|
8
|
-
}
|
|
9
|
-
const fkTable = relation.kind === "one" ? holder : target;
|
|
10
|
-
const referencedTable = relation.kind === "one" ? target : holder;
|
|
11
|
-
const problems = [];
|
|
12
|
-
if (!columnsOf(fkTable).has(relation.field)) {
|
|
13
|
-
problems.push({ column: relation.field, owner: fkTable.name, side: "field" });
|
|
14
|
-
}
|
|
15
|
-
if (!columnsOf(referencedTable).has(relation.references)) {
|
|
16
|
-
problems.push({ column: relation.references, owner: referencedTable.name, side: "references" });
|
|
17
|
-
}
|
|
18
|
-
return problems;
|
|
19
|
-
};
|
|
20
|
-
const relationReferencesUnknownField = {
|
|
21
|
-
categories: ["SCHEMA"],
|
|
22
|
-
description: "A relation references a foreign-key or referenced column that is not declared on its table, so the join can never resolve.",
|
|
23
|
-
facing: "INTERNAL",
|
|
24
|
-
level: "ERROR",
|
|
25
|
-
name: "relation_references_unknown_field",
|
|
26
|
-
remediation: "Fix the `field` / `references` column name in the relation, or add the missing column.",
|
|
27
|
-
run: (context) => {
|
|
28
|
-
const byName = new Map(context.schema.tables.map((table) => [table.name, table]));
|
|
29
|
-
const columnsCache = /* @__PURE__ */ new Map();
|
|
30
|
-
const columnsOf = (table) => {
|
|
31
|
-
let columns = columnsCache.get(table);
|
|
32
|
-
if (!columns) {
|
|
33
|
-
columns = tableColumnSet(table);
|
|
34
|
-
columnsCache.set(table, columns);
|
|
35
|
-
}
|
|
36
|
-
return columns;
|
|
37
|
-
};
|
|
38
|
-
return context.schema.tables.flatMap(
|
|
39
|
-
(table) => table.relations.flatMap(
|
|
40
|
-
(relation) => missingColumns(byName, columnsOf, table, relation).map(
|
|
41
|
-
(problem) => emit(relationReferencesUnknownField, {
|
|
42
|
-
cacheKey: `relation_references_unknown_field:${table.name}:${relation.name}:${problem.side}`,
|
|
43
|
-
detail: `Relation "${relation.name}" on table "${table.name}" uses ${problem.side} "${problem.column}", which is not declared on table "${problem.owner}".`,
|
|
44
|
-
metadata: { column: problem.column, owner: problem.owner, relation: relation.name, side: problem.side, table: table.name }
|
|
45
|
-
})
|
|
46
|
-
)
|
|
47
|
-
)
|
|
48
|
-
);
|
|
49
|
-
},
|
|
50
|
-
source: "static",
|
|
51
|
-
title: "Relation references unknown field"
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
export { relationReferencesUnknownField as default };
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const relationReferencesUnknownTable = {
|
|
4
|
-
categories: ["SCHEMA"],
|
|
5
|
-
description: "A relation targets a table that does not exist in the schema. The relation can never load — the target is a typo or a removed/renamed table.",
|
|
6
|
-
facing: "INTERNAL",
|
|
7
|
-
level: "ERROR",
|
|
8
|
-
name: "relation_references_unknown_table",
|
|
9
|
-
remediation: "Fix the target table name in the relation, or add the missing table to the schema.",
|
|
10
|
-
run: (context) => {
|
|
11
|
-
const findings = [];
|
|
12
|
-
const tableNames = new Set(context.schema.tables.map((table) => table.name));
|
|
13
|
-
for (const table of context.schema.tables) {
|
|
14
|
-
for (const relation of table.relations) {
|
|
15
|
-
if (tableNames.has(relation.table)) {
|
|
16
|
-
continue;
|
|
17
|
-
}
|
|
18
|
-
findings.push(
|
|
19
|
-
emit(relationReferencesUnknownTable, {
|
|
20
|
-
cacheKey: `relation_references_unknown_table:${table.name}:${relation.name}`,
|
|
21
|
-
detail: `Relation "${relation.name}" on table "${table.name}" targets table "${relation.table}", which does not exist in the schema.`,
|
|
22
|
-
metadata: { relation: relation.name, table: table.name, target: relation.table }
|
|
23
|
-
})
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return findings;
|
|
28
|
-
},
|
|
29
|
-
source: "static",
|
|
30
|
-
title: "Relation references unknown table"
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export { relationReferencesUnknownTable as default };
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const rlsUncoveredTable = {
|
|
4
|
-
categories: ["SECURITY"],
|
|
5
|
-
description: "A public procedure reads or writes a table that is covered by an RLS policy elsewhere in the app, but this procedure's builder chain does not include `.use(rls(...))`. The raw, unwrapped `ctx.db` silently bypasses every policy in the list.",
|
|
6
|
-
facing: "EXTERNAL",
|
|
7
|
-
level: "WARN",
|
|
8
|
-
name: "rls_uncovered_table",
|
|
9
|
-
remediation: "Add `.use(rls(policies))` to the procedure's builder chain — e.g. `c.use(rls(myPolicies)).query(...)` — so its `ctx.db` is wrapped by the same policy evaluator as the rest of the app. If this procedure is intentionally privileged (e.g. a background job), 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 policy/table cross-reference is clearest as one linear pass; splitting it would obscure the flow.
|
|
11
|
-
run: (context) => {
|
|
12
|
-
if (context.rlsProcedures === void 0) {
|
|
13
|
-
return [];
|
|
14
|
-
}
|
|
15
|
-
const policyCoveredTables = /* @__PURE__ */ new Set();
|
|
16
|
-
for (const procedure of context.rlsProcedures) {
|
|
17
|
-
for (const table of procedure.rlsTables) {
|
|
18
|
-
if (table !== "") {
|
|
19
|
-
policyCoveredTables.add(table);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
if (policyCoveredTables.size === 0) {
|
|
24
|
-
return [];
|
|
25
|
-
}
|
|
26
|
-
const findings = [];
|
|
27
|
-
for (const procedure of context.rlsProcedures) {
|
|
28
|
-
if (procedure.usesRls) {
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
if (procedure.visibility === "internal") {
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
const touched = /* @__PURE__ */ new Set();
|
|
35
|
-
for (const table of [...procedure.tablesRead, ...procedure.tablesWritten]) {
|
|
36
|
-
if (table !== "" && policyCoveredTables.has(table)) {
|
|
37
|
-
touched.add(table);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
for (const table of touched) {
|
|
41
|
-
findings.push(
|
|
42
|
-
emit(rlsUncoveredTable, {
|
|
43
|
-
cacheKey: `rls_uncovered_table:${procedure.file}:${procedure.exportName}:${table}`,
|
|
44
|
-
detail: `\`${procedure.exportName}\` in ${procedure.file} accesses table \`${table}\` without \`.use(rls(...))\` — the table is policy-gated elsewhere in the app but this procedure bypasses those policies entirely.`,
|
|
45
|
-
metadata: { exportName: procedure.exportName, file: procedure.file, table }
|
|
46
|
-
})
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return findings;
|
|
51
|
-
},
|
|
52
|
-
source: "static",
|
|
53
|
-
title: "RLS-gated table accessed without rls() middleware"
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
export { rlsUncoveredTable as default };
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const shapeTargetsGlobalTable = {
|
|
4
|
-
categories: ["PERFORMANCE"],
|
|
5
|
-
description: "A `defineShape` replicates from a `.global()` table. Global tables live outside the shard DO's SQLite op-log, so the shape is served through the cross-shard tier — coordinator/poll-refreshed and latency-tiered, not poke-live like a sharded shape.",
|
|
6
|
-
facing: "EXTERNAL",
|
|
7
|
-
level: "WARN",
|
|
8
|
-
name: "shape_targets_global_table",
|
|
9
|
-
remediation: "Expected if you want the cross-shard tier — just don't assume poke-live freshness; global shapes refresh on a poll. For live updates, replicate from a sharded table instead (denormalize the columns you need into the shard).",
|
|
10
|
-
run: (context) => {
|
|
11
|
-
if (context.shapes === void 0) {
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
const globalTables = new Set(context.schema.tables.filter((table) => table.shardKind === "global").map((table) => table.name));
|
|
15
|
-
const findings = [];
|
|
16
|
-
for (const shape of context.shapes) {
|
|
17
|
-
if (shape.table === void 0 || !globalTables.has(shape.table)) {
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
findings.push(
|
|
21
|
-
emit(shapeTargetsGlobalTable, {
|
|
22
|
-
cacheKey: `shape_targets_global_table:${shape.exportName}`,
|
|
23
|
-
detail: `Shape \`${shape.exportName}\` (${shape.file}) replicates from the \`.global()\` table \`${shape.table}\`. It is served through the cross-shard global tier — poll-refreshed and latency-tiered, not poke-live.`,
|
|
24
|
-
metadata: { exportName: shape.exportName, file: shape.file, table: shape.table }
|
|
25
|
-
})
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
return findings;
|
|
29
|
-
},
|
|
30
|
-
source: "static",
|
|
31
|
-
title: "Shape replicates from a global (cross-shard) table"
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export { shapeTargetsGlobalTable as default };
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const shapeUnknownTable = {
|
|
4
|
-
categories: ["SCHEMA"],
|
|
5
|
-
description: "A `defineShape` is bound to a `table` name that does not exist in the schema. The shape can never resolve a rowset — its subscription seeds empty and errors at the first flush.",
|
|
6
|
-
facing: "INTERNAL",
|
|
7
|
-
level: "ERROR",
|
|
8
|
-
name: "shape_unknown_table",
|
|
9
|
-
remediation: "Fix the shape's `table` to a real table name (check for a typo or a table that was renamed/removed).",
|
|
10
|
-
run: (context) => {
|
|
11
|
-
if (context.shapes === void 0) {
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
const knownTables = new Set(context.schema.tables.map((table) => table.name));
|
|
15
|
-
const findings = [];
|
|
16
|
-
for (const shape of context.shapes) {
|
|
17
|
-
if (shape.table === void 0 || knownTables.has(shape.table)) {
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
findings.push(
|
|
21
|
-
emit(shapeUnknownTable, {
|
|
22
|
-
cacheKey: `shape_unknown_table:${shape.exportName}`,
|
|
23
|
-
detail: `Shape \`${shape.exportName}\` (${shape.file}) replicates from table \`${shape.table}\`, which is not declared in the schema. The shape can never resolve a rowset.`,
|
|
24
|
-
metadata: { exportName: shape.exportName, file: shape.file, table: shape.table }
|
|
25
|
-
})
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
return findings;
|
|
29
|
-
},
|
|
30
|
-
source: "static",
|
|
31
|
-
title: "Shape bound to an unknown table"
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export { shapeUnknownTable as default };
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const softDeleteIncludeDeletedFromArgs = {
|
|
4
|
-
categories: ["SECURITY"],
|
|
5
|
-
description: "A `.public()` read passes `includeDeleted` (hardcoded `true` or derived from `args`) on a `.softDelete()` table, resurfacing soft-deleted rows to callers that should never see them.",
|
|
6
|
-
facing: "EXTERNAL",
|
|
7
|
-
level: "INFO",
|
|
8
|
-
name: "soft_delete_include_deleted_from_args",
|
|
9
|
-
remediation: "Drop `includeDeleted` from the public read so soft-deleted rows stay hidden, or move the trash/restore view behind an internal (or RLS/role-gated) procedure. Never wire `includeDeleted` from `args` on a public read — an attacker can set it to see rows the app treats as deleted.",
|
|
10
|
-
run: (context) => {
|
|
11
|
-
if (context.softDeleteReads === void 0) {
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
const softDeleteTables = new Set(context.schema.tables.filter((table) => table.softDelete !== void 0).map((table) => table.name));
|
|
15
|
-
if (softDeleteTables.size === 0) {
|
|
16
|
-
return [];
|
|
17
|
-
}
|
|
18
|
-
return context.softDeleteReads.filter((row) => row.visibility === "public" && row.table !== "" && softDeleteTables.has(row.table) && (row.hardcodedTrue || row.fromArgs)).map((row) => {
|
|
19
|
-
const location = `\`${row.exportName}\` (${row.file}:${row.line.toString()})`;
|
|
20
|
-
const detail = row.fromArgs ? `The public read in ${location} wires \`includeDeleted\` from request \`args\` on soft-delete table \`${row.table}\`, letting any caller resurface soft-deleted rows the app treats as gone.` : `The public read in ${location} hardcodes \`includeDeleted: true\` on soft-delete table \`${row.table}\`, returning soft-deleted rows to every caller.`;
|
|
21
|
-
return emit(softDeleteIncludeDeletedFromArgs, {
|
|
22
|
-
cacheKey: `soft_delete_include_deleted_from_args:${row.file}:${row.line.toString()}:${row.table}`,
|
|
23
|
-
detail,
|
|
24
|
-
metadata: {
|
|
25
|
-
exportName: row.exportName,
|
|
26
|
-
file: row.file,
|
|
27
|
-
line: row.line,
|
|
28
|
-
source: row.fromArgs ? "args" : "literal",
|
|
29
|
-
table: row.table
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
},
|
|
34
|
-
source: "static",
|
|
35
|
-
title: "Soft-deleted rows resurfaced via includeDeleted on a public read"
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export { softDeleteIncludeDeletedFromArgs as default };
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const sqlInjectionRisk = {
|
|
4
|
-
categories: ["SECURITY"],
|
|
5
|
-
description: "A `ctx.sql` tagged-template interpolates an unparameterized string-building expression (concatenation or nested template) instead of a bound value — splicing raw text into the query and reopening SQL injection.",
|
|
6
|
-
facing: "EXTERNAL",
|
|
7
|
-
level: "ERROR",
|
|
8
|
-
name: "sql_injection_risk",
|
|
9
|
-
remediation: "Pass the value through a bound placeholder so the driver parameterizes it — keep request input inside a `ctx.sql` placeholder instead of concatenating it into the query string. Never build SQL text from request input by hand.",
|
|
10
|
-
run: (context) => {
|
|
11
|
-
if (context.sqlInterpolations === void 0) {
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
return context.sqlInterpolations.map(
|
|
15
|
-
(interpolation) => emit(sqlInjectionRisk, {
|
|
16
|
-
cacheKey: `sql_injection_risk:${interpolation.file}:${interpolation.line.toString()}`,
|
|
17
|
-
detail: `\`ctx.sql\` in \`${interpolation.exportName}\` (${interpolation.file}:${interpolation.line.toString()}) interpolates a string-building expression instead of a bound value — a SQL-injection vector. Pass the value through a bound placeholder so the driver parameterizes it.`,
|
|
18
|
-
metadata: { exportName: interpolation.exportName, file: interpolation.file, line: interpolation.line }
|
|
19
|
-
})
|
|
20
|
-
);
|
|
21
|
-
},
|
|
22
|
-
source: "static",
|
|
23
|
-
title: "Possible SQL injection in ctx.sql interpolation"
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export { sqlInjectionRisk as default };
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const storageGenerateUploadUrlNoContentTypePin = {
|
|
4
|
-
categories: ["SECURITY"],
|
|
5
|
-
description: "A `ctx.storage.generateUploadUrl(key, …)` call has no `contentType` pin, so the signed PUT URL it mints authorizes any content-type/size the client chooses — `upload()`'s `allowedContentTypes`/`maxSize` guards never run for a client-side PUT against this URL.",
|
|
6
|
-
facing: "EXTERNAL",
|
|
7
|
-
level: "WARN",
|
|
8
|
-
name: "storage_generate_upload_url_no_content_type_pin",
|
|
9
|
-
remediation: "Pass `contentType` to `ctx.storage.generateUploadUrl(key, { contentType })` so the signature only authorizes a PUT with that exact content-type. If the client's upload size/type also needs bounding, prefer `upload()`/`store()` (with `allowedContentTypes`/`maxSize`) over a client-side signed PUT.",
|
|
10
|
-
run: (context) => {
|
|
11
|
-
if (context.storageUploads === void 0) {
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
return context.storageUploads.filter((row) => row.method === "generateUploadUrl" && row.analyzable && !row.presentKeys.includes("contentType")).map(
|
|
15
|
-
(row) => emit(storageGenerateUploadUrlNoContentTypePin, {
|
|
16
|
-
cacheKey: `storage_generate_upload_url_no_content_type_pin:${row.file}:${row.line.toString()}`,
|
|
17
|
-
detail: `\`ctx.storage.generateUploadUrl\` in \`${row.exportName}\` (${row.file}:${row.line.toString()}) has no \`contentType\` pin — the signed PUT it mints accepts any type/size, minted client-side and bypassing \`upload()\`'s guards.`,
|
|
18
|
-
metadata: { exportName: row.exportName, file: row.file, line: row.line }
|
|
19
|
-
})
|
|
20
|
-
);
|
|
21
|
-
},
|
|
22
|
-
source: "static",
|
|
23
|
-
title: "generateUploadUrl signed PUT with no content-type pin"
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export { storageGenerateUploadUrlNoContentTypePin as default };
|
|
@@ -1,19 +0,0 @@
|
|
|
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,47 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const SIGNER_METHODS = /* @__PURE__ */ new Set(["getPresignedUrl", "getSignedUrl"]);
|
|
4
|
-
const NEAR_MAX_EXPIRES_THRESHOLD_SECONDS = 6 * 24 * 60 * 60;
|
|
5
|
-
const isNativePresignedCall = (row) => row.method === "getPresignedUrl";
|
|
6
|
-
const isNearExpiryCeiling = (row) => SIGNER_METHODS.has(row.method) && row.expiresInSeconds !== void 0 && row.expiresInSeconds >= NEAR_MAX_EXPIRES_THRESHOLD_SECONDS;
|
|
7
|
-
const storagePresignedUrlForPrivateContent = {
|
|
8
|
-
categories: ["SECURITY"],
|
|
9
|
-
description: "`ctx.storage.getPresignedUrl(...)` mints a native S3 SigV4 URL that resolves directly against R2, bypassing the Worker's auth/RLS/rate-limit entirely — the wrong choice for private or policy-gated content. Either signer minting an `expiresInSeconds` near the shared 7-day ceiling also hands out a long-lived bearer credential.",
|
|
10
|
-
facing: "EXTERNAL",
|
|
11
|
-
level: "WARN",
|
|
12
|
-
name: "storage_presigned_url_for_private_content",
|
|
13
|
-
remediation: "For private/per-user/policy-gated content, use `getSignedUrl` (resolves back through the Worker, so your auth/RLS/rate-limit gates still run) instead of `getPresignedUrl`. Reserve `getPresignedUrl` for genuinely public or bulk content. On either signer, keep `expiresInSeconds` as short as the use case allows — well under the 7-day ceiling — rather than minting a near-maximum-lifetime URL.",
|
|
14
|
-
run: (context) => {
|
|
15
|
-
if (context.storageUploads === void 0) {
|
|
16
|
-
return [];
|
|
17
|
-
}
|
|
18
|
-
return context.storageUploads.filter((row) => isNativePresignedCall(row) || isNearExpiryCeiling(row)).map((row) => {
|
|
19
|
-
const native = isNativePresignedCall(row);
|
|
20
|
-
const nearCeiling = isNearExpiryCeiling(row);
|
|
21
|
-
const location = `\`${row.exportName}\` (${row.file}:${row.line.toString()})`;
|
|
22
|
-
let detail;
|
|
23
|
-
if (native && nearCeiling) {
|
|
24
|
-
detail = `\`ctx.storage.getPresignedUrl\` in ${location} mints a native S3 SigV4 URL — bypasses the Worker's auth/RLS/rate-limit entirely — with an \`expiresInSeconds\` of ${(row.expiresInSeconds ?? 0).toString()}s, near the 7-day signing ceiling.`;
|
|
25
|
-
} else if (native) {
|
|
26
|
-
detail = `\`ctx.storage.getPresignedUrl\` in ${location} mints a native S3 SigV4 URL that resolves directly against R2, bypassing the Worker's auth/RLS/rate-limit entirely. Use \`getSignedUrl\` for app-gated access to private content.`;
|
|
27
|
-
} else {
|
|
28
|
-
detail = `\`ctx.storage.${row.method}\` in ${location} requests an \`expiresInSeconds\` of ${(row.expiresInSeconds ?? 0).toString()}s, near the 7-day signing ceiling — a leaked link stays valid for almost a week.`;
|
|
29
|
-
}
|
|
30
|
-
return emit(storagePresignedUrlForPrivateContent, {
|
|
31
|
-
cacheKey: `storage_presigned_url_for_private_content:${row.file}:${row.line.toString()}`,
|
|
32
|
-
detail,
|
|
33
|
-
metadata: {
|
|
34
|
-
expiresInSeconds: row.expiresInSeconds,
|
|
35
|
-
exportName: row.exportName,
|
|
36
|
-
file: row.file,
|
|
37
|
-
line: row.line,
|
|
38
|
-
method: row.method
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
},
|
|
43
|
-
source: "static",
|
|
44
|
-
title: "Native presigned URL or near-max-TTL signed URL for private content"
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export { storagePresignedUrlForPrivateContent as default };
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const UPLOAD_METHODS = /* @__PURE__ */ new Set(["store", "upload"]);
|
|
4
|
-
const storageUploadWithoutContentTypeAllowlist = {
|
|
5
|
-
categories: ["SECURITY"],
|
|
6
|
-
description: "A `ctx.storage.upload`/`store` call has no `allowedContentTypes` allowlist, so an uploader can store any content-type — including `text/html` or `image/svg+xml` — which, served back from `publicBaseUrl`, is a stored-XSS vector against your own origin.",
|
|
7
|
-
facing: "EXTERNAL",
|
|
8
|
-
level: "WARN",
|
|
9
|
-
name: "storage_upload_without_content_type_allowlist",
|
|
10
|
-
remediation: 'Pass `allowedContentTypes: ["image/png", "image/jpeg", …]` to `ctx.storage.upload`/`store` so an uploaded object\'s `contentType` must match an approved list. Reject anything HTML/SVG/script-executable unless the app genuinely needs to serve it.',
|
|
11
|
-
run: (context) => {
|
|
12
|
-
if (context.storageUploads === void 0) {
|
|
13
|
-
return [];
|
|
14
|
-
}
|
|
15
|
-
return context.storageUploads.filter((row) => UPLOAD_METHODS.has(row.method) && row.analyzable && !row.presentKeys.includes("allowedContentTypes")).map(
|
|
16
|
-
(row) => emit(storageUploadWithoutContentTypeAllowlist, {
|
|
17
|
-
cacheKey: `storage_upload_without_content_type_allowlist:${row.file}:${row.line.toString()}`,
|
|
18
|
-
detail: `\`ctx.storage.${row.method}\` in \`${row.exportName}\` (${row.file}:${row.line.toString()}) has no \`allowedContentTypes\` allowlist — any content-type is accepted, including \`text/html\`/\`image/svg+xml\`, a stored-XSS risk if the object is served from \`publicBaseUrl\`.`,
|
|
19
|
-
metadata: { exportName: row.exportName, file: row.file, line: row.line, method: row.method }
|
|
20
|
-
})
|
|
21
|
-
);
|
|
22
|
-
},
|
|
23
|
-
source: "static",
|
|
24
|
-
title: "Storage upload with no content-type allowlist"
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export { storageUploadWithoutContentTypeAllowlist as default };
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const UPLOAD_METHODS = /* @__PURE__ */ new Set(["store", "upload"]);
|
|
4
|
-
const storageUploadWithoutMaxSize = {
|
|
5
|
-
categories: ["SECURITY"],
|
|
6
|
-
description: "A `ctx.storage.upload`/`store` call has no `maxSize` cap, so a caller can push an unbounded body straight into R2 through the Worker — unbounded storage/billing exhaustion, and for a streamed body, unbounded worker time.",
|
|
7
|
-
facing: "EXTERNAL",
|
|
8
|
-
level: "WARN",
|
|
9
|
-
name: "storage_upload_without_max_size",
|
|
10
|
-
remediation: "Pass a `maxSize` (bytes) to `ctx.storage.upload`/`store` sized to the largest legitimate object the app accepts. `ArrayBuffer`/`Blob` bodies are rejected up front when oversized; a `ReadableStream` body is aborted mid-transfer once the limit is exceeded.",
|
|
11
|
-
run: (context) => {
|
|
12
|
-
if (context.storageUploads === void 0) {
|
|
13
|
-
return [];
|
|
14
|
-
}
|
|
15
|
-
return context.storageUploads.filter((row) => UPLOAD_METHODS.has(row.method) && row.analyzable && !row.presentKeys.includes("maxSize")).map(
|
|
16
|
-
(row) => emit(storageUploadWithoutMaxSize, {
|
|
17
|
-
cacheKey: `storage_upload_without_max_size:${row.file}:${row.line.toString()}`,
|
|
18
|
-
detail: `\`ctx.storage.${row.method}\` in \`${row.exportName}\` (${row.file}:${row.line.toString()}) has no \`maxSize\` cap — a caller can push an unbounded body into R2, exhausting storage/billing.`,
|
|
19
|
-
metadata: { exportName: row.exportName, file: row.file, line: row.line, method: row.method }
|
|
20
|
-
})
|
|
21
|
-
);
|
|
22
|
-
},
|
|
23
|
-
source: "static",
|
|
24
|
-
title: "Storage upload with no size cap"
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export { storageUploadWithoutMaxSize as default };
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const tableWithoutInsert = {
|
|
4
|
-
categories: ["SCHEMA"],
|
|
5
|
-
description: 'No function inserts into this table via `ctx.db.insert("<table>", …)`. It may be read-only by design (seeded by a migration, replicated, or written through a path the advisor can\'t see) — or it may be dead schema.',
|
|
6
|
-
facing: "INTERNAL",
|
|
7
|
-
level: "INFO",
|
|
8
|
-
name: "table_without_insert",
|
|
9
|
-
remediation: 'If the table should be writable, add a mutation that calls `ctx.db.insert("<table>", …)`. If it is read-only or seeded elsewhere, this advisory can be ignored.',
|
|
10
|
-
run: (context) => {
|
|
11
|
-
if (context.inserts === void 0) {
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
const insertedTables = new Set(context.inserts.filter((write) => write.table !== "").map((write) => write.table));
|
|
15
|
-
const findings = [];
|
|
16
|
-
for (const table of context.schema.tables) {
|
|
17
|
-
if (insertedTables.has(table.name) || table.externallyManaged === true) {
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
findings.push(
|
|
21
|
-
emit(tableWithoutInsert, {
|
|
22
|
-
cacheKey: `table_without_insert:${table.name}`,
|
|
23
|
-
detail: `No function calls \`ctx.db.insert("${table.name}", …)\` — table "${table.name}" has no discovered insert path.`,
|
|
24
|
-
metadata: { table: table.name }
|
|
25
|
-
})
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
return findings;
|
|
29
|
-
},
|
|
30
|
-
source: "static",
|
|
31
|
-
title: "Table has no insert path"
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export { tableWithoutInsert as default };
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const unboundedStringArgument = {
|
|
4
|
-
categories: ["SECURITY"],
|
|
5
|
-
description: "A public `v.string()` argument has no maximum-length bound. An unbounded string lets a client submit arbitrarily large input — abusing storage and CPU on every request that processes it.",
|
|
6
|
-
facing: "EXTERNAL",
|
|
7
|
-
level: "INFO",
|
|
8
|
-
name: "unbounded_string_arg",
|
|
9
|
-
remediation: "Add a max-length bound via `.check(...)` / `.meta({ maxLength })` on the string validator (e.g. cap a name at 256, a body at a few KB). Size the cap to the field's real-world maximum.",
|
|
10
|
-
run: (context) => {
|
|
11
|
-
if (context.argValidators === void 0) {
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
const findings = [];
|
|
15
|
-
for (const procedure of context.argValidators) {
|
|
16
|
-
for (const argument of procedure.unboundedStringArgs) {
|
|
17
|
-
findings.push(
|
|
18
|
-
emit(unboundedStringArgument, {
|
|
19
|
-
cacheKey: `unbounded_string_arg:${procedure.file}:${procedure.exportName}:${argument}`,
|
|
20
|
-
detail: `Arg \`${argument}\` of public procedure \`${procedure.exportName}\` (${procedure.file}:${procedure.line.toString()}) is an unbounded \`v.string()\`. Add a max-length bound to cap payload size.`,
|
|
21
|
-
metadata: { argument, exportName: procedure.exportName, file: procedure.file, line: procedure.line }
|
|
22
|
-
})
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return findings;
|
|
27
|
-
},
|
|
28
|
-
source: "static",
|
|
29
|
-
title: "Public string argument has no length bound"
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export { unboundedStringArgument as default };
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
import { l as leadingIndexedColumns, P as PRIMARY_KEY, s as suggestIndexName } from './fk-index-IUK1ukgs.mjs';
|
|
3
|
-
|
|
4
|
-
const unindexedForeignKey = {
|
|
5
|
-
categories: ["PERFORMANCE"],
|
|
6
|
-
description: "A foreign-key column declared by a `one` relation has no index leading with it. Reads that filter or join on the column full-scan the table, which gets linearly slower as rows accumulate.",
|
|
7
|
-
facing: "EXTERNAL",
|
|
8
|
-
level: "INFO",
|
|
9
|
-
name: "unindexed_foreign_key",
|
|
10
|
-
remediation: 'Add a secondary index leading with the FK column, e.g. `.index("byAuthorId", ["authorId"])`.',
|
|
11
|
-
run: (context) => {
|
|
12
|
-
const findings = [];
|
|
13
|
-
for (const table of context.schema.tables) {
|
|
14
|
-
const indexed = leadingIndexedColumns(table);
|
|
15
|
-
for (const relation of table.relations) {
|
|
16
|
-
if (relation.kind !== "one") {
|
|
17
|
-
continue;
|
|
18
|
-
}
|
|
19
|
-
const fkColumn = relation.field;
|
|
20
|
-
if (fkColumn === PRIMARY_KEY || indexed.has(fkColumn)) {
|
|
21
|
-
continue;
|
|
22
|
-
}
|
|
23
|
-
const suggestedIndex = suggestIndexName(fkColumn);
|
|
24
|
-
findings.push(
|
|
25
|
-
emit(unindexedForeignKey, {
|
|
26
|
-
cacheKey: `unindexed_foreign_key:${table.name}:${fkColumn}`,
|
|
27
|
-
detail: `Relation "${relation.name}" on table "${table.name}" references "${relation.table}" via column "${fkColumn}", which is not the leading column of any index. Reads filtering or joining on "${fkColumn}" full-scan "${table.name}".`,
|
|
28
|
-
metadata: {
|
|
29
|
-
fkColumn,
|
|
30
|
-
references: { column: relation.references, table: relation.table },
|
|
31
|
-
relation: relation.name,
|
|
32
|
-
suggestedIndex: { fields: [fkColumn], name: suggestedIndex },
|
|
33
|
-
table: table.name
|
|
34
|
-
}
|
|
35
|
-
})
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return findings;
|
|
40
|
-
},
|
|
41
|
-
source: "static",
|
|
42
|
-
title: "Unindexed foreign key"
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
export { unindexedForeignKey as default };
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
import { P as PRIMARY_KEY, l as leadingIndexedColumns, s as suggestIndexName } from './fk-index-IUK1ukgs.mjs';
|
|
3
|
-
|
|
4
|
-
const unindexedRelationTarget = {
|
|
5
|
-
categories: ["PERFORMANCE"],
|
|
6
|
-
description: "A `many` relation's foreign-key column on the target table has no index leading with it. Relation predicates (`some`/`none`/`every`) and `with:` child reads filter the target on that column, full-scanning it as rows accumulate.",
|
|
7
|
-
facing: "EXTERNAL",
|
|
8
|
-
level: "INFO",
|
|
9
|
-
name: "unindexed_relation_target",
|
|
10
|
-
remediation: 'Add a secondary index on the target table leading with the FK column, e.g. `.index("byAuthorId", ["authorId"])`.',
|
|
11
|
-
run: (context) => {
|
|
12
|
-
const findings = [];
|
|
13
|
-
const tablesByName = new Map(context.schema.tables.map((table) => [table.name, table]));
|
|
14
|
-
for (const table of context.schema.tables) {
|
|
15
|
-
for (const relation of table.relations) {
|
|
16
|
-
if (relation.kind !== "many") {
|
|
17
|
-
continue;
|
|
18
|
-
}
|
|
19
|
-
const target = tablesByName.get(relation.table);
|
|
20
|
-
if (target === void 0) {
|
|
21
|
-
continue;
|
|
22
|
-
}
|
|
23
|
-
const fkColumn = relation.field;
|
|
24
|
-
if (fkColumn === PRIMARY_KEY) {
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
const coveredByForeignKeyLint = target.relations.some((targetRelation) => targetRelation.kind === "one" && targetRelation.field === fkColumn);
|
|
28
|
-
if (coveredByForeignKeyLint || leadingIndexedColumns(target).has(fkColumn)) {
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
const suggestedIndex = suggestIndexName(fkColumn);
|
|
32
|
-
findings.push(
|
|
33
|
-
emit(unindexedRelationTarget, {
|
|
34
|
-
cacheKey: `unindexed_relation_target:${relation.table}:${fkColumn}`,
|
|
35
|
-
detail: `Relation "${relation.name}" on table "${table.name}" is a to-many over "${relation.table}" via its column "${fkColumn}", which is not the leading column of any index on "${relation.table}". Relation predicates (\`some\`/\`none\`/\`every\`) and \`with:\` reads of "${relation.name}" full-scan "${relation.table}".`,
|
|
36
|
-
metadata: {
|
|
37
|
-
fkColumn,
|
|
38
|
-
references: { column: relation.references, table: table.name },
|
|
39
|
-
relation: relation.name,
|
|
40
|
-
suggestedIndex: { fields: [fkColumn], name: suggestedIndex },
|
|
41
|
-
table: relation.table
|
|
42
|
-
}
|
|
43
|
-
})
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return findings;
|
|
48
|
-
},
|
|
49
|
-
source: "static",
|
|
50
|
-
title: "Unindexed relation target"
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
export { unindexedRelationTarget as default };
|