@qodfy/core 0.2.6 → 0.2.8
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.ts +1 -0
- package/dist/index.js +850 -173
- package/package.json +3 -2
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -354,18 +354,10 @@ async function scanProject(input) {
|
|
|
354
354
|
relativeFile,
|
|
355
355
|
content
|
|
356
356
|
}) : null;
|
|
357
|
-
if (runWebhookChecks && apiRouteAnalysis
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
severity: apiRouteAnalysis.confidence === "high" ? "critical" : "warning",
|
|
362
|
-
confidence: apiRouteAnalysis.confidence,
|
|
363
|
-
title: "Webhook route may be missing signature verification",
|
|
364
|
-
message: "This webhook route appears to handle external events, but Qodfy could not find signature verification before the event is handled.",
|
|
365
|
-
file: relativeFile,
|
|
366
|
-
suggestion: getWebhookSignatureSuggestion(apiRouteAnalysis.webhookProvider),
|
|
367
|
-
fixPrompt: createWebhookSignatureFixPrompt(relativeFile),
|
|
368
|
-
evidence: apiRouteAnalysis.evidence
|
|
357
|
+
if (runWebhookChecks && apiRouteAnalysis) {
|
|
358
|
+
addWebhookSignatureIssues({
|
|
359
|
+
addIssue,
|
|
360
|
+
analysis: apiRouteAnalysis
|
|
369
361
|
});
|
|
370
362
|
}
|
|
371
363
|
if (runSecurityChecks) {
|
|
@@ -726,95 +718,125 @@ function isApiRoute(filePath) {
|
|
|
726
718
|
const sourceFileExtension = "(?:ts|tsx|js|jsx|mts|cts|mjs|cjs)";
|
|
727
719
|
return new RegExp(`/app/api(?:/.+)?/route\\.${sourceFileExtension}$`).test(normalizedFile) || new RegExp(`/pages/api/.+\\.${sourceFileExtension}$`).test(normalizedFile);
|
|
728
720
|
}
|
|
721
|
+
function addWebhookSignatureIssues({
|
|
722
|
+
addIssue,
|
|
723
|
+
analysis
|
|
724
|
+
}) {
|
|
725
|
+
for (const handler of analysis.handlers) {
|
|
726
|
+
if (handler.intent !== "webhook" || handler.hasWebhookVerification) {
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
addIssue({
|
|
730
|
+
ruleId: "webhook-missing-signature-verification",
|
|
731
|
+
category: "webhook",
|
|
732
|
+
severity: handler.confidence === "high" ? "critical" : "warning",
|
|
733
|
+
confidence: handler.confidence,
|
|
734
|
+
title: `Webhook ${handler.method} handler may be missing signature verification`,
|
|
735
|
+
message: `The ${handler.method} handler in this route appears to handle external events, but Qodfy could not find signature verification before the event is handled.`,
|
|
736
|
+
file: analysis.relativeFile,
|
|
737
|
+
suggestion: getWebhookSignatureSuggestion(analysis.webhookProvider),
|
|
738
|
+
fixPrompt: createWebhookSignatureFixPrompt(analysis.relativeFile, handler.method),
|
|
739
|
+
evidence: handler.evidence,
|
|
740
|
+
context: handler.context
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
}
|
|
729
744
|
function addApiRouteProtectionIssues({
|
|
730
745
|
addIssue,
|
|
731
746
|
includeLowConfidence,
|
|
732
747
|
analysis
|
|
733
748
|
}) {
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
if (analysis.intent === "public-read") {
|
|
738
|
-
if (includeLowConfidence) {
|
|
739
|
-
addIssue({
|
|
740
|
-
ruleId: "api-public-read-route",
|
|
741
|
-
category: "api",
|
|
742
|
-
severity: "info",
|
|
743
|
-
confidence: analysis.confidence,
|
|
744
|
-
title: "Public read API route detected",
|
|
745
|
-
message: "This route appears intentionally public. Authentication may not be required.",
|
|
746
|
-
file: analysis.relativeFile,
|
|
747
|
-
suggestion: "Verify that it only exposes public or published data and has appropriate validation, caching, and abuse protection.",
|
|
748
|
-
fixPrompt: createPublicReadRouteFixPrompt(analysis.relativeFile),
|
|
749
|
-
evidence: analysis.evidence
|
|
750
|
-
});
|
|
749
|
+
for (const handler of analysis.handlers) {
|
|
750
|
+
if (handler.intent === "webhook") {
|
|
751
|
+
continue;
|
|
751
752
|
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
}
|
|
753
|
+
if (handler.intent === "public-read") {
|
|
754
|
+
if (includeLowConfidence) {
|
|
755
|
+
addIssue({
|
|
756
|
+
ruleId: "api-public-read-route",
|
|
757
|
+
category: "api",
|
|
758
|
+
severity: "info",
|
|
759
|
+
confidence: handler.confidence,
|
|
760
|
+
title: `Public ${handler.method} handler detected`,
|
|
761
|
+
message: `The ${handler.method} handler in this route appears intentionally public. Authentication may not be required.`,
|
|
762
|
+
file: analysis.relativeFile,
|
|
763
|
+
suggestion: "Verify that it only exposes public or published data and has appropriate validation, caching, and abuse protection.",
|
|
764
|
+
fixPrompt: createPublicReadRouteFixPrompt(analysis.relativeFile, handler.method),
|
|
765
|
+
evidence: handler.evidence,
|
|
766
|
+
context: handler.context
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
continue;
|
|
768
770
|
}
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
}
|
|
771
|
+
if (handler.intent === "public-form") {
|
|
772
|
+
if (!handler.hasRateLimit && !handler.hasValidation) {
|
|
773
|
+
addIssue({
|
|
774
|
+
ruleId: "public-form-missing-abuse-protection",
|
|
775
|
+
category: "api",
|
|
776
|
+
severity: "warning",
|
|
777
|
+
confidence: handler.confidence,
|
|
778
|
+
title: `Public form ${handler.method} handler may be missing abuse protection`,
|
|
779
|
+
message: `The ${handler.method} handler in this route appears to accept public submissions. Consider adding rate limiting, validation, or spam protection.`,
|
|
780
|
+
file: analysis.relativeFile,
|
|
781
|
+
suggestion: "Check for rate limiting, validation, captcha, Turnstile, reCAPTCHA, hCaptcha, or another spam protection pattern.",
|
|
782
|
+
fixPrompt: createPublicFormProtectionFixPrompt(analysis.relativeFile, handler.method),
|
|
783
|
+
evidence: handler.evidence,
|
|
784
|
+
context: handler.context
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
continue;
|
|
785
788
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
789
|
+
if (handler.intent === "internal") {
|
|
790
|
+
if (!handler.hasAuth && !handler.hasSecretProtection) {
|
|
791
|
+
addIssue({
|
|
792
|
+
ruleId: "internal-route-missing-protection",
|
|
793
|
+
category: "security",
|
|
794
|
+
severity: "warning",
|
|
795
|
+
confidence: handler.confidence,
|
|
796
|
+
title: `Internal ${handler.method} handler may be missing protection`,
|
|
797
|
+
message: `The ${handler.method} handler in this route appears internal or operational. Confirm it is protected by auth, a secret token, or server-only access.`,
|
|
798
|
+
file: analysis.relativeFile,
|
|
799
|
+
suggestion: "Use the project's existing auth pattern or a secret token check for operational handlers such as cron, cleanup, or revalidation.",
|
|
800
|
+
fixPrompt: createInternalRouteProtectionFixPrompt(analysis.relativeFile, handler.method),
|
|
801
|
+
evidence: handler.evidence,
|
|
802
|
+
context: handler.context
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
if (handler.intent === "sensitive-mutation") {
|
|
808
|
+
if (!handler.hasAuth) {
|
|
809
|
+
addIssue({
|
|
810
|
+
ruleId: "sensitive-api-route-missing-auth",
|
|
811
|
+
category: "security",
|
|
812
|
+
severity: "warning",
|
|
813
|
+
confidence: handler.confidence,
|
|
814
|
+
title: getSensitiveHandlerTitle(handler),
|
|
815
|
+
message: `The ${handler.method} handler in this route appears to handle uploads or sensitive operations. Confirm it is protected before launch.`,
|
|
816
|
+
file: analysis.relativeFile,
|
|
817
|
+
suggestion: "Review the existing project auth/session pattern and apply it if this handler processes private data, uploads, payments, or account changes.",
|
|
818
|
+
fixPrompt: createApiAuthFixPrompt(analysis.relativeFile, handler.method, handler.intent),
|
|
819
|
+
evidence: handler.evidence,
|
|
820
|
+
context: handler.context
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
if (handler.authExpected === "review" && !handler.hasAuth) {
|
|
790
826
|
addIssue({
|
|
791
|
-
ruleId: "
|
|
792
|
-
category: "
|
|
827
|
+
ruleId: "api-mutation-route-review-auth",
|
|
828
|
+
category: "api",
|
|
793
829
|
severity: "warning",
|
|
794
|
-
confidence:
|
|
795
|
-
title: "
|
|
796
|
-
message:
|
|
830
|
+
confidence: handler.confidence,
|
|
831
|
+
title: "API mutation handler should be reviewed for authentication",
|
|
832
|
+
message: `The ${handler.method} handler mutates data or handles requests, but Qodfy could not determine whether authentication is required.`,
|
|
797
833
|
file: analysis.relativeFile,
|
|
798
|
-
suggestion: "
|
|
799
|
-
fixPrompt: createApiAuthFixPrompt(analysis.relativeFile),
|
|
800
|
-
evidence:
|
|
834
|
+
suggestion: "Confirm the handler is intentionally public, or add the existing project auth/session check before handling private data.",
|
|
835
|
+
fixPrompt: createApiAuthFixPrompt(analysis.relativeFile, handler.method, handler.intent),
|
|
836
|
+
evidence: handler.evidence,
|
|
837
|
+
context: handler.context
|
|
801
838
|
});
|
|
802
839
|
}
|
|
803
|
-
return;
|
|
804
|
-
}
|
|
805
|
-
if (analysis.authExpected === "review" && !analysis.hasAuth) {
|
|
806
|
-
addIssue({
|
|
807
|
-
ruleId: "api-mutation-route-review-auth",
|
|
808
|
-
category: "api",
|
|
809
|
-
severity: "warning",
|
|
810
|
-
confidence: analysis.confidence,
|
|
811
|
-
title: "API mutation route should be reviewed for authentication",
|
|
812
|
-
message: "This route mutates data or handles requests, but Qodfy could not determine whether authentication is required.",
|
|
813
|
-
file: analysis.relativeFile,
|
|
814
|
-
suggestion: "Confirm the route is intentionally public, or add the existing project auth/session check before handling private data.",
|
|
815
|
-
fixPrompt: createApiAuthFixPrompt(analysis.relativeFile),
|
|
816
|
-
evidence: analysis.evidence
|
|
817
|
-
});
|
|
818
840
|
}
|
|
819
841
|
}
|
|
820
842
|
function analyzeApiRoute({
|
|
@@ -822,25 +844,44 @@ function analyzeApiRoute({
|
|
|
822
844
|
relativeFile,
|
|
823
845
|
content
|
|
824
846
|
}) {
|
|
825
|
-
const
|
|
826
|
-
const
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
const
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
evidence.push({ label: "no exported HTTP method detected" });
|
|
847
|
+
const exportedHandlers = getExportedRouteHandlers(content);
|
|
848
|
+
const handlersToAnalyze = exportedHandlers.length > 0 ? exportedHandlers : getRouteHttpMethods(content).map((method) => ({
|
|
849
|
+
method,
|
|
850
|
+
body: content,
|
|
851
|
+
usedFallbackBody: true
|
|
852
|
+
}));
|
|
853
|
+
const handlers = handlersToAnalyze.map(
|
|
854
|
+
(handler) => analyzeApiHandler({
|
|
855
|
+
relativeFile,
|
|
856
|
+
content,
|
|
857
|
+
method: handler.method,
|
|
858
|
+
body: handler.body,
|
|
859
|
+
usedFallbackBody: handler.usedFallbackBody
|
|
860
|
+
})
|
|
861
|
+
);
|
|
862
|
+
for (const handler of handlers) {
|
|
863
|
+
handler.context = getHandlerContext(handler, handlers);
|
|
843
864
|
}
|
|
865
|
+
const methods = handlers.map((handler) => handler.method);
|
|
866
|
+
const webhookProvider = getRouteWebhookProvider(handlers, relativeFile, content);
|
|
867
|
+
return {
|
|
868
|
+
file,
|
|
869
|
+
relativeFile,
|
|
870
|
+
methods,
|
|
871
|
+
handlers,
|
|
872
|
+
routeIntent: getRouteIntent(handlers),
|
|
873
|
+
evidence: methods.map((method) => ({ label: "exports", detail: method })),
|
|
874
|
+
webhookProvider
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
function analyzeApiHandler({
|
|
878
|
+
relativeFile,
|
|
879
|
+
content,
|
|
880
|
+
method,
|
|
881
|
+
body,
|
|
882
|
+
usedFallbackBody
|
|
883
|
+
}) {
|
|
884
|
+
const normalizedFile = relativeFile.toLowerCase();
|
|
844
885
|
const webhookPathMatch = getRoutePathMatch(normalizedFile, ["webhook", "webhooks", "callback"]);
|
|
845
886
|
const internalPathMatch = getRoutePathMatch(normalizedFile, ["internal", "admin", "cron", "cleanup", "revalidate", "private"]);
|
|
846
887
|
const formPathMatch = getRoutePathMatch(normalizedFile, ["contact", "subscribe", "newsletter", "lead", "inquiry"]);
|
|
@@ -874,7 +915,35 @@ function analyzeApiRoute({
|
|
|
874
915
|
"sitemap",
|
|
875
916
|
"rss"
|
|
876
917
|
]);
|
|
918
|
+
const handlerContent = body || content;
|
|
919
|
+
const webhookRouteInfo = getWebhookRouteInfo(relativeFile, handlerContent);
|
|
920
|
+
const webhookProvider = webhookRouteInfo?.provider ?? getWebhookProvider(`${normalizedFile}
|
|
921
|
+
${handlerContent.toLowerCase()}`);
|
|
922
|
+
const protectionAnalysis = analyzeHandlerProtection({
|
|
923
|
+
handlerBody: handlerContent,
|
|
924
|
+
fullFileContent: content
|
|
925
|
+
});
|
|
926
|
+
const hasWeakAuthSignal = hasWeakAuthRelatedSignal(handlerContent);
|
|
927
|
+
const hasStrongProtection = hasStrongProtectionCallBeforeSensitiveWork(
|
|
928
|
+
handlerContent,
|
|
929
|
+
protectionAnalysis.sensitiveOperation
|
|
930
|
+
);
|
|
931
|
+
const hasAuth = protectionAnalysis.hasAccessControlGuard || hasStrongProtection;
|
|
932
|
+
const hasSecretProtection = hasSecretProtectionGuardBeforeSensitiveWork(
|
|
933
|
+
handlerContent,
|
|
934
|
+
protectionAnalysis.sensitiveOperation
|
|
935
|
+
);
|
|
936
|
+
const hasWeakSecretSignal = hasWeakSecretProtectionSignal(handlerContent);
|
|
937
|
+
const hasRateLimit = hasRateLimitSignal(handlerContent);
|
|
938
|
+
const hasValidation = hasValidationSignal(handlerContent);
|
|
939
|
+
const hasCacheHeaders = hasCacheHeaderSignal(handlerContent);
|
|
940
|
+
const hasMethodBlocking = hasMethodBlockingSignal(handlerContent);
|
|
941
|
+
const hasWebhookVerification = hasWebhookSignatureVerification(handlerContent, webhookProvider);
|
|
942
|
+
const evidence = [{ label: "exports", detail: method }];
|
|
877
943
|
let intent = "unknown";
|
|
944
|
+
if (usedFallbackBody) {
|
|
945
|
+
evidence.push({ label: "handler body extraction fallback", detail: "using full file" });
|
|
946
|
+
}
|
|
878
947
|
if (webhookRouteInfo || webhookPathMatch) {
|
|
879
948
|
intent = "webhook";
|
|
880
949
|
evidence.push({
|
|
@@ -884,54 +953,79 @@ function analyzeApiRoute({
|
|
|
884
953
|
} else if (internalPathMatch) {
|
|
885
954
|
intent = "internal";
|
|
886
955
|
evidence.push({ label: "path contains", detail: internalPathMatch });
|
|
887
|
-
} else if (formPathMatch) {
|
|
956
|
+
} else if (method === "POST" && formPathMatch) {
|
|
888
957
|
intent = "public-form";
|
|
889
958
|
evidence.push({ label: "path contains", detail: formPathMatch });
|
|
890
|
-
} else if (
|
|
959
|
+
} else if (method === "GET" && publicContentPathMatch && !sensitivePathMatch && !internalPathMatch) {
|
|
960
|
+
intent = "public-read";
|
|
961
|
+
evidence.push({ label: "public read path detected", detail: publicContentPathMatch });
|
|
962
|
+
} else if (isMutationMethod(method) && sensitivePathMatch && !hasMethodBlocking) {
|
|
891
963
|
intent = "sensitive-mutation";
|
|
892
964
|
evidence.push({ label: "path contains", detail: sensitivePathMatch });
|
|
893
|
-
} else if (methods.includes("GET") && publicContentPathMatch && !sensitivePathMatch && !internalPathMatch) {
|
|
894
|
-
intent = "public-read";
|
|
895
|
-
evidence.push({ label: "public content route detected", detail: publicContentPathMatch });
|
|
896
965
|
}
|
|
897
|
-
if (
|
|
898
|
-
evidence.push({
|
|
966
|
+
if (protectionAnalysis.sensitiveOperation) {
|
|
967
|
+
evidence.push({
|
|
968
|
+
label: `sensitive operation ${protectionAnalysis.sensitiveOperation.label} detected`,
|
|
969
|
+
detail: `${method} handler`
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
if (protectionAnalysis.hasAccessControlGuard) {
|
|
973
|
+
evidence.push(...protectionAnalysis.evidence);
|
|
974
|
+
} else if (hasStrongProtection) {
|
|
975
|
+
evidence.push({
|
|
976
|
+
label: "strong protection call detected before sensitive work",
|
|
977
|
+
detail: `${method} handler`
|
|
978
|
+
});
|
|
979
|
+
} else if (hasWeakAuthSignal) {
|
|
980
|
+
evidence.push({
|
|
981
|
+
label: "possible auth-related signal detected",
|
|
982
|
+
detail: `${method} handler`
|
|
983
|
+
});
|
|
984
|
+
} else if (protectionAnalysis.sensitiveOperation) {
|
|
985
|
+
evidence.push({ label: `no access-control guard detected before sensitive operation`, detail: `${method} handler` });
|
|
899
986
|
} else {
|
|
900
|
-
evidence.push({ label:
|
|
987
|
+
evidence.push({ label: `no auth/session check detected in ${method} handler` });
|
|
901
988
|
}
|
|
902
989
|
if (hasSecretProtection) {
|
|
903
|
-
evidence.push({ label:
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
evidence.push({ label: "rate limit detected" });
|
|
907
|
-
} else if (intent === "public-form") {
|
|
908
|
-
evidence.push({ label: "no rate limit detected" });
|
|
990
|
+
evidence.push({ label: `secret-token guard detected before sensitive work`, detail: `${method} handler` });
|
|
991
|
+
} else if (hasWeakSecretSignal) {
|
|
992
|
+
evidence.push({ label: `possible secret/token signal detected`, detail: `${method} handler` });
|
|
909
993
|
}
|
|
910
|
-
if (
|
|
911
|
-
evidence.push({
|
|
912
|
-
|
|
913
|
-
|
|
994
|
+
if (intent === "public-form") {
|
|
995
|
+
evidence.push({
|
|
996
|
+
label: hasRateLimit ? "rate limit detected" : "no rate limit detected",
|
|
997
|
+
detail: `${method} handler`
|
|
998
|
+
});
|
|
999
|
+
evidence.push({
|
|
1000
|
+
label: hasValidation ? "validation detected" : "no validation detected",
|
|
1001
|
+
detail: `${method} handler`
|
|
1002
|
+
});
|
|
914
1003
|
}
|
|
915
|
-
if (
|
|
916
|
-
|
|
1004
|
+
if (intent === "public-read") {
|
|
1005
|
+
if (hasRateLimit) {
|
|
1006
|
+
evidence.push({ label: "rate limit detected", detail: `${method} handler` });
|
|
1007
|
+
}
|
|
1008
|
+
if (hasValidation) {
|
|
1009
|
+
evidence.push({ label: "validation detected", detail: `${method} handler` });
|
|
1010
|
+
}
|
|
1011
|
+
if (hasCacheHeaders) {
|
|
1012
|
+
evidence.push({ label: "cache/public-read safety signal detected", detail: `${method} handler` });
|
|
1013
|
+
}
|
|
917
1014
|
}
|
|
918
1015
|
if (hasMethodBlocking) {
|
|
919
|
-
evidence.push({ label: "method blocking detected" });
|
|
1016
|
+
evidence.push({ label: "method blocking detected", detail: `${method} handler` });
|
|
920
1017
|
}
|
|
921
1018
|
if (intent === "webhook") {
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
}
|
|
1019
|
+
evidence.push({
|
|
1020
|
+
label: hasWebhookVerification ? "webhook signature verification detected" : "no webhook signature verification detected",
|
|
1021
|
+
detail: `${method} handler`
|
|
1022
|
+
});
|
|
927
1023
|
}
|
|
928
1024
|
return {
|
|
929
|
-
|
|
930
|
-
relativeFile,
|
|
931
|
-
methods,
|
|
1025
|
+
method,
|
|
932
1026
|
intent,
|
|
933
|
-
authExpected:
|
|
934
|
-
confidence:
|
|
1027
|
+
authExpected: getHandlerAuthExpectation(intent, method, hasMethodBlocking),
|
|
1028
|
+
confidence: getApiHandlerConfidence(intent, method, webhookRouteInfo, hasMethodBlocking),
|
|
935
1029
|
evidence,
|
|
936
1030
|
hasAuth,
|
|
937
1031
|
hasSecretProtection,
|
|
@@ -939,31 +1033,89 @@ function analyzeApiRoute({
|
|
|
939
1033
|
hasValidation,
|
|
940
1034
|
hasCacheHeaders,
|
|
941
1035
|
hasMethodBlocking,
|
|
942
|
-
hasWebhookVerification
|
|
943
|
-
webhookProvider
|
|
1036
|
+
hasWebhookVerification
|
|
944
1037
|
};
|
|
945
1038
|
}
|
|
946
|
-
function
|
|
1039
|
+
function getHandlerContext(handler, handlers) {
|
|
1040
|
+
const context = [];
|
|
1041
|
+
for (const otherHandler of handlers) {
|
|
1042
|
+
if (otherHandler.method === handler.method) {
|
|
1043
|
+
continue;
|
|
1044
|
+
}
|
|
1045
|
+
context.push({ label: "route also exports", detail: otherHandler.method });
|
|
1046
|
+
if (otherHandler.intent === "public-read") {
|
|
1047
|
+
context.push({ label: "public read handler detected", detail: otherHandler.method });
|
|
1048
|
+
}
|
|
1049
|
+
if (otherHandler.hasCacheHeaders) {
|
|
1050
|
+
context.push({ label: "cache/public-read safety signal detected", detail: `${otherHandler.method} handler` });
|
|
1051
|
+
}
|
|
1052
|
+
if (otherHandler.hasMethodBlocking) {
|
|
1053
|
+
context.push({ label: "method blocking detected", detail: `${otherHandler.method} handler` });
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return context.length > 0 ? context : void 0;
|
|
1057
|
+
}
|
|
1058
|
+
function getRouteIntent(handlers) {
|
|
1059
|
+
const intentPriority = [
|
|
1060
|
+
"webhook",
|
|
1061
|
+
"internal",
|
|
1062
|
+
"sensitive-mutation",
|
|
1063
|
+
"public-form",
|
|
1064
|
+
"public-read",
|
|
1065
|
+
"unknown"
|
|
1066
|
+
];
|
|
1067
|
+
return intentPriority.find(
|
|
1068
|
+
(intent) => handlers.some((handler) => handler.intent === intent)
|
|
1069
|
+
) ?? "unknown";
|
|
1070
|
+
}
|
|
1071
|
+
function getRouteWebhookProvider(handlers, relativeFile, content) {
|
|
1072
|
+
const routeWebhookInfo = getWebhookRouteInfo(relativeFile, content);
|
|
1073
|
+
if (routeWebhookInfo?.provider && routeWebhookInfo.provider !== "unknown") {
|
|
1074
|
+
return routeWebhookInfo.provider;
|
|
1075
|
+
}
|
|
1076
|
+
const webhookHandler = handlers.find((handler) => handler.intent === "webhook");
|
|
1077
|
+
if (!webhookHandler) {
|
|
1078
|
+
return "unknown";
|
|
1079
|
+
}
|
|
1080
|
+
const providerFromEvidence = webhookHandler.evidence.find(
|
|
1081
|
+
(item) => item.label === "webhook content detected" && item.detail && item.detail !== "unknown"
|
|
1082
|
+
);
|
|
1083
|
+
return providerFromEvidence?.detail ?? "unknown";
|
|
1084
|
+
}
|
|
1085
|
+
function getHandlerAuthExpectation(intent, method, hasMethodBlocking) {
|
|
1086
|
+
if (hasMethodBlocking || intent === "public-read" || intent === "public-form" || intent === "webhook") {
|
|
1087
|
+
return false;
|
|
1088
|
+
}
|
|
947
1089
|
if (intent === "internal" || intent === "sensitive-mutation") {
|
|
948
1090
|
return true;
|
|
949
1091
|
}
|
|
950
|
-
if (
|
|
1092
|
+
if (isMutationMethod(method)) {
|
|
951
1093
|
return "review";
|
|
952
1094
|
}
|
|
953
1095
|
return false;
|
|
954
1096
|
}
|
|
955
|
-
function
|
|
1097
|
+
function getApiHandlerConfidence(intent, method, webhookRouteInfo, hasMethodBlocking) {
|
|
1098
|
+
if (hasMethodBlocking) {
|
|
1099
|
+
return "low";
|
|
1100
|
+
}
|
|
956
1101
|
if (intent === "sensitive-mutation" || intent === "internal") {
|
|
957
1102
|
return "high";
|
|
958
1103
|
}
|
|
959
1104
|
if (intent === "webhook") {
|
|
960
1105
|
return webhookRouteInfo?.confidence === "high" ? "high" : "medium";
|
|
961
1106
|
}
|
|
962
|
-
if (intent === "public-form" || intent === "unknown" &&
|
|
1107
|
+
if (intent === "public-form" || intent === "unknown" && isMutationMethod(method)) {
|
|
963
1108
|
return "medium";
|
|
964
1109
|
}
|
|
965
1110
|
return "low";
|
|
966
1111
|
}
|
|
1112
|
+
function getSensitiveHandlerTitle(handler) {
|
|
1113
|
+
const pathSignal = handler.evidence.find((item) => item.label === "path contains")?.detail;
|
|
1114
|
+
if (handler.method === "POST" && pathSignal === "upload") {
|
|
1115
|
+
return "Upload POST handler may be missing authentication";
|
|
1116
|
+
}
|
|
1117
|
+
return `Sensitive ${handler.method} handler may be missing authentication`;
|
|
1118
|
+
}
|
|
967
1119
|
function getRoutePathMatch(normalizedFile, terms) {
|
|
968
1120
|
return terms.find((term) => {
|
|
969
1121
|
const escapedTerm = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -971,16 +1123,123 @@ function getRoutePathMatch(normalizedFile, terms) {
|
|
|
971
1123
|
});
|
|
972
1124
|
}
|
|
973
1125
|
function getExportedHttpMethods(content) {
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1126
|
+
return getExportedRouteHandlers(content).map((handler) => handler.method);
|
|
1127
|
+
}
|
|
1128
|
+
function getExportedRouteHandlers(content) {
|
|
1129
|
+
const handlers = [];
|
|
1130
|
+
const functionExportPattern = /\bexport\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE)\s*\(/g;
|
|
1131
|
+
const constExportPattern = /\bexport\s+const\s+(GET|POST|PUT|PATCH|DELETE)\s*=/g;
|
|
977
1132
|
for (const match of content.matchAll(functionExportPattern)) {
|
|
978
|
-
|
|
1133
|
+
handlers.push(extractRouteHandlerBody(content, match.index ?? 0, match[1], "function"));
|
|
979
1134
|
}
|
|
980
1135
|
for (const match of content.matchAll(constExportPattern)) {
|
|
981
|
-
|
|
1136
|
+
handlers.push(extractRouteHandlerBody(content, match.index ?? 0, match[1], "const"));
|
|
982
1137
|
}
|
|
983
|
-
return
|
|
1138
|
+
return handlers.sort(
|
|
1139
|
+
(leftHandler, rightHandler) => getMethodRank(leftHandler.method) - getMethodRank(rightHandler.method)
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
function extractRouteHandlerBody(content, exportIndex, method, exportKind) {
|
|
1143
|
+
const nextExportIndex = findNextRouteHandlerExport(content, exportIndex + 1);
|
|
1144
|
+
const handlerEnd = nextExportIndex === -1 ? content.length : nextExportIndex;
|
|
1145
|
+
const openBraceIndex = getRouteHandlerBodyOpenBrace(content, exportIndex, handlerEnd, exportKind);
|
|
1146
|
+
if (openBraceIndex !== -1 && openBraceIndex < handlerEnd) {
|
|
1147
|
+
const closeBraceIndex = findMatchingBrace(content, openBraceIndex);
|
|
1148
|
+
if (closeBraceIndex !== -1) {
|
|
1149
|
+
return {
|
|
1150
|
+
method,
|
|
1151
|
+
body: content.slice(openBraceIndex, closeBraceIndex + 1),
|
|
1152
|
+
usedFallbackBody: false
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
return {
|
|
1157
|
+
method,
|
|
1158
|
+
body: content.slice(exportIndex, handlerEnd),
|
|
1159
|
+
usedFallbackBody: true
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
function getRouteHandlerBodyOpenBrace(content, exportIndex, handlerEnd, exportKind) {
|
|
1163
|
+
if (exportKind === "function") {
|
|
1164
|
+
const openParenIndex = content.indexOf("(", exportIndex);
|
|
1165
|
+
if (openParenIndex !== -1 && openParenIndex < handlerEnd) {
|
|
1166
|
+
const closeParenIndex = findMatchingParen(content, openParenIndex);
|
|
1167
|
+
if (closeParenIndex !== -1 && closeParenIndex < handlerEnd) {
|
|
1168
|
+
return content.indexOf("{", closeParenIndex);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
const equalsIndex = content.indexOf("=", exportIndex);
|
|
1173
|
+
if (equalsIndex !== -1 && equalsIndex < handlerEnd) {
|
|
1174
|
+
const arrowIndex = content.indexOf("=>", equalsIndex);
|
|
1175
|
+
if (arrowIndex !== -1 && arrowIndex < handlerEnd) {
|
|
1176
|
+
return content.indexOf("{", arrowIndex);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return content.indexOf("{", exportIndex);
|
|
1180
|
+
}
|
|
1181
|
+
function findNextRouteHandlerExport(content, startIndex) {
|
|
1182
|
+
const nextExportPattern = /\bexport\s+(?:async\s+)?(?:function|const)\s+(GET|POST|PUT|PATCH|DELETE)\b/g;
|
|
1183
|
+
nextExportPattern.lastIndex = startIndex;
|
|
1184
|
+
const match = nextExportPattern.exec(content);
|
|
1185
|
+
return match?.index ?? -1;
|
|
1186
|
+
}
|
|
1187
|
+
function findMatchingParen(content, openParenIndex) {
|
|
1188
|
+
let depth = 0;
|
|
1189
|
+
for (let index = openParenIndex; index < content.length; index++) {
|
|
1190
|
+
const character = content[index];
|
|
1191
|
+
if (character === "(") {
|
|
1192
|
+
depth++;
|
|
1193
|
+
} else if (character === ")") {
|
|
1194
|
+
depth--;
|
|
1195
|
+
if (depth === 0) {
|
|
1196
|
+
return index;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
return -1;
|
|
1201
|
+
}
|
|
1202
|
+
function getIfStatements(content, startIndex = 0, endIndex = content.length) {
|
|
1203
|
+
const statements = [];
|
|
1204
|
+
const ifPattern = /\bif\s*\(/g;
|
|
1205
|
+
ifPattern.lastIndex = startIndex;
|
|
1206
|
+
let match;
|
|
1207
|
+
while ((match = ifPattern.exec(content)) !== null) {
|
|
1208
|
+
const matchIndex = match.index;
|
|
1209
|
+
if (matchIndex > endIndex) {
|
|
1210
|
+
break;
|
|
1211
|
+
}
|
|
1212
|
+
const openParenIndex = content.indexOf("(", matchIndex);
|
|
1213
|
+
if (openParenIndex === -1 || openParenIndex > endIndex) {
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1216
|
+
const closeParenIndex = findMatchingParen(content, openParenIndex);
|
|
1217
|
+
if (closeParenIndex === -1 || closeParenIndex > endIndex) {
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
statements.push({
|
|
1221
|
+
index: matchIndex,
|
|
1222
|
+
condition: content.slice(openParenIndex + 1, closeParenIndex),
|
|
1223
|
+
branchStartIndex: closeParenIndex + 1
|
|
1224
|
+
});
|
|
1225
|
+
ifPattern.lastIndex = closeParenIndex + 1;
|
|
1226
|
+
}
|
|
1227
|
+
return statements;
|
|
1228
|
+
}
|
|
1229
|
+
function findMatchingBrace(content, openBraceIndex) {
|
|
1230
|
+
let depth = 0;
|
|
1231
|
+
for (let index = openBraceIndex; index < content.length; index++) {
|
|
1232
|
+
const character = content[index];
|
|
1233
|
+
if (character === "{") {
|
|
1234
|
+
depth++;
|
|
1235
|
+
} else if (character === "}") {
|
|
1236
|
+
depth--;
|
|
1237
|
+
if (depth === 0) {
|
|
1238
|
+
return index;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
return -1;
|
|
984
1243
|
}
|
|
985
1244
|
function getRouteHttpMethods(content) {
|
|
986
1245
|
const methods = new Set(getExportedHttpMethods(content));
|
|
@@ -994,19 +1253,372 @@ function getRouteHttpMethods(content) {
|
|
|
994
1253
|
}
|
|
995
1254
|
return [...methods];
|
|
996
1255
|
}
|
|
997
|
-
function
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
);
|
|
1256
|
+
function getMethodRank(method) {
|
|
1257
|
+
const methodOrder = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
1258
|
+
return methodOrder.indexOf(method);
|
|
1001
1259
|
}
|
|
1002
|
-
function
|
|
1003
|
-
|
|
1004
|
-
|
|
1260
|
+
function isMutationMethod(method) {
|
|
1261
|
+
return method !== "GET";
|
|
1262
|
+
}
|
|
1263
|
+
function analyzeHandlerProtection({
|
|
1264
|
+
handlerBody,
|
|
1265
|
+
fullFileContent
|
|
1266
|
+
}) {
|
|
1267
|
+
const imports = getImportInfos(fullFileContent);
|
|
1268
|
+
const sensitiveOperation = getFirstSensitiveOperation(handlerBody);
|
|
1269
|
+
const helperAssignments = getHelperAssignments(handlerBody);
|
|
1270
|
+
for (const assignment of helperAssignments) {
|
|
1271
|
+
if (isRawRequestAccessorHelper(assignment.helperName)) {
|
|
1272
|
+
continue;
|
|
1273
|
+
}
|
|
1274
|
+
if (!isGuardHelperAssignmentNearTop(assignment.index, handlerBody, sensitiveOperation)) {
|
|
1275
|
+
continue;
|
|
1276
|
+
}
|
|
1277
|
+
const guard = findAccessControlGuardForVariable({
|
|
1278
|
+
handlerBody,
|
|
1279
|
+
variableName: assignment.variableName,
|
|
1280
|
+
startIndex: assignment.endIndex
|
|
1281
|
+
});
|
|
1282
|
+
if (!guard) {
|
|
1283
|
+
continue;
|
|
1284
|
+
}
|
|
1285
|
+
if (sensitiveOperation && guard.endIndex > sensitiveOperation.index) {
|
|
1286
|
+
continue;
|
|
1287
|
+
}
|
|
1288
|
+
const importInfo = getImportInfoForHelper(imports, assignment.helperName);
|
|
1289
|
+
const evidence = [
|
|
1290
|
+
{ label: "access-control guard detected" },
|
|
1291
|
+
{ label: "helper call assigned to variable", detail: assignment.variableName },
|
|
1292
|
+
{ label: "guard checks variable", detail: assignment.variableName },
|
|
1293
|
+
{ label: guard.returnSignal }
|
|
1294
|
+
];
|
|
1295
|
+
if (sensitiveOperation) {
|
|
1296
|
+
evidence.push({ label: "guard appears before sensitive operation", detail: sensitiveOperation.label });
|
|
1297
|
+
}
|
|
1298
|
+
if (importInfo?.isProtectionSource) {
|
|
1299
|
+
evidence.push({ label: "helper imported from protection module", detail: importInfo.source });
|
|
1300
|
+
}
|
|
1301
|
+
return {
|
|
1302
|
+
hasAccessControlGuard: true,
|
|
1303
|
+
evidence,
|
|
1304
|
+
sensitiveOperation
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
return {
|
|
1308
|
+
hasAccessControlGuard: false,
|
|
1309
|
+
evidence: [],
|
|
1310
|
+
sensitiveOperation
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
function getImportInfos(content) {
|
|
1314
|
+
const imports = [];
|
|
1315
|
+
const importPattern = /import\s+(?:type\s+)?([\s\S]*?)\s+from\s+["']([^"']+)["'];?/g;
|
|
1316
|
+
for (const match of content.matchAll(importPattern)) {
|
|
1317
|
+
const importClause = match[1]?.trim();
|
|
1318
|
+
const source = match[2];
|
|
1319
|
+
if (!importClause || !source) {
|
|
1320
|
+
continue;
|
|
1321
|
+
}
|
|
1322
|
+
const isProtectionSource = isProtectionImportSource(source);
|
|
1323
|
+
const namedImportMatch = importClause.match(/\{([\s\S]*?)\}/);
|
|
1324
|
+
if (namedImportMatch?.[1]) {
|
|
1325
|
+
const namedImports = namedImportMatch[1].split(",");
|
|
1326
|
+
for (const namedImport of namedImports) {
|
|
1327
|
+
const parts = namedImport.trim().split(/\s+as\s+/i);
|
|
1328
|
+
const importedName = parts[0]?.trim();
|
|
1329
|
+
const localName = parts[1]?.trim() ?? importedName;
|
|
1330
|
+
if (importedName && localName) {
|
|
1331
|
+
imports.push({
|
|
1332
|
+
localName,
|
|
1333
|
+
importedName,
|
|
1334
|
+
source,
|
|
1335
|
+
isProtectionSource
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
const clauseBeforeNamedImports = importClause.split("{")[0]?.replace(/,\s*$/, "").trim();
|
|
1341
|
+
if (clauseBeforeNamedImports && !clauseBeforeNamedImports.startsWith("*")) {
|
|
1342
|
+
const defaultImport = clauseBeforeNamedImports.split(",")[0]?.trim();
|
|
1343
|
+
if (defaultImport && /^[A-Za-z_$][\w$]*$/.test(defaultImport)) {
|
|
1344
|
+
imports.push({
|
|
1345
|
+
localName: defaultImport,
|
|
1346
|
+
importedName: "default",
|
|
1347
|
+
source,
|
|
1348
|
+
isProtectionSource
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
const namespaceImportMatch = importClause.match(/\*\s+as\s+([A-Za-z_$][\w$]*)/);
|
|
1353
|
+
if (namespaceImportMatch?.[1]) {
|
|
1354
|
+
imports.push({
|
|
1355
|
+
localName: namespaceImportMatch[1],
|
|
1356
|
+
importedName: "*",
|
|
1357
|
+
source,
|
|
1358
|
+
isProtectionSource
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
return imports;
|
|
1363
|
+
}
|
|
1364
|
+
function isProtectionImportSource(source) {
|
|
1365
|
+
const normalizedSource = source.toLowerCase();
|
|
1366
|
+
const protectionSourceTerms = [
|
|
1367
|
+
"auth",
|
|
1368
|
+
"session",
|
|
1369
|
+
"sessions",
|
|
1370
|
+
"staff",
|
|
1371
|
+
"permission",
|
|
1372
|
+
"permissions",
|
|
1373
|
+
"access",
|
|
1374
|
+
"access-control",
|
|
1375
|
+
"security",
|
|
1376
|
+
"user",
|
|
1377
|
+
"users"
|
|
1378
|
+
];
|
|
1379
|
+
return protectionSourceTerms.some((term) => normalizedSource.includes(term));
|
|
1380
|
+
}
|
|
1381
|
+
function getHelperAssignments(handlerBody) {
|
|
1382
|
+
const assignments = [];
|
|
1383
|
+
const assignmentPattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:await\s+)?([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)?)\s*\(/g;
|
|
1384
|
+
for (const match of handlerBody.matchAll(assignmentPattern)) {
|
|
1385
|
+
const variableName = match[1];
|
|
1386
|
+
const helperName = match[2];
|
|
1387
|
+
const index = match.index ?? 0;
|
|
1388
|
+
if (!variableName || !helperName) {
|
|
1389
|
+
continue;
|
|
1390
|
+
}
|
|
1391
|
+
assignments.push({
|
|
1392
|
+
variableName,
|
|
1393
|
+
helperName,
|
|
1394
|
+
index,
|
|
1395
|
+
endIndex: index + match[0].length
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
return assignments;
|
|
1399
|
+
}
|
|
1400
|
+
function isRawRequestAccessorHelper(helperName) {
|
|
1401
|
+
const normalizedHelperName = helperName.toLowerCase();
|
|
1402
|
+
const rawAccessorHelpers = [
|
|
1403
|
+
"request.headers.get",
|
|
1404
|
+
"req.headers.get",
|
|
1405
|
+
"headers.get",
|
|
1406
|
+
"request.cookies.get",
|
|
1407
|
+
"req.cookies.get",
|
|
1408
|
+
"request.nexturl.searchparams.get",
|
|
1409
|
+
"req.nexturl.searchparams.get",
|
|
1410
|
+
"searchparams.get",
|
|
1411
|
+
"cookies"
|
|
1412
|
+
];
|
|
1413
|
+
return rawAccessorHelpers.includes(normalizedHelperName);
|
|
1414
|
+
}
|
|
1415
|
+
function isGuardHelperAssignmentNearTop(assignmentIndex, handlerBody, sensitiveOperation) {
|
|
1416
|
+
if (sensitiveOperation) {
|
|
1417
|
+
return assignmentIndex < sensitiveOperation.index;
|
|
1418
|
+
}
|
|
1419
|
+
return assignmentIndex < Math.min(1500, handlerBody.length);
|
|
1420
|
+
}
|
|
1421
|
+
function findAccessControlGuardForVariable({
|
|
1422
|
+
handlerBody,
|
|
1423
|
+
variableName,
|
|
1424
|
+
startIndex
|
|
1425
|
+
}) {
|
|
1426
|
+
const guardSearchEnd = Math.min(handlerBody.length, startIndex + 2500);
|
|
1427
|
+
const ifStatements = getIfStatements(handlerBody, startIndex, guardSearchEnd);
|
|
1428
|
+
for (const ifStatement of ifStatements) {
|
|
1429
|
+
if (!doesConditionBlockWhenVariableMissing(ifStatement.condition, variableName)) {
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
const branch = getIfFailureBranch(handlerBody, ifStatement.branchStartIndex);
|
|
1433
|
+
const returnSignal = getAccessDeniedReturnSignal(branch.text);
|
|
1434
|
+
if (!returnSignal) {
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
return {
|
|
1438
|
+
endIndex: branch.endIndex,
|
|
1439
|
+
returnSignal
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
return null;
|
|
1005
1443
|
}
|
|
1006
|
-
function
|
|
1444
|
+
function doesConditionBlockWhenVariableMissing(condition, variableName) {
|
|
1445
|
+
const escapedVariableName = escapeRegExp(variableName);
|
|
1446
|
+
return new RegExp(`!\\s*${escapedVariableName}(?:\\b|\\?\\.|\\.)`).test(condition) || new RegExp(`\\b${escapedVariableName}\\s*(?:={2,3})\\s*(?:null|undefined|false)\\b`).test(condition) || new RegExp(`\\b(?:null|undefined|false)\\s*(?:={2,3})\\s*${escapedVariableName}\\b`).test(condition) || new RegExp(`!\\s*${escapedVariableName}(?:\\?\\.)?\\.[A-Za-z_$][\\w$]*`).test(condition) || new RegExp(`\\b${escapedVariableName}(?:\\?\\.)?\\.[A-Za-z_$][\\w$]*\\s*(?:={2,3})\\s*false\\b`).test(condition) || new RegExp(`\\bfalse\\s*(?:={2,3})\\s*${escapedVariableName}(?:\\?\\.)?\\.[A-Za-z_$][\\w$]*\\b`).test(condition);
|
|
1447
|
+
}
|
|
1448
|
+
function getIfFailureBranch(handlerBody, branchStartIndex) {
|
|
1449
|
+
let index = branchStartIndex;
|
|
1450
|
+
while (/\s/.test(handlerBody[index] ?? "")) {
|
|
1451
|
+
index++;
|
|
1452
|
+
}
|
|
1453
|
+
if (handlerBody[index] === "{") {
|
|
1454
|
+
const closeBraceIndex = findMatchingBrace(handlerBody, index);
|
|
1455
|
+
if (closeBraceIndex !== -1) {
|
|
1456
|
+
return {
|
|
1457
|
+
text: handlerBody.slice(index, closeBraceIndex + 1),
|
|
1458
|
+
endIndex: closeBraceIndex
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
const semicolonIndex = handlerBody.indexOf(";", index);
|
|
1463
|
+
const newlineIndex = handlerBody.indexOf("\n", index);
|
|
1464
|
+
const branchEndIndex = [semicolonIndex, newlineIndex].filter((candidateIndex) => candidateIndex !== -1).sort((leftIndex, rightIndex) => leftIndex - rightIndex)[0] ?? Math.min(handlerBody.length, index + 300);
|
|
1465
|
+
return {
|
|
1466
|
+
text: handlerBody.slice(index, branchEndIndex + 1),
|
|
1467
|
+
endIndex: branchEndIndex
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
function getAccessDeniedReturnSignal(branchText) {
|
|
1471
|
+
const normalizedBranch = branchText.toLowerCase();
|
|
1472
|
+
if (!normalizedBranch.includes("return") && !normalizedBranch.includes("redirect(")) {
|
|
1473
|
+
return null;
|
|
1474
|
+
}
|
|
1475
|
+
if (/\bstatus\s*:\s*401\b/.test(normalizedBranch) || /\b401\b/.test(normalizedBranch)) {
|
|
1476
|
+
return "guard returns 401";
|
|
1477
|
+
}
|
|
1478
|
+
if (/\bstatus\s*:\s*403\b/.test(normalizedBranch) || /\b403\b/.test(normalizedBranch)) {
|
|
1479
|
+
return "guard returns 403";
|
|
1480
|
+
}
|
|
1481
|
+
if (normalizedBranch.includes("unauthorized")) {
|
|
1482
|
+
return "guard returns Unauthorized";
|
|
1483
|
+
}
|
|
1484
|
+
if (normalizedBranch.includes("forbidden")) {
|
|
1485
|
+
return "guard returns Forbidden";
|
|
1486
|
+
}
|
|
1487
|
+
if (/redirect\s*\(\s*["']\/(?:login|sign-in|signin|auth)/.test(normalizedBranch)) {
|
|
1488
|
+
return "guard redirects to login";
|
|
1489
|
+
}
|
|
1490
|
+
return null;
|
|
1491
|
+
}
|
|
1492
|
+
function getImportInfoForHelper(imports, helperName) {
|
|
1493
|
+
const helperRootName = helperName.split(".")[0];
|
|
1494
|
+
return imports.find((importInfo) => importInfo.localName === helperRootName);
|
|
1495
|
+
}
|
|
1496
|
+
function getFirstSensitiveOperation(handlerBody) {
|
|
1497
|
+
const sensitiveOperationPatterns = [
|
|
1498
|
+
{ label: "request.formData", pattern: /\brequest\.formData\s*\(/i },
|
|
1499
|
+
{ label: "request.json", pattern: /\brequest\.json\s*\(/i },
|
|
1500
|
+
{ label: "file.arrayBuffer", pattern: /\b[A-Za-z_$][\w$]*\.arrayBuffer\s*\(/i },
|
|
1501
|
+
{ label: "Buffer.from", pattern: /\bBuffer\.from\s*\(/ },
|
|
1502
|
+
{ label: "uploadToR2", pattern: /\buploadToR2\s*\(/i },
|
|
1503
|
+
{ label: "upload", pattern: /\bupload[A-Za-z0-9_$]*\s*\(/i },
|
|
1504
|
+
{ label: "putObject", pattern: /\bputObject\s*\(/i },
|
|
1505
|
+
{ label: "storage", pattern: /\bstorage\b/i },
|
|
1506
|
+
{ label: "write", pattern: /\bwrite[A-Za-z0-9_$]*\s*\(/i },
|
|
1507
|
+
{ label: "create", pattern: /\bcreate[A-Za-z0-9_$]*\s*\(/i },
|
|
1508
|
+
{ label: "update", pattern: /\bupdate[A-Za-z0-9_$]*\s*\(/i },
|
|
1509
|
+
{ label: "delete", pattern: /\bdelete[A-Za-z0-9_$]*\s*\(/i },
|
|
1510
|
+
{ label: "insert", pattern: /\binsert[A-Za-z0-9_$]*\s*\(/i },
|
|
1511
|
+
{ label: "cleanup", pattern: /\bcleanup[A-Za-z0-9_$]*\s*\(/i },
|
|
1512
|
+
{ label: "revalidate", pattern: /\brevalidate[A-Za-z0-9_$]*\s*\(/i },
|
|
1513
|
+
{ label: "prisma", pattern: /\bprisma\./i },
|
|
1514
|
+
{ label: "db", pattern: /\bdb\./i },
|
|
1515
|
+
{ label: "checkout", pattern: /\bcheckout\b/i },
|
|
1516
|
+
{ label: "payment", pattern: /\bpayment\b/i },
|
|
1517
|
+
{ label: "send", pattern: /\bsend[A-Za-z0-9_$]*\s*\(/i },
|
|
1518
|
+
{ label: "mutation", pattern: /\bmutation\b/i }
|
|
1519
|
+
];
|
|
1520
|
+
const matches = [];
|
|
1521
|
+
for (const sensitiveOperationPattern of sensitiveOperationPatterns) {
|
|
1522
|
+
const match = sensitiveOperationPattern.pattern.exec(handlerBody);
|
|
1523
|
+
if (match?.index !== void 0) {
|
|
1524
|
+
matches.push({
|
|
1525
|
+
label: sensitiveOperationPattern.label,
|
|
1526
|
+
index: match.index
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
return matches.sort((leftMatch, rightMatch) => leftMatch.index - rightMatch.index)[0];
|
|
1531
|
+
}
|
|
1532
|
+
function escapeRegExp(value) {
|
|
1533
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1534
|
+
}
|
|
1535
|
+
function hasWeakAuthRelatedSignal(content) {
|
|
1536
|
+
const normalizedContent = stripStrongProtectionIdentifiers(content).toLowerCase();
|
|
1537
|
+
const weakSignals = [
|
|
1538
|
+
"session",
|
|
1539
|
+
"authorization",
|
|
1540
|
+
"bearer",
|
|
1541
|
+
"cookies()",
|
|
1542
|
+
"request.cookies",
|
|
1543
|
+
"middleware",
|
|
1544
|
+
"getuser",
|
|
1545
|
+
"jwt",
|
|
1546
|
+
"auth(",
|
|
1547
|
+
"getserversession",
|
|
1548
|
+
"currentuser",
|
|
1549
|
+
"clerkclient"
|
|
1550
|
+
];
|
|
1551
|
+
return weakSignals.some((signal) => normalizedContent.includes(signal));
|
|
1552
|
+
}
|
|
1553
|
+
function stripStrongProtectionIdentifiers(content) {
|
|
1554
|
+
return content.replace(/\b(?:await\s+)?require(?:Auth|User|Admin)\s*\([^)]*\)/gi, "").replace(/\bauth\.protect\s*\([^)]*\)/gi, "").replace(/\b(?:await\s+)?verifySession\s*\([^)]*\)/gi, "");
|
|
1555
|
+
}
|
|
1556
|
+
function hasStrongProtectionCallBeforeSensitiveWork(handlerBody, sensitiveOperation) {
|
|
1557
|
+
const protectionCutoffIndex = sensitiveOperation?.index ?? Math.min(2e3, handlerBody.length);
|
|
1558
|
+
const strongProtectionPatterns = [
|
|
1559
|
+
/\b(?:await\s+)?requireAuth\s*\(/gi,
|
|
1560
|
+
/\b(?:await\s+)?requireUser\s*\(/gi,
|
|
1561
|
+
/\b(?:await\s+)?requireAdmin\s*\(/gi,
|
|
1562
|
+
/\bauth\.protect\s*\(/gi,
|
|
1563
|
+
/(?:^|[;{\n]\s*)(?:await\s+)?verifySession\s*\(/gi
|
|
1564
|
+
];
|
|
1565
|
+
for (const pattern of strongProtectionPatterns) {
|
|
1566
|
+
pattern.lastIndex = 0;
|
|
1567
|
+
for (const match of handlerBody.matchAll(pattern)) {
|
|
1568
|
+
if ((match.index ?? Number.POSITIVE_INFINITY) < protectionCutoffIndex) {
|
|
1569
|
+
return true;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
return hasAuthGuardRedirectBeforeIndex(handlerBody, protectionCutoffIndex) || hasThrowBasedAuthGuardBeforeIndex(handlerBody, protectionCutoffIndex);
|
|
1574
|
+
}
|
|
1575
|
+
function hasAuthGuardRedirectBeforeIndex(handlerBody, cutoffIndex) {
|
|
1576
|
+
const redirectPattern = /\bif\s*\(([\s\S]*?)\)\s*(?:return\s+)?redirect\s*\(\s*["']\/(?:login|sign-in|signin|auth)/gi;
|
|
1577
|
+
for (const match of handlerBody.matchAll(redirectPattern)) {
|
|
1578
|
+
const condition = match[1] ?? "";
|
|
1579
|
+
if ((match.index ?? Number.POSITIVE_INFINITY) < cutoffIndex && /\!/.test(condition)) {
|
|
1580
|
+
return true;
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
return false;
|
|
1584
|
+
}
|
|
1585
|
+
function hasThrowBasedAuthGuardBeforeIndex(handlerBody, cutoffIndex) {
|
|
1586
|
+
const throwGuardPattern = /\bif\s*\(([\s\S]*?)\)\s*(?:throw\s+new\s+\w+|throw\s+[^;]*(?:Unauthorized|Forbidden|401|403))/gi;
|
|
1587
|
+
for (const match of handlerBody.matchAll(throwGuardPattern)) {
|
|
1588
|
+
const condition = match[1] ?? "";
|
|
1589
|
+
const guardText = match[0] ?? "";
|
|
1590
|
+
if ((match.index ?? Number.POSITIVE_INFINITY) < cutoffIndex && /\!/.test(condition) && /Unauthorized|Forbidden|401|403/i.test(guardText)) {
|
|
1591
|
+
return true;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
return false;
|
|
1595
|
+
}
|
|
1596
|
+
function hasWeakSecretProtectionSignal(content) {
|
|
1007
1597
|
const normalizedContent = content.toLowerCase();
|
|
1008
1598
|
return /\bprocess\.env\.[A-Za-z0-9_]*SECRET\b/.test(content) || /\bprocess\.env\[['"`][A-Za-z0-9_]*SECRET['"`]\]/.test(content) || normalizedContent.includes("cron_secret") || normalizedContent.includes("revalidate_secret") || normalizedContent.includes("authorization") || normalizedContent.includes("bearer") || normalizedContent.includes("token");
|
|
1009
1599
|
}
|
|
1600
|
+
function hasSecretProtectionGuardBeforeSensitiveWork(handlerBody, sensitiveOperation) {
|
|
1601
|
+
const protectionCutoffIndex = sensitiveOperation?.index ?? Math.min(2e3, handlerBody.length);
|
|
1602
|
+
const ifStatements = getIfStatements(handlerBody, 0, protectionCutoffIndex);
|
|
1603
|
+
for (const ifStatement of ifStatements) {
|
|
1604
|
+
if (!isSecretTokenGuardCondition(ifStatement.condition)) {
|
|
1605
|
+
continue;
|
|
1606
|
+
}
|
|
1607
|
+
const branch = getIfFailureBranch(handlerBody, ifStatement.branchStartIndex);
|
|
1608
|
+
if (getAccessDeniedReturnSignal(branch.text)) {
|
|
1609
|
+
return true;
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
return false;
|
|
1613
|
+
}
|
|
1614
|
+
function isSecretTokenGuardCondition(condition) {
|
|
1615
|
+
const normalizedCondition = condition.toLowerCase();
|
|
1616
|
+
const hasSecretInput = /\b(?:token|secret|authorization|bearer)\b/i.test(condition) || /\bheaders\.get\s*\(\s*["'][^"']*(?:authorization|secret|token|key)[^"']*["']\s*\)/i.test(condition) || /\bsearchParams\.get\s*\(\s*["'](?:secret|token|key)["']\s*\)/i.test(condition);
|
|
1617
|
+
const hasExpectedSecret = /\bprocess\.env\.[A-Za-z0-9_]*(?:SECRET|TOKEN|KEY)\b/.test(condition) || /\b(?:expected|valid|required|cron|revalidate)[A-Za-z0-9_]*(?:Secret|Token|Key)\b/.test(condition) || /\b(?:expected|valid|required|cron|revalidate)[a-z0-9_]*(?:secret|token|key)\b/.test(normalizedCondition);
|
|
1618
|
+
const hasSecretValidationCall = /\b(?:isValid|verify|validate|compare)[A-Za-z0-9_]*(?:Secret|Token|Key)?\s*\(/.test(condition) || /\btimingSafeEqual\s*\(/.test(condition);
|
|
1619
|
+
const hasComparison = /!==|!=|===|==/.test(condition) || /\b(?:timingSafeEqual|compare|verify|isValid|validate)[A-Za-z0-9_]*\s*\(/.test(condition);
|
|
1620
|
+
return hasSecretInput && (hasExpectedSecret || hasSecretValidationCall) && hasComparison;
|
|
1621
|
+
}
|
|
1010
1622
|
function hasRateLimitSignal(content) {
|
|
1011
1623
|
const normalizedContent = content.toLowerCase();
|
|
1012
1624
|
return normalizedContent.includes("ratelimit") || normalizedContent.includes("rate limit") || normalizedContent.includes("limiter") || normalizedContent.includes("upstash") || normalizedContent.includes("throttle");
|
|
@@ -1017,7 +1629,7 @@ function hasValidationSignal(content) {
|
|
|
1017
1629
|
}
|
|
1018
1630
|
function hasCacheHeaderSignal(content) {
|
|
1019
1631
|
const normalizedContent = content.toLowerCase();
|
|
1020
|
-
return normalizedContent.includes("cache-control") || normalizedContent.includes("s-maxage") || normalizedContent.includes("stale-while-revalidate") || normalizedContent.includes("public") || normalizedContent.includes("published")
|
|
1632
|
+
return normalizedContent.includes("cache-control") || normalizedContent.includes("s-maxage") || normalizedContent.includes("stale-while-revalidate") || normalizedContent.includes("public") || normalizedContent.includes("published");
|
|
1021
1633
|
}
|
|
1022
1634
|
function hasMethodBlockingSignal(content) {
|
|
1023
1635
|
const normalizedContent = content.toLowerCase();
|
|
@@ -1179,7 +1791,54 @@ function getLargeFileIssueCopy(kind) {
|
|
|
1179
1791
|
suggestion: "Review whether this file mixes unrelated responsibilities. If so, split it into smaller modules while preserving behavior."
|
|
1180
1792
|
};
|
|
1181
1793
|
}
|
|
1182
|
-
function createApiAuthFixPrompt(file) {
|
|
1794
|
+
function createApiAuthFixPrompt(file, method, intent = "unknown") {
|
|
1795
|
+
if (method && intent === "sensitive-mutation") {
|
|
1796
|
+
return `Review the API route at ${file}.
|
|
1797
|
+
|
|
1798
|
+
Qodfy detected a possible issue in the ${method} handler.
|
|
1799
|
+
|
|
1800
|
+
Goal:
|
|
1801
|
+
Determine whether the ${method} handler should be protected.
|
|
1802
|
+
|
|
1803
|
+
Instructions:
|
|
1804
|
+
- Inspect each exported HTTP handler separately.
|
|
1805
|
+
- Do not add authentication to a GET handler if it is intentionally public.
|
|
1806
|
+
- If the ${method} handler already has a guard that returns 401/403 before sensitive logic, explain where it happens and do not add duplicate auth.
|
|
1807
|
+
- If the ${method} handler handles file uploads, private data, storage writes, payments, account changes, or user-specific actions, add the existing project auth/session check before sensitive logic.
|
|
1808
|
+
- Also verify protections such as input validation, file size limits for uploads, storage path safety, and rate limiting where relevant.
|
|
1809
|
+
- Do not introduce a new auth provider.
|
|
1810
|
+
- Do not refactor unrelated code.
|
|
1811
|
+
- Keep existing response formats unchanged.
|
|
1812
|
+
- If the ${method} handler is intentionally public, add a short comment explaining why and confirm abuse protection exists.
|
|
1813
|
+
|
|
1814
|
+
Return:
|
|
1815
|
+
- Whether each handler is public or protected.
|
|
1816
|
+
- Whether the ${method} handler should be protected.
|
|
1817
|
+
- The safest code change.
|
|
1818
|
+
- Edge cases to test.`;
|
|
1819
|
+
}
|
|
1820
|
+
if (method && intent === "unknown") {
|
|
1821
|
+
return `Review the API route at ${file}.
|
|
1822
|
+
|
|
1823
|
+
Qodfy detected a mutation handler that should be reviewed: ${method}.
|
|
1824
|
+
|
|
1825
|
+
Goal:
|
|
1826
|
+
Determine whether the ${method} handler should be public or protected.
|
|
1827
|
+
|
|
1828
|
+
Instructions:
|
|
1829
|
+
- Inspect each exported HTTP handler separately.
|
|
1830
|
+
- Check what data the ${method} handler reads, writes, or returns.
|
|
1831
|
+
- If it handles private data, user-specific data, writes, uploads, or admin actions, add the existing project auth/session check.
|
|
1832
|
+
- If it is intentionally public, document why and confirm validation and abuse protection exist.
|
|
1833
|
+
- Do not introduce a new auth provider.
|
|
1834
|
+
- Do not refactor unrelated code.
|
|
1835
|
+
- Keep existing response formats unchanged.
|
|
1836
|
+
|
|
1837
|
+
Return:
|
|
1838
|
+
- Whether the ${method} handler should be protected.
|
|
1839
|
+
- The safest code change, if any.
|
|
1840
|
+
- Edge cases to test.`;
|
|
1841
|
+
}
|
|
1183
1842
|
return `Review the API route at ${file}.
|
|
1184
1843
|
|
|
1185
1844
|
Goal:
|
|
@@ -1198,34 +1857,40 @@ Return:
|
|
|
1198
1857
|
- The updated code.
|
|
1199
1858
|
- Any edge cases I should test.`;
|
|
1200
1859
|
}
|
|
1201
|
-
function createPublicReadRouteFixPrompt(file) {
|
|
1860
|
+
function createPublicReadRouteFixPrompt(file, method = "GET") {
|
|
1202
1861
|
return `Review the public read API route at ${file}.
|
|
1203
1862
|
|
|
1863
|
+
Qodfy detected this as a likely public ${method} handler.
|
|
1864
|
+
|
|
1204
1865
|
Goal:
|
|
1205
|
-
Verify that
|
|
1866
|
+
Verify that the ${method} handler is safe to remain public.
|
|
1206
1867
|
|
|
1207
1868
|
Instructions:
|
|
1208
|
-
-
|
|
1869
|
+
- Inspect each exported HTTP handler separately.
|
|
1870
|
+
- Confirm the ${method} handler only exposes published, public, or non-sensitive data.
|
|
1209
1871
|
- Check that route params and query values are validated or sanitized.
|
|
1210
1872
|
- Check for appropriate cache headers where useful.
|
|
1211
1873
|
- Check for abuse protection if the route can be called heavily.
|
|
1212
|
-
- Do not add user authentication
|
|
1874
|
+
- Do not add user authentication to the ${method} handler unless it should be private.
|
|
1213
1875
|
- Do not refactor unrelated code.
|
|
1214
1876
|
|
|
1215
1877
|
Return:
|
|
1216
|
-
- Whether
|
|
1878
|
+
- Whether the ${method} handler appears intentionally public.
|
|
1217
1879
|
- Any low-risk safety improvements.
|
|
1218
1880
|
- Any edge cases I should test.`;
|
|
1219
1881
|
}
|
|
1220
|
-
function createPublicFormProtectionFixPrompt(file) {
|
|
1882
|
+
function createPublicFormProtectionFixPrompt(file, method = "POST") {
|
|
1221
1883
|
return `Review the public form API route at ${file}.
|
|
1222
1884
|
|
|
1885
|
+
Qodfy detected this as a likely public ${method} handler.
|
|
1886
|
+
|
|
1223
1887
|
Goal:
|
|
1224
1888
|
Verify validation, rate limiting, and spam protection.
|
|
1225
1889
|
|
|
1226
1890
|
Instructions:
|
|
1227
|
-
-
|
|
1228
|
-
-
|
|
1891
|
+
- Inspect each exported HTTP handler separately.
|
|
1892
|
+
- Confirm submitted input in the ${method} handler is validated before it is used.
|
|
1893
|
+
- Check for rate limiting or another abuse protection pattern on the ${method} handler.
|
|
1229
1894
|
- Check whether captcha, Turnstile, reCAPTCHA, or hCaptcha is appropriate.
|
|
1230
1895
|
- Do not add user authentication unless the form should be private.
|
|
1231
1896
|
- Do not introduce a new service unless necessary.
|
|
@@ -1236,18 +1901,24 @@ Return:
|
|
|
1236
1901
|
- The safest minimal change if protection is missing.
|
|
1237
1902
|
- Any edge cases I should test.`;
|
|
1238
1903
|
}
|
|
1239
|
-
function createInternalRouteProtectionFixPrompt(file) {
|
|
1904
|
+
function createInternalRouteProtectionFixPrompt(file, method) {
|
|
1905
|
+
const handlerLine = method ? `
|
|
1906
|
+
Qodfy detected this as a likely internal ${method} handler.
|
|
1907
|
+
` : "";
|
|
1908
|
+
const handlerReference = method ? `the ${method} handler` : "the route";
|
|
1240
1909
|
return `Review the internal API route at ${file}.
|
|
1910
|
+
${handlerLine}
|
|
1241
1911
|
|
|
1242
1912
|
Goal:
|
|
1243
1913
|
Confirm this operational route is protected before launch.
|
|
1244
1914
|
|
|
1245
1915
|
Instructions:
|
|
1246
|
-
-
|
|
1916
|
+
- Inspect each exported HTTP handler separately.
|
|
1917
|
+
- Check whether ${handlerReference} is protected by existing auth, a secret token, or server-only access.
|
|
1247
1918
|
- For cron, cleanup, revalidation, or admin routes, prefer the existing project protection pattern.
|
|
1248
1919
|
- Do not introduce a new auth provider.
|
|
1249
1920
|
- Do not change route behavior unless protection is missing.
|
|
1250
|
-
- If
|
|
1921
|
+
- If ${handlerReference} is intentionally reachable, add a short comment explaining the protection boundary.
|
|
1251
1922
|
|
|
1252
1923
|
Return:
|
|
1253
1924
|
- Whether protection already exists.
|
|
@@ -1301,7 +1972,7 @@ Create a safe refactor plan without changing behavior.
|
|
|
1301
1972
|
|
|
1302
1973
|
Instructions:
|
|
1303
1974
|
- Identify the main responsibilities inside this module.
|
|
1304
|
-
- Check whether the
|
|
1975
|
+
- Check whether the module mixes unrelated concerns such as data access, filtering, mapping, sorting, constants, types, validation, or business rules.
|
|
1305
1976
|
- Suggest smaller TypeScript modules that could be extracted safely.
|
|
1306
1977
|
- Do not rewrite the whole file at once.
|
|
1307
1978
|
- Do not change business logic.
|
|
@@ -1467,8 +2138,13 @@ Return:
|
|
|
1467
2138
|
- The updated code.
|
|
1468
2139
|
- Any environment variables required.`;
|
|
1469
2140
|
}
|
|
1470
|
-
function createWebhookSignatureFixPrompt(file) {
|
|
2141
|
+
function createWebhookSignatureFixPrompt(file, method) {
|
|
2142
|
+
const handlerLine = method ? `
|
|
2143
|
+
Qodfy detected a possible issue in the ${method} webhook handler.
|
|
2144
|
+
` : "";
|
|
2145
|
+
const handlerReference = method ? `the ${method} handler` : "the webhook route";
|
|
1471
2146
|
return `Review the webhook API route at ${file}.
|
|
2147
|
+
${handlerLine}
|
|
1472
2148
|
|
|
1473
2149
|
Goal:
|
|
1474
2150
|
Verify that webhook signature validation happens before the event is handled.
|
|
@@ -1476,7 +2152,8 @@ Verify that webhook signature validation happens before the event is handled.
|
|
|
1476
2152
|
Instructions:
|
|
1477
2153
|
- Detect which provider this webhook belongs to based on imports, headers, and environment variables.
|
|
1478
2154
|
- Use the provider's existing verification pattern if already present.
|
|
1479
|
-
-
|
|
2155
|
+
- Verify that ${handlerReference} validates the provider signature before processing the event unless the provider requires a different order.
|
|
2156
|
+
- Do not add user authentication unless the webhook provider requires it.
|
|
1480
2157
|
- Do not introduce unrelated changes.
|
|
1481
2158
|
- If verification already exists, explain where it happens.
|
|
1482
2159
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qodfy/core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "Scanner engine for Qodfy, an open-source launch readiness scanner for AI-built apps.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"qodfy",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|
|
56
56
|
"build": "tsup src/index.ts --format esm --dts --clean --tsconfig tsconfig.json",
|
|
57
|
-
"dev": "tsx src/index.ts"
|
|
57
|
+
"dev": "tsx src/index.ts",
|
|
58
|
+
"test": "node --import tsx --test src/protection.test.ts"
|
|
58
59
|
}
|
|
59
60
|
}
|