@qodfy/core 0.2.7 → 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.js +422 -10
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -919,8 +919,21 @@ function analyzeApiHandler({
|
|
|
919
919
|
const webhookRouteInfo = getWebhookRouteInfo(relativeFile, handlerContent);
|
|
920
920
|
const webhookProvider = webhookRouteInfo?.provider ?? getWebhookProvider(`${normalizedFile}
|
|
921
921
|
${handlerContent.toLowerCase()}`);
|
|
922
|
-
const
|
|
923
|
-
|
|
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);
|
|
924
937
|
const hasRateLimit = hasRateLimitSignal(handlerContent);
|
|
925
938
|
const hasValidation = hasValidationSignal(handlerContent);
|
|
926
939
|
const hasCacheHeaders = hasCacheHeaderSignal(handlerContent);
|
|
@@ -950,13 +963,33 @@ ${handlerContent.toLowerCase()}`);
|
|
|
950
963
|
intent = "sensitive-mutation";
|
|
951
964
|
evidence.push({ label: "path contains", detail: sensitivePathMatch });
|
|
952
965
|
}
|
|
953
|
-
if (
|
|
954
|
-
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` });
|
|
955
986
|
} else {
|
|
956
987
|
evidence.push({ label: `no auth/session check detected in ${method} handler` });
|
|
957
988
|
}
|
|
958
989
|
if (hasSecretProtection) {
|
|
959
|
-
evidence.push({ label: `secret
|
|
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` });
|
|
960
993
|
}
|
|
961
994
|
if (intent === "public-form") {
|
|
962
995
|
evidence.push({
|
|
@@ -1166,6 +1199,33 @@ function findMatchingParen(content, openParenIndex) {
|
|
|
1166
1199
|
}
|
|
1167
1200
|
return -1;
|
|
1168
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
|
+
}
|
|
1169
1229
|
function findMatchingBrace(content, openBraceIndex) {
|
|
1170
1230
|
let depth = 0;
|
|
1171
1231
|
for (let index = openBraceIndex; index < content.length; index++) {
|
|
@@ -1200,14 +1260,365 @@ function getMethodRank(method) {
|
|
|
1200
1260
|
function isMutationMethod(method) {
|
|
1201
1261
|
return method !== "GET";
|
|
1202
1262
|
}
|
|
1203
|
-
function
|
|
1204
|
-
|
|
1205
|
-
|
|
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;
|
|
1443
|
+
}
|
|
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);
|
|
1206
1574
|
}
|
|
1207
|
-
function
|
|
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) {
|
|
1208
1597
|
const normalizedContent = content.toLowerCase();
|
|
1209
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");
|
|
1210
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
|
+
}
|
|
1211
1622
|
function hasRateLimitSignal(content) {
|
|
1212
1623
|
const normalizedContent = content.toLowerCase();
|
|
1213
1624
|
return normalizedContent.includes("ratelimit") || normalizedContent.includes("rate limit") || normalizedContent.includes("limiter") || normalizedContent.includes("upstash") || normalizedContent.includes("throttle");
|
|
@@ -1392,7 +1803,8 @@ Determine whether the ${method} handler should be protected.
|
|
|
1392
1803
|
Instructions:
|
|
1393
1804
|
- Inspect each exported HTTP handler separately.
|
|
1394
1805
|
- Do not add authentication to a GET handler if it is intentionally public.
|
|
1395
|
-
- If the ${method} handler
|
|
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.
|
|
1396
1808
|
- Also verify protections such as input validation, file size limits for uploads, storage path safety, and rate limiting where relevant.
|
|
1397
1809
|
- Do not introduce a new auth provider.
|
|
1398
1810
|
- Do not refactor unrelated code.
|
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
|
}
|