@lunora/advisor 1.0.0-alpha.34 → 1.0.0-alpha.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +61 -1
- package/dist/index.d.ts +61 -1
- package/dist/index.mjs +1 -200
- package/dist/packem_shared/AE_METRIC_EVENTS-LP_0D7Uw.mjs +1 -0
- package/dist/packem_shared/actionFetchSsrf-BbWxy1Yf.mjs +1 -0
- package/dist/packem_shared/adminRouteWithoutGuard-DSo4t9j4.mjs +1 -0
- package/dist/packem_shared/aiRawRunEscapeHatch-DfhjYm43.mjs +1 -0
- package/dist/packem_shared/aiToolSideEffectPromptInjection-DnWQOoKi.mjs +1 -0
- package/dist/packem_shared/aiUnboundedGenerationPublic-D3HZrh5_.mjs +1 -0
- package/dist/packem_shared/allowUnauthenticatedShardAccessEnabled-D4aMNBb0.mjs +1 -0
- package/dist/packem_shared/argument-derived-sink-BpO3NPPM.mjs +1 -0
- package/dist/packem_shared/authApiCallWithoutHeaders-BItMMdZP.mjs +1 -0
- package/dist/packem_shared/authCsrfCheckDisabled-De9ammtV.mjs +1 -0
- package/dist/packem_shared/authEmailVerificationDisabled-BFKdJcx4.mjs +1 -0
- package/dist/packem_shared/authSecureCookiesDisabled-B4WIzvMm.mjs +1 -0
- package/dist/packem_shared/authSessionFreshageZero-Bpcaf_Pe.mjs +1 -0
- package/dist/packem_shared/authTrustedOriginsWildcard-DjU9NXEm.mjs +1 -0
- package/dist/packem_shared/browserAllowPrivateTargets-Dxyeh6lg.mjs +1 -0
- package/dist/packem_shared/browserUserUrlWithoutAllowlist-BHBS6b0N.mjs +1 -0
- package/dist/packem_shared/circularFk-BMuJKmuZ.mjs +1 -0
- package/dist/packem_shared/constraintValidator-N339mfyx.mjs +1 -0
- package/dist/packem_shared/containerInstanceKeyFromUserInput-C8S4T3ty.mjs +1 -0
- package/dist/packem_shared/containerOversizedInstance-B9wW_sU5.mjs +1 -0
- package/dist/packem_shared/containerPublicInternet-Bx2J7tha.mjs +1 -0
- package/dist/packem_shared/containerRuntimeEgressRelaxation-BQ2ZD1aV.mjs +1 -0
- package/dist/packem_shared/containerStartEnableInternetOverride-B62WUura.mjs +1 -0
- package/dist/packem_shared/dedupeCacheKeys-7JgxaQWV.mjs +1 -0
- package/dist/packem_shared/duplicateIndex-DFT7qLQD.mjs +1 -0
- package/dist/packem_shared/emptyIndex-BiXEfyfN.mjs +1 -0
- package/dist/packem_shared/exportSinkMisconfigured-DvpAaa6j.mjs +1 -0
- package/dist/packem_shared/externalSourceIncrementalNoDeletePath-DnoL64YY.mjs +1 -0
- package/dist/packem_shared/externalSourceOnGlobal-DNPLoNUg.mjs +1 -0
- package/dist/packem_shared/externalSourceUnscoped-DqBp5jYr.mjs +1 -0
- package/dist/packem_shared/filterWithoutIndex-ht2GZQWV.mjs +1 -0
- package/dist/packem_shared/finding-BAVgqnCF.mjs +1 -0
- package/dist/packem_shared/fk-index-Bmk19RvQ.mjs +1 -0
- package/dist/packem_shared/flagGatesSecurityWithUnsafeDefault-DPGFEHTt.mjs +1 -0
- package/dist/packem_shared/fromServerSchema-CJ-AQ2kw.mjs +1 -0
- package/dist/packem_shared/geoIndexFieldNotGeopoint-DAGLZtaH.mjs +1 -0
- package/dist/packem_shared/geoIndexUnused-BgDE6_AR.mjs +1 -0
- package/dist/packem_shared/hardcodedSecret-rIRc3aNk.mjs +1 -0
- package/dist/packem_shared/helpers-DwlvEUCh.mjs +1 -0
- package/dist/packem_shared/hotShard-DVs_U17R.mjs +1 -0
- package/dist/packem_shared/httpActionMissingAuthGuard-BZDdeW_D.mjs +1 -0
- package/dist/packem_shared/httpActionResponseHeaderInjection-D063rB7U.mjs +1 -0
- package/dist/packem_shared/hyperdriveOutsideAction-CRmOQz67.mjs +1 -0
- package/dist/packem_shared/identityUndeclaredClaimTrusted-BYRYGQ74.mjs +1 -0
- package/dist/packem_shared/imagesUrlSourceFromUserInput-Cy5pc_dA.mjs +1 -0
- package/dist/packem_shared/indexReferencesUnknownField-r3QHlaz6.mjs +1 -0
- package/dist/packem_shared/indexUtilization-QfbIH_w4.mjs +1 -0
- package/dist/packem_shared/insertManyUnsafeUserData-DrH7PdRs.mjs +1 -0
- package/dist/packem_shared/kvUnscopedUserKeyIdor-B4lGxRxh.mjs +1 -0
- package/dist/packem_shared/mailInboundDispatchWithoutVerify-BivYJ5F9.mjs +1 -0
- package/dist/packem_shared/mailRecipientFromRequestInput-BR_oSh65.mjs +1 -0
- package/dist/packem_shared/maskUncoveredPiiColumn-DqPfZM2O.mjs +1 -0
- package/dist/packem_shared/maskWeakHashStrategyOnPii-CEiiGgT7.mjs +1 -0
- package/dist/packem_shared/maskedRelationLeakViaWith-CxYk0MDW.mjs +1 -0
- package/dist/packem_shared/mutatorFullRowReplace-CEgecSGm.mjs +1 -0
- package/dist/packem_shared/nondeterministicQueryMutation-BaMfbAk_.mjs +1 -0
- package/dist/packem_shared/normalizeIdUsedAsAuthorization-FjISSXJm.mjs +1 -0
- package/dist/packem_shared/notifyMissingPushConfig-C_GbVw3S.mjs +1 -0
- package/dist/packem_shared/notifySendOutsideAction-B0jZuUP3.mjs +1 -0
- package/dist/packem_shared/outputProjectionMissingOnPublicRead-9Pm9766s.mjs +1 -0
- package/dist/packem_shared/ownerFieldFromArgsNotAuth-BoR9vvst.mjs +1 -0
- package/dist/packem_shared/paymentCreateWithoutAuthorize-CurtqN8K.mjs +1 -0
- package/dist/packem_shared/paymentWebhookWideTolerance-I7ingfR8.mjs +1 -0
- package/dist/packem_shared/plaintextSecretInWranglerVariables-DXjKIm8H.mjs +1 -0
- package/dist/packem_shared/policyReferencesUnknownTable-D3WelAQa.mjs +1 -0
- package/dist/packem_shared/privilegedDispatchUnvalidatedPayload-74GuGye1.mjs +1 -0
- package/dist/packem_shared/privilegedFanoutFromPublicProcedure-C2j2GSUN.mjs +1 -0
- package/dist/packem_shared/publicArgumentUsesAny-BJb4cTqL.mjs +1 -0
- package/dist/packem_shared/publicMutationWithoutRatelimit-D9phluHW.mjs +1 -0
- package/dist/packem_shared/publicTableRlsOptoutConfusion-CaWtPGAn.mjs +1 -0
- package/dist/packem_shared/queueWithoutDlq-BLY3iy9Q.mjs +1 -0
- package/dist/packem_shared/r2sqlOutsideAction-CKK_I1Ms.mjs +1 -0
- package/dist/packem_shared/ratelimitDefaultMemoryStore-mOdI3Jmb.mjs +1 -0
- package/dist/packem_shared/ratelimitKeySpoofableOrGlobal-DaGId_Fh.mjs +1 -0
- package/dist/packem_shared/ratelimitMiddlewareFailOpen-CTWVosEc.mjs +1 -0
- package/dist/packem_shared/relationReferencesUnknownField-clqyB69Y.mjs +1 -0
- package/dist/packem_shared/relationReferencesUnknownTable-CLdrhkmS.mjs +1 -0
- package/dist/packem_shared/rlsUncoveredTable-DYHgnoos.mjs +1 -0
- package/dist/packem_shared/shapeTargetsGlobalTable-Blz-aSkg.mjs +1 -0
- package/dist/packem_shared/shapeUnknownTable-DFxtyFio.mjs +1 -0
- package/dist/packem_shared/signupMutationWithoutDisposableGating-3ir808Vz.mjs +1 -0
- package/dist/packem_shared/softDeleteIncludeDeletedFromArgs-DgxpQHRA.mjs +1 -0
- package/dist/packem_shared/sqlInjectionRisk-D2NaT-5c.mjs +1 -0
- package/dist/packem_shared/storageGenerateUploadUrlNoContentTypePin-C9cjl6Jh.mjs +1 -0
- package/dist/packem_shared/storageKeyFromUserArgs-BeS5cp56.mjs +1 -0
- package/dist/packem_shared/storagePresignedUrlForPrivateContent-j7VhwIdu.mjs +1 -0
- package/dist/packem_shared/storageUploadWithoutContentTypeAllowlist-oxKLecuf.mjs +1 -0
- package/dist/packem_shared/storageUploadWithoutMaxSize-DUDxPutk.mjs +1 -0
- package/dist/packem_shared/tableWithoutInsert-D3AhaNzR.mjs +1 -0
- package/dist/packem_shared/ttlFieldNotTimestamp-BFiRkSZD.mjs +1 -0
- package/dist/packem_shared/unboundedStringArgument-AkkIcr-S.mjs +1 -0
- package/dist/packem_shared/unindexedForeignKey-DK4eWXkb.mjs +1 -0
- package/dist/packem_shared/unindexedRelationTarget-B5fFq1E8.mjs +1 -0
- package/dist/packem_shared/unrestrictedWhereBranch-YKiydie4.mjs +1 -0
- package/dist/packem_shared/userCreatingMutationWithoutCaptcha-iDjRkUFO.mjs +1 -0
- package/dist/packem_shared/vectorsNamespaceFromUserInput-Cyq5B2kF.mjs +1 -0
- package/dist/packem_shared/workflowDuplicateStepName-cXF6Rpvg.mjs +1 -0
- package/dist/packem_shared/workflowUnknownTarget-JF8rITLP.mjs +1 -0
- package/dist/packem_shared/workflowUnused-Dx_FQwGE.mjs +1 -0
- package/package.json +2 -2
- 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-CNcAVuYa.mjs +0 -127
- package/dist/packem_shared/constraintValidator-CxwtpJ6E.mjs +0 -206
- 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/dedupeCacheKeys-r5B7u_yq.mjs +0 -13
- package/dist/packem_shared/duplicateIndex-BOublMSt.mjs +0 -57
- package/dist/packem_shared/emptyIndex-BX8EuEY7.mjs +0 -32
- package/dist/packem_shared/exportSinkMisconfigured-897sVMUa.mjs +0 -46
- package/dist/packem_shared/externalSourceIncrementalNoDeletePath-DvX9cRBD.mjs +0 -45
- 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-CVnbSf8j.mjs +0 -70
- package/dist/packem_shared/geoIndexFieldNotGeopoint-CcaInLhY.mjs +0 -40
- package/dist/packem_shared/geoIndexUnused-D7I0UyND.mjs +0 -40
- package/dist/packem_shared/hardcodedSecret-Be-pKVdn.mjs +0 -31
- package/dist/packem_shared/helpers-BySnKhVB.mjs +0 -31
- package/dist/packem_shared/hotShard-CkC7qpre.mjs +0 -60
- 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/notifyMissingPushConfig-Ca9EzRzy.mjs +0 -27
- package/dist/packem_shared/notifySendOutsideAction-BUZSIZa4.mjs +0 -30
- 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/signupMutationWithoutDisposableGating-Dnbol2CS.mjs +0 -39
- 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/ttlFieldNotTimestamp-Dpaj63Z6.mjs +0 -36
- 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,26 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const actionFetchSsrf = {
|
|
4
|
-
categories: ["SECURITY"],
|
|
5
|
-
description: "An action's `ctx.fetch(url)` fetches a URL derived from the handler's `args`. `ctx.fetch` applies no host allowlist, so request-controlled URLs let a caller reach cloud metadata endpoints or internal services — server-side request forgery.",
|
|
6
|
-
facing: "EXTERNAL",
|
|
7
|
-
level: "ERROR",
|
|
8
|
-
name: "action_fetch_ssrf",
|
|
9
|
-
remediation: "Validate the URL against an allowlist of expected hosts before fetching, and reject private, loopback, and link-local addresses (notably `169.254.169.254`). Never pass request input straight to `ctx.fetch`.",
|
|
10
|
-
run: (context) => {
|
|
11
|
-
if (context.argumentDerivedFetches === void 0) {
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
return context.argumentDerivedFetches.map(
|
|
15
|
-
(fetchCall) => emit(actionFetchSsrf, {
|
|
16
|
-
cacheKey: `action_fetch_ssrf:${fetchCall.file}:${fetchCall.line.toString()}`,
|
|
17
|
-
detail: `\`ctx.fetch\` in \`${fetchCall.exportName}\` (${fetchCall.file}:${fetchCall.line.toString()}) fetches a URL derived from \`args\` — a server-side request forgery vector. Validate the URL against a host allowlist and reject private/link-local targets before fetching.`,
|
|
18
|
-
metadata: { exportName: fetchCall.exportName, file: fetchCall.file, line: fetchCall.line }
|
|
19
|
-
})
|
|
20
|
-
);
|
|
21
|
-
},
|
|
22
|
-
source: "static",
|
|
23
|
-
title: "Possible SSRF from arg-derived ctx.fetch URL"
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export { actionFetchSsrf as default };
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const adminRouteWithoutGuard = {
|
|
4
|
-
categories: ["SECURITY"],
|
|
5
|
-
description: "An `httpRoute` on an admin/privileged path has no visible auth/admin guard. REST routes aren't covered by RLS, so an unguarded `/admin/*` route is callable by anyone who reaches the URL.",
|
|
6
|
-
facing: "EXTERNAL",
|
|
7
|
-
level: "WARN",
|
|
8
|
-
name: "admin_route_without_guard",
|
|
9
|
-
remediation: "Assert an authenticated, authorized caller at the top of the handler — check `ctx.auth` / `getSession(...)`, or a `requireAdmin`-style guard — and reject (401/403) before doing privileged work. For machine callers, verify a secret with a timing-safe compare.",
|
|
10
|
-
run: (context) => {
|
|
11
|
-
if (context.adminRoutes === void 0) {
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
const findings = [];
|
|
15
|
-
for (const route of context.adminRoutes) {
|
|
16
|
-
if (route.usesGuard) {
|
|
17
|
-
continue;
|
|
18
|
-
}
|
|
19
|
-
findings.push(
|
|
20
|
-
emit(adminRouteWithoutGuard, {
|
|
21
|
-
cacheKey: `admin_route_without_guard:${route.file}:${route.method}:${route.path}`,
|
|
22
|
-
detail: `Route \`${route.method} ${route.path}\` (\`${route.exportName}\` in ${route.file}) is on an admin/privileged path but its handler shows no auth/admin guard. Assert an authorized caller before doing privileged work.`,
|
|
23
|
-
metadata: { exportName: route.exportName, file: route.file, method: route.method, path: route.path }
|
|
24
|
-
})
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
return findings;
|
|
28
|
-
},
|
|
29
|
-
source: "static",
|
|
30
|
-
title: "Admin route without an auth guard"
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export { adminRouteWithoutGuard as default };
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const aiRawRunEscapeHatch = {
|
|
4
|
-
categories: ["SECURITY"],
|
|
5
|
-
description: "A `ctx.ai.run(model, …)` call selects its model from the handler's `args` with no server-side scoping. `ctx.ai.run` is the raw Workers AI binding passthrough, so an arg-derived model id lets any caller pick an arbitrary model — bypassing the typed `ctx.ai.model(...)` + AI-SDK layer's cap/schema.",
|
|
6
|
-
facing: "EXTERNAL",
|
|
7
|
-
level: "WARN",
|
|
8
|
-
name: "ai_raw_run_escape_hatch",
|
|
9
|
-
remediation: `Select the model from a fixed server-side allowlist — never from \`args\`. Prefer the typed \`ctx.ai.model(...)\` + \`generateText\`/\`streamText\` path with an output cap over the raw \`ctx.ai.run\` escape hatch.`,
|
|
10
|
-
run: (context) => {
|
|
11
|
-
if (context.aiRawRuns === void 0) {
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
return context.aiRawRuns.map(
|
|
15
|
-
(access) => emit(aiRawRunEscapeHatch, {
|
|
16
|
-
cacheKey: `ai_raw_run_escape_hatch:${access.file}:${access.line.toString()}`,
|
|
17
|
-
detail: `\`ctx.ai.run\` in \`${access.exportName}\` (${access.file}:${access.line.toString()}) selects its model from \`args\` with no server-side scoping — any caller can pick an arbitrary model, bypassing the typed AI-SDK layer's cap/schema. Select the model from a fixed server-side allowlist instead.`,
|
|
18
|
-
metadata: { exportName: access.exportName, file: access.file, line: access.line }
|
|
19
|
-
})
|
|
20
|
-
);
|
|
21
|
-
},
|
|
22
|
-
source: "static",
|
|
23
|
-
title: "Possible arbitrary model selection from arg-derived ctx.ai.run"
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export { aiRawRunEscapeHatch as default };
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const aiToolSideEffectPromptInjection = {
|
|
4
|
-
categories: ["SECURITY"],
|
|
5
|
-
description: "A `generateText` / `streamText` call whose prompt/messages carry user-supplied text and whose model-callable `tools` perform a privileged side effect (DB write, `ctx.run` dispatch, or outbound send). An injected instruction in the user text can steer the model into firing that action — the confused-deputy prompt-injection hazard.",
|
|
6
|
-
facing: "EXTERNAL",
|
|
7
|
-
level: "WARN",
|
|
8
|
-
name: "ai_tool_side_effect_prompt_injection",
|
|
9
|
-
remediation: "Don't let a model with user-derived input directly trigger a privileged action. Have the tool return a proposal the server validates and authorizes before executing, gate the side effect behind an explicit human/step confirmation, or keep side-effecting tools out of any generation whose prompt includes untrusted input.",
|
|
10
|
-
run: (context) => {
|
|
11
|
-
if (context.aiToolSideEffects === void 0) {
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
return context.aiToolSideEffects.filter((row) => row.userInputDerived).map((row) => {
|
|
15
|
-
const location = `\`${row.exportName}\` (${row.file}:${row.line.toString()})`;
|
|
16
|
-
return emit(aiToolSideEffectPromptInjection, {
|
|
17
|
-
cacheKey: `ai_tool_side_effect_prompt_injection:${row.file}:${row.line.toString()}`,
|
|
18
|
-
detail: `\`${row.method}\` in ${location} feeds user-derived input to a model whose tools reach \`${row.sideEffect}\`. An injected instruction can steer the model into firing that side effect.`,
|
|
19
|
-
metadata: {
|
|
20
|
-
exportName: row.exportName,
|
|
21
|
-
file: row.file,
|
|
22
|
-
line: row.line,
|
|
23
|
-
method: row.method,
|
|
24
|
-
sideEffect: row.sideEffect
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
},
|
|
29
|
-
source: "static",
|
|
30
|
-
title: "AI tool side effect reachable via prompt injection"
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export { aiToolSideEffectPromptInjection as default };
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const aiUnboundedGenerationPublic = {
|
|
4
|
-
categories: ["SECURITY"],
|
|
5
|
-
description: "A public procedure runs an AI generation (`generateText`/`streamText`/`generateObject`/`streamObject`) with no `maxOutputTokens` bound. Generation bills per output token, so an anonymous caller can request arbitrarily long completions in a loop — a denial-of-wallet vector that also ties up worker time on long streams.",
|
|
6
|
-
facing: "EXTERNAL",
|
|
7
|
-
level: "WARN",
|
|
8
|
-
name: "ai_unbounded_generation_public",
|
|
9
|
-
remediation: "Cap the completion with `maxOutputTokens` in the generation config, and rate-limit the public entry point (`.use(rateLimit(...))`). For expensive models, prefer running generation from an internal, authenticated path rather than exposing it directly to anonymous callers.",
|
|
10
|
-
run: (context) => {
|
|
11
|
-
if (context.procedureProtections === void 0) {
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
return context.procedureProtections.filter((procedure) => procedure.unboundedAiGeneration && procedure.visibility === "public").map(
|
|
15
|
-
(procedure) => emit(aiUnboundedGenerationPublic, {
|
|
16
|
-
cacheKey: `ai_unbounded_generation_public:${procedure.file}:${procedure.exportName}`,
|
|
17
|
-
detail: `\`${procedure.exportName}\` (${procedure.file}) is public and runs an AI generation with no \`maxOutputTokens\` bound — an anonymous caller can drive unbounded, billable completions in a loop. Add a \`maxOutputTokens\` cap and rate-limit the entry point.`,
|
|
18
|
-
metadata: { exportName: procedure.exportName, file: procedure.file, kind: procedure.kind }
|
|
19
|
-
})
|
|
20
|
-
);
|
|
21
|
-
},
|
|
22
|
-
source: "static",
|
|
23
|
-
title: "Public procedure runs unbounded AI generation (no maxOutputTokens)"
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export { aiUnboundedGenerationPublic as default };
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const allowUnauthenticatedShardAccessEnabled = {
|
|
4
|
-
categories: ["SECURITY"],
|
|
5
|
-
description: "`.extend(() => ({ allowUnauthenticatedShardAccess: true }))` disables the fail-closed default that denies an unauthenticated shard lookup. Combined with a schema that doesn't enforce `.rls(\"required\")` everywhere (or that leaves a table `.public()`), an unauthenticated caller can shard-hop into another tenant's rows with no row-security guard behind the door.",
|
|
6
|
-
facing: "EXTERNAL",
|
|
7
|
-
level: "WARN",
|
|
8
|
-
name: "allow_unauthenticated_shard_access_enabled",
|
|
9
|
-
remediation: "Prefer `authorizeShard` / `authorizeFanOut` (which take precedence over `allowUnauthenticatedShardAccess` and let you allow specific unauthenticated cases) over the blanket opt-out. If the app genuinely needs open shard access, enforce `.rls(\"required\")` on the schema with no `.public()` tables so a missing identity still can't read another tenant's rows.",
|
|
10
|
-
run: (context) => {
|
|
11
|
-
if (context.configCalls === void 0) {
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
const hasRlsGap = context.schema.rlsMode !== "required" || context.schema.tables.some((table) => table.isPublic);
|
|
15
|
-
if (!hasRlsGap) {
|
|
16
|
-
return [];
|
|
17
|
-
}
|
|
18
|
-
return context.configCalls.filter((call) => call.callee === "extend" && call.trueKeys.includes("allowUnauthenticatedShardAccess")).map(
|
|
19
|
-
(call) => emit(allowUnauthenticatedShardAccessEnabled, {
|
|
20
|
-
cacheKey: `allow_unauthenticated_shard_access_enabled:${call.file}:${call.line.toString()}`,
|
|
21
|
-
detail: `\`.extend(...)\` in ${call.file}:${call.line.toString()} sets \`allowUnauthenticatedShardAccess: true\`, and the schema has an RLS gap (no \`.rls("required")\`, or a \`.public()\` table) — an unauthenticated caller can shard-hop into another tenant's rows with no row-security guard behind the door.`,
|
|
22
|
-
metadata: { callee: call.callee, file: call.file, line: call.line }
|
|
23
|
-
})
|
|
24
|
-
);
|
|
25
|
-
},
|
|
26
|
-
source: "static",
|
|
27
|
-
title: "Unauthenticated shard access enabled on an RLS-gapped schema"
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export { allowUnauthenticatedShardAccessEnabled as default };
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const makeArgumentDerivedSinkLint = (config) => {
|
|
4
|
-
const lint = {
|
|
5
|
-
categories: config.categories,
|
|
6
|
-
description: config.description,
|
|
7
|
-
facing: config.facing,
|
|
8
|
-
level: config.level,
|
|
9
|
-
name: config.name,
|
|
10
|
-
remediation: config.remediation,
|
|
11
|
-
run: (context) => {
|
|
12
|
-
const accesses = config.getAccesses(context);
|
|
13
|
-
if (accesses === void 0 || config.suppressWhen?.(context) === true) {
|
|
14
|
-
return [];
|
|
15
|
-
}
|
|
16
|
-
return accesses.map(
|
|
17
|
-
(access) => emit(lint, {
|
|
18
|
-
cacheKey: config.cacheKey(access),
|
|
19
|
-
detail: config.detail(access),
|
|
20
|
-
metadata: config.metadata(access)
|
|
21
|
-
})
|
|
22
|
-
);
|
|
23
|
-
},
|
|
24
|
-
source: "static",
|
|
25
|
-
title: config.title
|
|
26
|
-
};
|
|
27
|
-
return lint;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export { makeArgumentDerivedSinkLint as m };
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const authApiCallWithoutHeaders = {
|
|
4
|
-
categories: ["SECURITY"],
|
|
5
|
-
description: "A `ctx.authApi.<method>(...)` call omits `headers` — better-auth skips session authorization and runs the call with full server-to-server privileges, regardless of the caller's identity.",
|
|
6
|
-
facing: "EXTERNAL",
|
|
7
|
-
level: "WARN",
|
|
8
|
-
name: "auth_api_call_without_headers",
|
|
9
|
-
remediation: "Pass the inbound `headers` from the incoming request into every `ctx.authApi.*` call: `ctx.authApi.<method>({ body, headers: request.headers })`. See the `withAuthPlugins` middleware doc for details.",
|
|
10
|
-
run: (context) => {
|
|
11
|
-
if (context.authApiCalls === void 0) {
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
const findings = [];
|
|
15
|
-
const occurrenceCount = /* @__PURE__ */ new Map();
|
|
16
|
-
for (const call of context.authApiCalls) {
|
|
17
|
-
if (call.hasHeaders) {
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
const baseKey = `${call.file}:${call.line.toString()}:${call.method}`;
|
|
21
|
-
const occurrence = (occurrenceCount.get(baseKey) ?? 0) + 1;
|
|
22
|
-
occurrenceCount.set(baseKey, occurrence);
|
|
23
|
-
const occurrenceSuffix = occurrence > 1 ? `:${occurrence.toString()}` : "";
|
|
24
|
-
findings.push(
|
|
25
|
-
emit(authApiCallWithoutHeaders, {
|
|
26
|
-
cacheKey: `auth_api_call_without_headers:${baseKey}${occurrenceSuffix}`,
|
|
27
|
-
detail: `\`ctx.authApi.${call.method || "<method>"}(…)\` in ${call.exportName} (${call.file}:${call.line.toString()}) is called without \`headers\` — better-auth skips session authorization, so this runs with full privileges. Pass the inbound \`headers\`.`,
|
|
28
|
-
metadata: { exportName: call.exportName, file: call.file, line: call.line, method: call.method }
|
|
29
|
-
})
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
return findings;
|
|
33
|
-
},
|
|
34
|
-
source: "static",
|
|
35
|
-
title: "Privileged ctx.authApi call missing headers"
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export { authApiCallWithoutHeaders as default };
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const authCsrfCheckDisabled = {
|
|
4
|
-
categories: ["SECURITY"],
|
|
5
|
-
description: "A `createAuth({...})` call sets `advanced.disableCSRFCheck: true`, turning off better-auth's origin validation for state-changing auth requests — a cross-site request riding the ambient auth cookie is processed like a same-origin one.",
|
|
6
|
-
facing: "EXTERNAL",
|
|
7
|
-
level: "ERROR",
|
|
8
|
-
name: "auth_csrf_check_disabled",
|
|
9
|
-
remediation: "Remove `advanced.disableCSRFCheck: true` from `createAuth({...})` and rely on `trustedOrigins` for origin validation instead of disabling the check.",
|
|
10
|
-
run: (context) => {
|
|
11
|
-
if (context.authConfigs === void 0) {
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
return context.authConfigs.filter((config) => config.analyzable && config.disableCsrfCheck).map(
|
|
15
|
-
(config) => emit(authCsrfCheckDisabled, {
|
|
16
|
-
cacheKey: `auth_csrf_check_disabled:${config.file}:${config.line.toString()}`,
|
|
17
|
-
detail: `\`createAuth\` in \`${config.exportName}\` (${config.file}:${config.line.toString()}) sets \`advanced.disableCSRFCheck: true\`, turning off origin validation for state-changing auth requests. Remove the flag and rely on \`trustedOrigins\` instead.`,
|
|
18
|
-
metadata: { exportName: config.exportName, file: config.file, line: config.line }
|
|
19
|
-
})
|
|
20
|
-
);
|
|
21
|
-
},
|
|
22
|
-
source: "static",
|
|
23
|
-
title: "createAuth CSRF check disabled"
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export { authCsrfCheckDisabled as default };
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const authEmailVerificationDisabled = {
|
|
4
|
-
categories: ["SECURITY"],
|
|
5
|
-
description: "A `createAuth({...})` call enables `emailAndPassword` with no `requireEmailVerification: true`. better-auth defaults email verification off, so an account is usable before the caller has proven ownership of the email address.",
|
|
6
|
-
facing: "EXTERNAL",
|
|
7
|
-
level: "WARN",
|
|
8
|
-
name: "auth_email_verification_disabled",
|
|
9
|
-
remediation: "Set `emailAndPassword.requireEmailVerification: true` on `createAuth({...})` so a new account can't act until its email address is verified.",
|
|
10
|
-
run: (context) => {
|
|
11
|
-
if (context.authConfigs === void 0) {
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
return context.authConfigs.filter((config) => config.analyzable && config.emailPasswordEnabled && !config.requireEmailVerification).map(
|
|
15
|
-
(config) => emit(authEmailVerificationDisabled, {
|
|
16
|
-
cacheKey: `auth_email_verification_disabled:${config.file}:${config.line.toString()}`,
|
|
17
|
-
detail: `\`createAuth\` in \`${config.exportName}\` (${config.file}:${config.line.toString()}) enables \`emailAndPassword\` with no \`requireEmailVerification: true\`, so an account is usable before its email is proven. Set \`emailAndPassword.requireEmailVerification: true\`.`,
|
|
18
|
-
metadata: { exportName: config.exportName, file: config.file, line: config.line }
|
|
19
|
-
})
|
|
20
|
-
);
|
|
21
|
-
},
|
|
22
|
-
source: "static",
|
|
23
|
-
title: "createAuth email verification disabled"
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export { authEmailVerificationDisabled as default };
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const authSecureCookiesDisabled = {
|
|
4
|
-
categories: ["SECURITY"],
|
|
5
|
-
description: "A `createAuth({...})` call sets `advanced.useSecureCookies: false`, overriding Lunora's secure-by-default posture — the session cookie ships without `Secure` even on an HTTPS deployment, so it can be sent over a plaintext connection.",
|
|
6
|
-
facing: "EXTERNAL",
|
|
7
|
-
level: "ERROR",
|
|
8
|
-
name: "auth_secure_cookies_disabled",
|
|
9
|
-
remediation: "Remove `advanced.useSecureCookies: false` from `createAuth({...})` — let Lunora's secure-by-default posture set the `Secure` cookie attribute for an HTTPS deployment.",
|
|
10
|
-
run: (context) => {
|
|
11
|
-
if (context.authConfigs === void 0) {
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
return context.authConfigs.filter((config) => config.analyzable && config.secureCookiesDisabled).map(
|
|
15
|
-
(config) => emit(authSecureCookiesDisabled, {
|
|
16
|
-
cacheKey: `auth_secure_cookies_disabled:${config.file}:${config.line.toString()}`,
|
|
17
|
-
detail: `\`createAuth\` in \`${config.exportName}\` (${config.file}:${config.line.toString()}) sets \`advanced.useSecureCookies: false\`, so the session cookie ships without \`Secure\` even on an HTTPS deployment. Remove the override and let Lunora's secure-by-default posture apply.`,
|
|
18
|
-
metadata: { exportName: config.exportName, file: config.file, line: config.line }
|
|
19
|
-
})
|
|
20
|
-
);
|
|
21
|
-
},
|
|
22
|
-
source: "static",
|
|
23
|
-
title: "createAuth secure cookies disabled"
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export { authSecureCookiesDisabled as default };
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const authSessionFreshageZero = {
|
|
4
|
-
categories: ["SECURITY"],
|
|
5
|
-
description: "A `createAuth({...})` call sets `session.freshAge: 0`, disabling better-auth's recent-reauth check for sensitive operations — a long-lived stolen session can perform them without proving the caller still controls the credentials.",
|
|
6
|
-
facing: "EXTERNAL",
|
|
7
|
-
level: "WARN",
|
|
8
|
-
name: "auth_session_freshage_zero",
|
|
9
|
-
remediation: "Remove `session.freshAge: 0` from `createAuth({...})` (or set it to a real window) so sensitive operations require a recently re-authenticated session.",
|
|
10
|
-
run: (context) => {
|
|
11
|
-
if (context.authConfigs === void 0) {
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
return context.authConfigs.filter((config) => config.analyzable && config.sessionFreshAgeZero).map(
|
|
15
|
-
(config) => emit(authSessionFreshageZero, {
|
|
16
|
-
cacheKey: `auth_session_freshage_zero:${config.file}:${config.line.toString()}`,
|
|
17
|
-
detail: `\`createAuth\` in \`${config.exportName}\` (${config.file}:${config.line.toString()}) sets \`session.freshAge: 0\`, disabling the recent-reauth check for sensitive operations. Remove the override or set a real window.`,
|
|
18
|
-
metadata: { exportName: config.exportName, file: config.file, line: config.line }
|
|
19
|
-
})
|
|
20
|
-
);
|
|
21
|
-
},
|
|
22
|
-
source: "static",
|
|
23
|
-
title: "createAuth session freshAge zero"
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export { authSessionFreshageZero as default };
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const authTrustedOriginsWildcard = {
|
|
4
|
-
categories: ["SECURITY"],
|
|
5
|
-
description: "A `createAuth({...})` call's `trustedOrigins` array contains a `\"*\"` entry, disabling better-auth's CSRF/origin validation entirely — any origin is accepted for state-changing auth requests.",
|
|
6
|
-
facing: "EXTERNAL",
|
|
7
|
-
level: "ERROR",
|
|
8
|
-
name: "auth_trusted_origins_wildcard",
|
|
9
|
-
remediation: 'List the specific origins your deployment serves in `trustedOrigins` instead of `"*"` — a wildcard disables origin validation for every caller.',
|
|
10
|
-
run: (context) => {
|
|
11
|
-
if (context.authConfigs === void 0) {
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
return context.authConfigs.filter((config) => config.analyzable && config.trustedOriginsWildcard).map(
|
|
15
|
-
(config) => emit(authTrustedOriginsWildcard, {
|
|
16
|
-
cacheKey: `auth_trusted_origins_wildcard:${config.file}:${config.line.toString()}`,
|
|
17
|
-
detail: `\`createAuth\` in \`${config.exportName}\` (${config.file}:${config.line.toString()}) sets \`trustedOrigins\` to include \`"*"\`, disabling CSRF/origin validation for every request. List the specific origins your deployment serves instead.`,
|
|
18
|
-
metadata: { exportName: config.exportName, file: config.file, line: config.line }
|
|
19
|
-
})
|
|
20
|
-
);
|
|
21
|
-
},
|
|
22
|
-
source: "static",
|
|
23
|
-
title: "createAuth trustedOrigins wildcard"
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export { authTrustedOriginsWildcard as default };
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const browserAllowPrivateTargets = {
|
|
4
|
-
categories: ["SECURITY"],
|
|
5
|
-
description: "`createBrowser({ allowPrivateTargets: true })` disables the private/internal-IP SSRF guard, letting a navigated URL reach cloud metadata endpoints, internal services, or loopback. Combined with a request-supplied URL this is a server-side request forgery path.",
|
|
6
|
-
facing: "EXTERNAL",
|
|
7
|
-
level: "ERROR",
|
|
8
|
-
name: "browser_allow_private_targets",
|
|
9
|
-
remediation: "Remove `allowPrivateTargets: true` from `createBrowser({...})` so the private/internal-address guard stays on. If a specific internal host is genuinely required, pin it with an `allowedHosts` allowlist instead of opening all private targets.",
|
|
10
|
-
run: (context) => {
|
|
11
|
-
if (context.configCalls === void 0) {
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
return context.configCalls.filter((call) => call.callee === "createBrowser" && call.trueKeys.includes("allowPrivateTargets")).map(
|
|
15
|
-
(call) => emit(browserAllowPrivateTargets, {
|
|
16
|
-
cacheKey: `browser_allow_private_targets:${call.file}:${call.line.toString()}`,
|
|
17
|
-
detail: `\`createBrowser({ allowPrivateTargets: true })\` in ${call.file}:${call.line.toString()} disables the private/internal-address SSRF guard — a navigated URL can then reach cloud metadata, internal services, or loopback. Remove the override, or pin the specific host with \`allowedHosts\` instead.`,
|
|
18
|
-
metadata: { callee: call.callee, file: call.file, line: call.line }
|
|
19
|
-
})
|
|
20
|
-
);
|
|
21
|
-
},
|
|
22
|
-
source: "static",
|
|
23
|
-
title: "Browser with private-target SSRF guard disabled"
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export { browserAllowPrivateTargets as default };
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { m as makeArgumentDerivedSinkLint } from './argument-derived-sink-C1xTTqAt.mjs';
|
|
2
|
-
|
|
3
|
-
const browserUserUrlWithoutAllowlist = makeArgumentDerivedSinkLint({
|
|
4
|
-
cacheKey: (access) => `browser_user_url_without_allowlist:${access.file}:${access.line.toString()}`,
|
|
5
|
-
categories: ["SECURITY"],
|
|
6
|
-
description: "A `ctx.browser.<method>(url, …)` call navigates to a URL derived from the handler's `args` with no server-side scoping, and no `createBrowser` allowlist contains it. The default guard blocks private targets but not arbitrary public URLs, so any caller can make the headless browser an open-proxy / SSRF tool (fetch arbitrary third-party URLs, DNS-rebind to internal hosts).",
|
|
7
|
-
detail: (access) => `\`ctx.browser.${access.method}\` in \`${access.exportName}\` (${access.file}:${access.line.toString()}) navigates to a URL derived from \`args\` with no server-side scoping, and no \`createBrowser\` allowlist contains it — the default guard blocks private targets but not arbitrary public URLs, so any caller can turn the headless browser into an open-proxy / SSRF tool. Pin \`allowedHosts\` (and/or \`resolveDns\`) on \`createBrowser({...})\`, and derive the URL from server-trusted state where possible.`,
|
|
8
|
-
facing: "EXTERNAL",
|
|
9
|
-
getAccesses: (context) => context.browserUrlAccesses,
|
|
10
|
-
level: "WARN",
|
|
11
|
-
metadata: (access) => {
|
|
12
|
-
return { exportName: access.exportName, file: access.file, line: access.line, method: access.method };
|
|
13
|
-
},
|
|
14
|
-
name: "browser_user_url_without_allowlist",
|
|
15
|
-
remediation: "Pin the browser with an `allowedHosts` allowlist (and/or `resolveDns`) on `createBrowser({...})`, and derive the navigation URL from server-trusted state where possible rather than passing `args` straight to `ctx.browser`.",
|
|
16
|
-
// A `createBrowser` hardened with an `allowedHosts` allowlist or a pinned
|
|
17
|
-
// `resolveDns` contains the SSRF surface — suppress every finding when one is
|
|
18
|
-
// visible. Only an analyzable (non-spread, static object-literal) config call
|
|
19
|
-
// counts; an opaque config could set the key elsewhere but can't be relied on.
|
|
20
|
-
suppressWhen: (context) => (context.configCalls ?? []).some(
|
|
21
|
-
(call) => call.callee === "createBrowser" && call.analyzable && (call.presentKeys.includes("allowedHosts") || call.presentKeys.includes("resolveDns"))
|
|
22
|
-
),
|
|
23
|
-
title: "Browser navigates to arg-derived URL with no allowlist"
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
export { browserUserUrlWithoutAllowlist as default };
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const MAX_CYCLES = 100;
|
|
4
|
-
const circularFk = {
|
|
5
|
-
categories: ["SCHEMA"],
|
|
6
|
-
description: "A chain of foreign-key relations forms a cycle (e.g. A → B → C → A). Circular FK dependencies can cause unexpected behavior during DELETE operations — a CASCADE chain may loop indefinitely, and even a RESTRICT cycle prevents deletion of any row in the loop without temporarily disabling constraints.",
|
|
7
|
-
facing: "INTERNAL",
|
|
8
|
-
level: "WARN",
|
|
9
|
-
name: "circular_fk",
|
|
10
|
-
remediation: "Remove or break the cycle by dropping at least one FK relation from the loop. Consider replacing the circular dependency with a nullable FK and explicit application logic, or restructure the schema using a junction table.",
|
|
11
|
-
run: (context) => {
|
|
12
|
-
const findings = [];
|
|
13
|
-
const order = [...new Set(context.schema.tables.map((table) => table.name))].sort();
|
|
14
|
-
const indexOf = new Map(order.map((name, index) => [name, index]));
|
|
15
|
-
const adjacency = /* @__PURE__ */ new Map();
|
|
16
|
-
for (const table of context.schema.tables) {
|
|
17
|
-
for (const relation of table.relations) {
|
|
18
|
-
if (relation.kind !== "one" || !indexOf.has(relation.table)) {
|
|
19
|
-
continue;
|
|
20
|
-
}
|
|
21
|
-
let targets = adjacency.get(table.name);
|
|
22
|
-
if (targets === void 0) {
|
|
23
|
-
targets = /* @__PURE__ */ new Set();
|
|
24
|
-
adjacency.set(table.name, targets);
|
|
25
|
-
}
|
|
26
|
-
targets.add(relation.table);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
const blocked = /* @__PURE__ */ new Set();
|
|
30
|
-
const blockMap = /* @__PURE__ */ new Map();
|
|
31
|
-
const stack = [];
|
|
32
|
-
const reported = /* @__PURE__ */ new Set();
|
|
33
|
-
let capped = false;
|
|
34
|
-
const unblock = (vertex) => {
|
|
35
|
-
blocked.delete(vertex);
|
|
36
|
-
const dependents = blockMap.get(vertex);
|
|
37
|
-
if (dependents === void 0) {
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
for (const dependent of dependents) {
|
|
41
|
-
dependents.delete(dependent);
|
|
42
|
-
if (blocked.has(dependent)) {
|
|
43
|
-
unblock(dependent);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
const emitCurrentCycle = () => {
|
|
48
|
-
const cycle = [...stack];
|
|
49
|
-
if (cycle.length < 2) {
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
let minIndex = 0;
|
|
53
|
-
for (let index = 1; index < cycle.length; index += 1) {
|
|
54
|
-
if (cycle[index] < cycle[minIndex]) {
|
|
55
|
-
minIndex = index;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
const canonical = [...cycle.slice(minIndex), ...cycle.slice(0, minIndex)];
|
|
59
|
-
const key = canonical.join("→");
|
|
60
|
-
if (reported.has(key)) {
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
reported.add(key);
|
|
64
|
-
const displayPath = [...canonical, canonical[0]].join(" → ");
|
|
65
|
-
findings.push(
|
|
66
|
-
emit(circularFk, {
|
|
67
|
-
cacheKey: `circular_fk:${key}`,
|
|
68
|
-
detail: `Circular foreign-key dependency detected: ${displayPath}. This cycle can cause unexpected behavior during DELETE operations.`,
|
|
69
|
-
metadata: {
|
|
70
|
-
cycle: canonical,
|
|
71
|
-
path: displayPath,
|
|
72
|
-
tables: canonical
|
|
73
|
-
}
|
|
74
|
-
})
|
|
75
|
-
);
|
|
76
|
-
if (reported.size >= MAX_CYCLES) {
|
|
77
|
-
capped = true;
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
const circuit = (vertex, start, startIndex) => {
|
|
81
|
-
let foundCycle = false;
|
|
82
|
-
stack.push(vertex);
|
|
83
|
-
blocked.add(vertex);
|
|
84
|
-
for (const neighbor of adjacency.get(vertex) ?? []) {
|
|
85
|
-
if (capped) {
|
|
86
|
-
break;
|
|
87
|
-
}
|
|
88
|
-
if (indexOf.get(neighbor) < startIndex) {
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
if (neighbor === start) {
|
|
92
|
-
emitCurrentCycle();
|
|
93
|
-
foundCycle = true;
|
|
94
|
-
} else if (!blocked.has(neighbor) && circuit(neighbor, start, startIndex)) {
|
|
95
|
-
foundCycle = true;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
if (foundCycle) {
|
|
99
|
-
unblock(vertex);
|
|
100
|
-
} else {
|
|
101
|
-
for (const neighbor of adjacency.get(vertex) ?? []) {
|
|
102
|
-
if (indexOf.get(neighbor) < startIndex) {
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
let dependents = blockMap.get(neighbor);
|
|
106
|
-
if (dependents === void 0) {
|
|
107
|
-
dependents = /* @__PURE__ */ new Set();
|
|
108
|
-
blockMap.set(neighbor, dependents);
|
|
109
|
-
}
|
|
110
|
-
dependents.add(vertex);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
stack.pop();
|
|
114
|
-
return foundCycle;
|
|
115
|
-
};
|
|
116
|
-
for (let startIndex = 0; startIndex < order.length && !capped; startIndex += 1) {
|
|
117
|
-
blocked.clear();
|
|
118
|
-
blockMap.clear();
|
|
119
|
-
circuit(order[startIndex], order[startIndex], startIndex);
|
|
120
|
-
}
|
|
121
|
-
return findings;
|
|
122
|
-
},
|
|
123
|
-
source: "static",
|
|
124
|
-
title: "Circular foreign-key dependency"
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
export { circularFk as default };
|