@next-secure-check/rules 0.2.0 → 0.4.0
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/LICENSE +22 -21
- package/README.md +1 -1
- package/dist/analysis-cache.d.ts +14 -0
- package/dist/analysis-cache.d.ts.map +1 -0
- package/dist/analysis-cache.js +28 -0
- package/dist/analysis-cache.js.map +1 -0
- package/dist/ast-utils.d.ts +24 -0
- package/dist/ast-utils.d.ts.map +1 -1
- package/dist/ast-utils.js +79 -348
- package/dist/ast-utils.js.map +1 -1
- package/dist/command-ast.d.ts +14 -0
- package/dist/command-ast.d.ts.map +1 -0
- package/dist/command-ast.js +40 -0
- package/dist/command-ast.js.map +1 -0
- package/dist/command-discovery.d.ts +8 -0
- package/dist/command-discovery.d.ts.map +1 -0
- package/dist/command-discovery.js +92 -0
- package/dist/command-discovery.js.map +1 -0
- package/dist/command-flow.d.ts +9 -0
- package/dist/command-flow.d.ts.map +1 -0
- package/dist/command-flow.js +377 -0
- package/dist/command-flow.js.map +1 -0
- package/dist/command-guards.d.ts +4 -0
- package/dist/command-guards.d.ts.map +1 -0
- package/dist/command-guards.js +92 -0
- package/dist/command-guards.js.map +1 -0
- package/dist/password-ast.d.ts +4 -0
- package/dist/password-ast.d.ts.map +1 -0
- package/dist/password-ast.js +102 -0
- package/dist/password-ast.js.map +1 -0
- package/dist/route-ast.d.ts +8 -0
- package/dist/route-ast.d.ts.map +1 -0
- package/dist/route-ast.js +165 -0
- package/dist/route-ast.js.map +1 -0
- package/dist/rule-utils.d.ts +1 -2
- package/dist/rule-utils.d.ts.map +1 -1
- package/dist/rule-utils.js +1 -13
- package/dist/rule-utils.js.map +1 -1
- package/dist/security-rules.d.ts.map +1 -1
- package/dist/security-rules.js +57 -17
- package/dist/security-rules.js.map +1 -1
- package/dist/sql-ast.d.ts +3 -0
- package/dist/sql-ast.d.ts.map +1 -0
- package/dist/sql-ast.js +36 -0
- package/dist/sql-ast.js.map +1 -0
- package/dist/xss-ast.d.ts +13 -0
- package/dist/xss-ast.d.ts.map +1 -0
- package/dist/xss-ast.js +114 -0
- package/dist/xss-ast.js.map +1 -0
- package/package.json +3 -3
package/LICENSE
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 SetraTheXX
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SetraTheXX
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Built-in deterministic security rules for `next-secure-check`.
|
|
|
5
5
|
This is an internal workspace package published for the `next-secure-check` CLI. Rules cover common Next.js security checks across secrets, injection, XSS, auth, config, headers, upload, and validation categories. Most users should install the CLI package instead:
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx next-secure-check scan .
|
|
8
|
+
npx --yes next-secure-check@latest scan .
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
See the main repository for rule documentation:
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SourceFile } from "@next-secure-check/core";
|
|
2
|
+
export type AnalysisCacheStats = {
|
|
3
|
+
cacheHits: number;
|
|
4
|
+
cacheMisses: number;
|
|
5
|
+
};
|
|
6
|
+
export declare class SourceAnalysisCache<TValue> {
|
|
7
|
+
private cache;
|
|
8
|
+
private cacheHits;
|
|
9
|
+
private cacheMisses;
|
|
10
|
+
get(sourceFile: SourceFile, create: () => TValue): TValue;
|
|
11
|
+
stats(): AnalysisCacheStats;
|
|
12
|
+
clear(): void;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=analysis-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analysis-cache.d.ts","sourceRoot":"","sources":["../src/analysis-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAE1D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,qBAAa,mBAAmB,CAAC,MAAM;IACrC,OAAO,CAAC,KAAK,CAAqC;IAClD,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,WAAW,CAAK;IAExB,GAAG,CAAC,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,MAAM,GAAG,MAAM;IAazD,KAAK,IAAI,kBAAkB;IAO3B,KAAK,IAAI,IAAI;CAKd"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export class SourceAnalysisCache {
|
|
2
|
+
cache = new WeakMap();
|
|
3
|
+
cacheHits = 0;
|
|
4
|
+
cacheMisses = 0;
|
|
5
|
+
get(sourceFile, create) {
|
|
6
|
+
const cached = this.cache.get(sourceFile);
|
|
7
|
+
if (cached) {
|
|
8
|
+
this.cacheHits += 1;
|
|
9
|
+
return cached;
|
|
10
|
+
}
|
|
11
|
+
const value = create();
|
|
12
|
+
this.cache.set(sourceFile, value);
|
|
13
|
+
this.cacheMisses += 1;
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
stats() {
|
|
17
|
+
return {
|
|
18
|
+
cacheHits: this.cacheHits,
|
|
19
|
+
cacheMisses: this.cacheMisses
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
clear() {
|
|
23
|
+
this.cache = new WeakMap();
|
|
24
|
+
this.cacheHits = 0;
|
|
25
|
+
this.cacheMisses = 0;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=analysis-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analysis-cache.js","sourceRoot":"","sources":["../src/analysis-cache.ts"],"names":[],"mappings":"AAOA,MAAM,OAAO,mBAAmB;IACtB,KAAK,GAAG,IAAI,OAAO,EAAsB,CAAC;IAC1C,SAAS,GAAG,CAAC,CAAC;IACd,WAAW,GAAG,CAAC,CAAC;IAExB,GAAG,CAAC,UAAsB,EAAE,MAAoB;QAC9C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAC1C,IAAI,MAAM,EAAE,CAAC;YACX,IAAI,CAAC,SAAS,IAAI,CAAC,CAAC;YACpB,OAAO,MAAM,CAAC;QAChB,CAAC;QAED,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC;QACvB,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QAClC,IAAI,CAAC,WAAW,IAAI,CAAC,CAAC;QACtB,OAAO,KAAK,CAAC;IACf,CAAC;IAED,KAAK;QACH,OAAO;YACL,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,WAAW,EAAE,IAAI,CAAC,WAAW;SAC9B,CAAC;IACJ,CAAC;IAED,KAAK;QACH,IAAI,CAAC,KAAK,GAAG,IAAI,OAAO,EAAsB,CAAC;QAC/C,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC;QACnB,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC;IACvB,CAAC;CACF"}
|
package/dist/ast-utils.d.ts
CHANGED
|
@@ -1,17 +1,41 @@
|
|
|
1
1
|
import type { Severity, SourceFile } from "@next-secure-check/core";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import { type AnalysisCacheStats } from "./analysis-cache.js";
|
|
2
4
|
export type AstMatch = {
|
|
3
5
|
line: number;
|
|
4
6
|
column: number;
|
|
5
7
|
evidence: string;
|
|
6
8
|
sourceLine: string;
|
|
9
|
+
evidencePath?: string;
|
|
7
10
|
};
|
|
8
11
|
export type DangerouslySetInnerHtmlMatch = AstMatch & {
|
|
9
12
|
severity: Extract<Severity, "LOW" | "MEDIUM">;
|
|
10
13
|
};
|
|
14
|
+
export type AnalysisFacts = {
|
|
15
|
+
sourceFile: ts.SourceFile;
|
|
16
|
+
commandIdentifiers: ReadonlySet<string>;
|
|
17
|
+
childProcessNamespaces: ReadonlySet<string>;
|
|
18
|
+
commandDeclarationNodes: readonly ts.Node[];
|
|
19
|
+
commandSourcePaths: ReadonlyMap<ts.CallExpression, string>;
|
|
20
|
+
safeCommandCalls: ReadonlySet<ts.CallExpression>;
|
|
21
|
+
routeHandlerNodes: readonly ts.Node[];
|
|
22
|
+
sanitizerIdentifiers: ReadonlySet<string>;
|
|
23
|
+
safeHtmlIdentifiers: ReadonlySet<string>;
|
|
24
|
+
hasPasswordHashing: boolean;
|
|
25
|
+
hasAuthIntent: boolean;
|
|
26
|
+
hasValidationIntent: boolean;
|
|
27
|
+
hasUploadHandling: boolean;
|
|
28
|
+
};
|
|
29
|
+
export type AnalysisFactsCacheStats = AnalysisCacheStats;
|
|
30
|
+
export declare function getAnalysisFacts(file: SourceFile): AnalysisFacts;
|
|
31
|
+
export declare function getAnalysisFactsCacheStats(): AnalysisFactsCacheStats;
|
|
32
|
+
export declare function resetAnalysisFactsCacheForTests(): void;
|
|
11
33
|
export declare function findCommandExecutionMatches(file: SourceFile): AstMatch[];
|
|
12
34
|
export declare function findRawSqlConcatMatches(file: SourceFile): AstMatch[];
|
|
13
35
|
export declare function findDangerouslySetInnerHtmlMatches(file: SourceFile): DangerouslySetInnerHtmlMatch[];
|
|
14
36
|
export declare function findPasswordHandlingMatches(file: SourceFile): AstMatch[];
|
|
15
37
|
export declare function findRouteHandlerExports(file: SourceFile): AstMatch[];
|
|
38
|
+
export declare function hasAuthIntentSignal(file: SourceFile): boolean;
|
|
39
|
+
export declare function hasValidationIntentSignal(file: SourceFile): boolean;
|
|
16
40
|
export declare function findUploadRouteHandlerMatches(file: SourceFile): AstMatch[];
|
|
17
41
|
//# sourceMappingURL=ast-utils.d.ts.map
|
package/dist/ast-utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ast-utils.d.ts","sourceRoot":"","sources":["../src/ast-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"ast-utils.d.ts","sourceRoot":"","sources":["../src/ast-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACpE,OAAO,EAAE,MAAM,YAAY,CAAC;AAC5B,OAAO,EAAuB,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAenF,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG,QAAQ,GAAG;IACpD,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,KAAK,GAAG,QAAQ,CAAC,CAAC;CAC/C,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC;IAC1B,kBAAkB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACxC,sBAAsB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC5C,uBAAuB,EAAE,SAAS,EAAE,CAAC,IAAI,EAAE,CAAC;IAC5C,kBAAkB,EAAE,WAAW,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC;IAC3D,gBAAgB,EAAE,WAAW,CAAC,EAAE,CAAC,cAAc,CAAC,CAAC;IACjD,iBAAiB,EAAE,SAAS,EAAE,CAAC,IAAI,EAAE,CAAC;IACtC,oBAAoB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC1C,mBAAmB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACzC,kBAAkB,EAAE,OAAO,CAAC;IAC5B,aAAa,EAAE,OAAO,CAAC;IACvB,mBAAmB,EAAE,OAAO,CAAC;IAC7B,iBAAiB,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,kBAAkB,CAAC;AAIzD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,UAAU,GAAG,aAAa,CAWhE;AAED,wBAAgB,0BAA0B,IAAI,uBAAuB,CAEpE;AAED,wBAAgB,+BAA+B,IAAI,IAAI,CAEtD;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,UAAU,GAAG,QAAQ,EAAE,CAmBxE;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,UAAU,GAAG,QAAQ,EAAE,CAGpE;AAED,wBAAgB,kCAAkC,CAAC,IAAI,EAAE,UAAU,GAAG,4BAA4B,EAAE,CAQnG;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,UAAU,GAAG,QAAQ,EAAE,CAOxE;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,UAAU,GAAG,QAAQ,EAAE,CAGpE;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAE7D;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAEnE;AAED,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,UAAU,GAAG,QAAQ,EAAE,CAc1E"}
|
package/dist/ast-utils.js
CHANGED
|
@@ -1,379 +1,110 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
collectChildProcessRequires(node, commandIdentifiers, childProcessNamespaces, declarationNodes);
|
|
2
|
+
import { SourceAnalysisCache } from "./analysis-cache.js";
|
|
3
|
+
import { collectCommandDiscovery } from "./command-discovery.js";
|
|
4
|
+
import { collectCommandFlowFacts, isAnalyzableCommandExecutionCall } from "./command-flow.js";
|
|
5
|
+
import { findPasswordHandlingNodes, hasPasswordHashingCall } from "./password-ast.js";
|
|
6
|
+
import { findRawSqlConcatNodes } from "./sql-ast.js";
|
|
7
|
+
import { ROUTE_HANDLER_NAMES, exportedRouteHandlerName, hasAuthIntentInSource, hasValidationIntentInSource, isApiRouteFilePath, isUploadHandlingNode } from "./route-ast.js";
|
|
8
|
+
import { createXssAnalysisFacts, findDangerouslySetInnerHtmlNodes } from "./xss-ast.js";
|
|
9
|
+
const analysisFactsCache = new SourceAnalysisCache();
|
|
10
|
+
export function getAnalysisFacts(file) {
|
|
11
|
+
return analysisFactsCache.get(file, () => {
|
|
12
|
+
const sourceFile = ts.createSourceFile(file.path, file.content, ts.ScriptTarget.Latest, true, scriptKindForPath(file.path));
|
|
13
|
+
return createAnalysisFacts(sourceFile);
|
|
15
14
|
});
|
|
16
|
-
|
|
15
|
+
}
|
|
16
|
+
export function getAnalysisFactsCacheStats() {
|
|
17
|
+
return analysisFactsCache.stats();
|
|
18
|
+
}
|
|
19
|
+
export function resetAnalysisFactsCacheForTests() {
|
|
20
|
+
analysisFactsCache.clear();
|
|
21
|
+
}
|
|
22
|
+
export function findCommandExecutionMatches(file) {
|
|
23
|
+
const { sourceFile, commandIdentifiers, childProcessNamespaces, commandDeclarationNodes, commandSourcePaths, safeCommandCalls } = getAnalysisFacts(file);
|
|
24
|
+
const matches = commandDeclarationNodes.map((node) => matchFromNode(file, sourceFile, node));
|
|
17
25
|
visit(sourceFile, (node) => {
|
|
18
|
-
if (!ts.isCallExpression(node) ||
|
|
26
|
+
if (!ts.isCallExpression(node) ||
|
|
27
|
+
safeCommandCalls.has(node) ||
|
|
28
|
+
!isAnalyzableCommandExecutionCall(node, commandIdentifiers, childProcessNamespaces)) {
|
|
19
29
|
return;
|
|
20
30
|
}
|
|
21
|
-
|
|
31
|
+
const match = matchFromNode(file, sourceFile, node);
|
|
32
|
+
const evidencePath = commandSourcePaths.get(node);
|
|
33
|
+
matches.push(evidencePath ? { ...match, evidencePath } : match);
|
|
22
34
|
});
|
|
23
35
|
return dedupeMatches(matches);
|
|
24
36
|
}
|
|
25
37
|
export function findRawSqlConcatMatches(file) {
|
|
26
|
-
const sourceFile =
|
|
27
|
-
|
|
28
|
-
visit(sourceFile, (node) => {
|
|
29
|
-
if (ts.isCallExpression(node) && isSqlQuerySinkCall(node)) {
|
|
30
|
-
const [firstArgument] = node.arguments;
|
|
31
|
-
if (isInterpolatedSqlTemplate(firstArgument)) {
|
|
32
|
-
matches.push(matchFromNode(file, sourceFile, node));
|
|
33
|
-
}
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
if (ts.isTaggedTemplateExpression(node) && isRawSqlTaggedTemplate(node) && isInterpolatedSqlTemplate(node.template)) {
|
|
37
|
-
matches.push(matchFromNode(file, sourceFile, node));
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
return dedupeMatches(matches);
|
|
38
|
+
const { sourceFile } = getAnalysisFacts(file);
|
|
39
|
+
return dedupeMatches(findRawSqlConcatNodes(sourceFile).map((node) => matchFromNode(file, sourceFile, node)));
|
|
41
40
|
}
|
|
42
41
|
export function findDangerouslySetInnerHtmlMatches(file) {
|
|
43
|
-
const sourceFile
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
const severity = dangerouslySetInnerHtmlSeverity(node.initializer);
|
|
50
|
-
if (!severity) {
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
matches.push({
|
|
54
|
-
...matchFromNode(file, sourceFile, node),
|
|
55
|
-
severity
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
return dedupeMatches(matches);
|
|
42
|
+
const { sourceFile, sanitizerIdentifiers, safeHtmlIdentifiers } = getAnalysisFacts(file);
|
|
43
|
+
return dedupeMatches(findDangerouslySetInnerHtmlNodes(sourceFile, sanitizerIdentifiers, safeHtmlIdentifiers).map(({ node, severity }) => ({
|
|
44
|
+
...matchFromNode(file, sourceFile, node),
|
|
45
|
+
severity
|
|
46
|
+
})));
|
|
59
47
|
}
|
|
60
48
|
export function findPasswordHandlingMatches(file) {
|
|
61
|
-
const sourceFile
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return matches;
|
|
49
|
+
const { sourceFile, hasPasswordHashing } = getAnalysisFacts(file);
|
|
50
|
+
if (hasPasswordHashing) {
|
|
51
|
+
return [];
|
|
65
52
|
}
|
|
66
|
-
|
|
67
|
-
if (isPasswordHandlingNode(node)) {
|
|
68
|
-
matches.push(matchFromNode(file, sourceFile, node));
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
return dedupeMatches(matches);
|
|
53
|
+
return dedupeMatches(findPasswordHandlingNodes(sourceFile).map((node) => matchFromNode(file, sourceFile, node)));
|
|
72
54
|
}
|
|
73
55
|
export function findRouteHandlerExports(file) {
|
|
74
|
-
const sourceFile
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return dedupeMatches(matches);
|
|
56
|
+
const { sourceFile, routeHandlerNodes } = getAnalysisFacts(file);
|
|
57
|
+
return dedupeMatches(routeHandlerNodes.map((node) => matchFromNode(file, sourceFile, node)));
|
|
58
|
+
}
|
|
59
|
+
export function hasAuthIntentSignal(file) {
|
|
60
|
+
return getAnalysisFacts(file).hasAuthIntent;
|
|
61
|
+
}
|
|
62
|
+
export function hasValidationIntentSignal(file) {
|
|
63
|
+
return getAnalysisFacts(file).hasValidationIntent;
|
|
83
64
|
}
|
|
84
65
|
export function findUploadRouteHandlerMatches(file) {
|
|
85
66
|
if (!isApiRouteFilePath(file.path)) {
|
|
86
67
|
return [];
|
|
87
68
|
}
|
|
88
|
-
const sourceFile
|
|
89
|
-
const routeHandlerMatches =
|
|
69
|
+
const { sourceFile, routeHandlerNodes, hasUploadHandling } = getAnalysisFacts(file);
|
|
70
|
+
const routeHandlerMatches = routeHandlerNodes
|
|
71
|
+
.filter((node) => {
|
|
72
|
+
const name = exportedRouteHandlerName(node);
|
|
73
|
+
return name === "DEFAULT" || name === "POST" || name === "PUT" || name === "PATCH";
|
|
74
|
+
})
|
|
75
|
+
.map((node) => matchFromNode(file, sourceFile, node));
|
|
76
|
+
return hasUploadHandling ? dedupeMatches(routeHandlerMatches) : [];
|
|
77
|
+
}
|
|
78
|
+
function createAnalysisFacts(sourceFile) {
|
|
79
|
+
const commandDiscovery = collectCommandDiscovery(sourceFile);
|
|
80
|
+
const commandFlow = collectCommandFlowFacts(sourceFile, commandDiscovery.commandIdentifiers, commandDiscovery.childProcessNamespaces);
|
|
81
|
+
const routeHandlerNodes = [];
|
|
90
82
|
let hasUploadHandling = false;
|
|
91
83
|
visit(sourceFile, (node) => {
|
|
92
|
-
const
|
|
93
|
-
if (
|
|
94
|
-
|
|
84
|
+
const routeName = exportedRouteHandlerName(node);
|
|
85
|
+
if (routeName && (routeName === "DEFAULT" || ROUTE_HANDLER_NAMES.has(routeName))) {
|
|
86
|
+
routeHandlerNodes.push(node);
|
|
95
87
|
}
|
|
96
88
|
if (!hasUploadHandling && isUploadHandlingNode(node)) {
|
|
97
89
|
hasUploadHandling = true;
|
|
98
90
|
}
|
|
99
91
|
});
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
if (!namedBindings || !ts.isNamedImports(namedBindings)) {
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
for (const importSpecifier of namedBindings.elements) {
|
|
122
|
-
const importedName = importSpecifier.propertyName?.text ?? importSpecifier.name.text;
|
|
123
|
-
if (!COMMAND_EXECUTION_NAMES.has(importedName)) {
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
commandIdentifiers.add(importSpecifier.name.text);
|
|
127
|
-
declarationNodes.push(importSpecifier);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
function collectChildProcessRequires(node, commandIdentifiers, childProcessNamespaces, declarationNodes) {
|
|
131
|
-
if (!ts.isVariableDeclaration(node) || !node.initializer) {
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
if (isRequireChildProcessCall(node.initializer)) {
|
|
135
|
-
if (ts.isIdentifier(node.name)) {
|
|
136
|
-
childProcessNamespaces.add(node.name.text);
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
if (ts.isObjectBindingPattern(node.name)) {
|
|
140
|
-
for (const element of node.name.elements) {
|
|
141
|
-
const importedName = bindingElementName(element);
|
|
142
|
-
const localName = bindingElementLocalName(element);
|
|
143
|
-
if (!importedName || !localName || !COMMAND_EXECUTION_NAMES.has(importedName)) {
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
commandIdentifiers.add(localName);
|
|
147
|
-
declarationNodes.push(element);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
if (ts.isPropertyAccessExpression(node.initializer) && isRequireChildProcessCall(node.initializer.expression)) {
|
|
153
|
-
const importedName = node.initializer.name.text;
|
|
154
|
-
if (ts.isIdentifier(node.name) && COMMAND_EXECUTION_NAMES.has(importedName)) {
|
|
155
|
-
commandIdentifiers.add(node.name.text);
|
|
156
|
-
declarationNodes.push(node);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
function isCommandExecutionCall(expression, commandIdentifiers, childProcessNamespaces) {
|
|
161
|
-
if (ts.isIdentifier(expression)) {
|
|
162
|
-
return commandIdentifiers.has(expression.text);
|
|
163
|
-
}
|
|
164
|
-
if (!ts.isPropertyAccessExpression(expression) || !COMMAND_EXECUTION_NAMES.has(expression.name.text)) {
|
|
165
|
-
return false;
|
|
166
|
-
}
|
|
167
|
-
return ts.isIdentifier(expression.expression) && childProcessNamespaces.has(expression.expression.text);
|
|
168
|
-
}
|
|
169
|
-
function isSqlQuerySinkCall(node) {
|
|
170
|
-
const expression = node.expression;
|
|
171
|
-
return ts.isPropertyAccessExpression(expression) && SQL_QUERY_METHOD_NAMES.has(expression.name.text);
|
|
172
|
-
}
|
|
173
|
-
function isRawSqlTaggedTemplate(node) {
|
|
174
|
-
const tag = node.tag;
|
|
175
|
-
return ts.isPropertyAccessExpression(tag) && SQL_RAW_TAG_NAMES.has(tag.name.text);
|
|
176
|
-
}
|
|
177
|
-
function isInterpolatedSqlTemplate(node) {
|
|
178
|
-
return node !== undefined && ts.isTemplateExpression(node) && SQL_KEYWORD_PATTERN.test(node.getText());
|
|
179
|
-
}
|
|
180
|
-
function dangerouslySetInnerHtmlSeverity(initializer) {
|
|
181
|
-
if (!initializer || ts.isStringLiteral(initializer)) {
|
|
182
|
-
return undefined;
|
|
183
|
-
}
|
|
184
|
-
if (!ts.isJsxExpression(initializer) || !initializer.expression) {
|
|
185
|
-
return undefined;
|
|
186
|
-
}
|
|
187
|
-
const expression = initializer.expression;
|
|
188
|
-
if (!ts.isObjectLiteralExpression(expression)) {
|
|
189
|
-
return "LOW";
|
|
190
|
-
}
|
|
191
|
-
const htmlExpression = expression.properties
|
|
192
|
-
.filter(ts.isPropertyAssignment)
|
|
193
|
-
.find((property) => propertyNameText(property.name) === "__html")?.initializer;
|
|
194
|
-
if (!htmlExpression || isStaticHtmlExpression(htmlExpression) || isSanitizedHtmlExpression(htmlExpression)) {
|
|
195
|
-
return undefined;
|
|
196
|
-
}
|
|
197
|
-
return "MEDIUM";
|
|
198
|
-
}
|
|
199
|
-
function propertyNameText(name) {
|
|
200
|
-
if (ts.isIdentifier(name) || ts.isStringLiteralLike(name) || ts.isNumericLiteral(name)) {
|
|
201
|
-
return name.text;
|
|
202
|
-
}
|
|
203
|
-
return undefined;
|
|
204
|
-
}
|
|
205
|
-
function isStaticHtmlExpression(expression) {
|
|
206
|
-
return ts.isStringLiteralLike(expression) || ts.isNoSubstitutionTemplateLiteral(expression);
|
|
207
|
-
}
|
|
208
|
-
function isSanitizedHtmlExpression(expression) {
|
|
209
|
-
if (!ts.isCallExpression(expression)) {
|
|
210
|
-
return false;
|
|
211
|
-
}
|
|
212
|
-
const sanitizerName = callExpressionName(expression.expression);
|
|
213
|
-
return sanitizerName !== undefined && /^(?:sanitize|sanitizeHtml|sanitizeMarkdown)$/i.test(sanitizerName);
|
|
214
|
-
}
|
|
215
|
-
function callExpressionName(expression) {
|
|
216
|
-
if (ts.isIdentifier(expression)) {
|
|
217
|
-
return expression.text;
|
|
218
|
-
}
|
|
219
|
-
if (ts.isPropertyAccessExpression(expression)) {
|
|
220
|
-
return expression.name.text;
|
|
221
|
-
}
|
|
222
|
-
return undefined;
|
|
223
|
-
}
|
|
224
|
-
function hasPasswordHashingCall(sourceFile) {
|
|
225
|
-
let hasHashingCall = false;
|
|
226
|
-
visit(sourceFile, (node) => {
|
|
227
|
-
if (hasHashingCall || !ts.isCallExpression(node)) {
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
hasHashingCall = isPasswordHashingCall(node);
|
|
231
|
-
});
|
|
232
|
-
return hasHashingCall;
|
|
233
|
-
}
|
|
234
|
-
function isPasswordHashingCall(node) {
|
|
235
|
-
const expression = node.expression;
|
|
236
|
-
if (ts.isIdentifier(expression)) {
|
|
237
|
-
return /^scrypt(?:Sync)?$/i.test(expression.text);
|
|
238
|
-
}
|
|
239
|
-
if (!ts.isPropertyAccessExpression(expression)) {
|
|
240
|
-
return false;
|
|
241
|
-
}
|
|
242
|
-
const methodName = expression.name.text;
|
|
243
|
-
const targetName = expressionNameText(expression.expression);
|
|
244
|
-
if (/^scrypt(?:Sync)?$/i.test(methodName) && targetName === "crypto") {
|
|
245
|
-
return true;
|
|
246
|
-
}
|
|
247
|
-
return methodName === "hash" && (targetName === "bcrypt" || targetName === "argon2");
|
|
248
|
-
}
|
|
249
|
-
function isPasswordHandlingNode(node) {
|
|
250
|
-
if (ts.isVariableDeclaration(node)) {
|
|
251
|
-
return isPasswordVariableDeclaration(node);
|
|
252
|
-
}
|
|
253
|
-
return (ts.isPropertyAccessExpression(node) &&
|
|
254
|
-
node.name.text === "password" &&
|
|
255
|
-
isRequestBodyExpression(node.expression) &&
|
|
256
|
-
!isVariableDeclarationInitializer(node));
|
|
257
|
-
}
|
|
258
|
-
function isPasswordVariableDeclaration(node) {
|
|
259
|
-
if (!node.initializer) {
|
|
260
|
-
return false;
|
|
261
|
-
}
|
|
262
|
-
if (ts.isIdentifier(node.name) && node.name.text === "password") {
|
|
263
|
-
return isPasswordSourceExpression(node.initializer);
|
|
264
|
-
}
|
|
265
|
-
if (!ts.isObjectBindingPattern(node.name)) {
|
|
266
|
-
return false;
|
|
267
|
-
}
|
|
268
|
-
return node.name.elements.some((element) => bindingElementName(element) === "password") && isCredentialJsonExpression(node.initializer);
|
|
269
|
-
}
|
|
270
|
-
function isPasswordSourceExpression(expression) {
|
|
271
|
-
return isPasswordPropertyAccess(expression) || isPasswordFormDataGetCall(expression);
|
|
272
|
-
}
|
|
273
|
-
function isPasswordPropertyAccess(expression) {
|
|
274
|
-
return ts.isPropertyAccessExpression(expression) && expression.name.text === "password" && isCredentialContainerExpression(expression.expression);
|
|
275
|
-
}
|
|
276
|
-
function isPasswordFormDataGetCall(expression) {
|
|
277
|
-
if (!ts.isCallExpression(expression) || !ts.isPropertyAccessExpression(expression.expression) || expression.expression.name.text !== "get") {
|
|
278
|
-
return false;
|
|
279
|
-
}
|
|
280
|
-
const [fieldName] = expression.arguments;
|
|
281
|
-
return fieldName !== undefined && ts.isStringLiteralLike(fieldName) && fieldName.text === "password";
|
|
282
|
-
}
|
|
283
|
-
function isVariableDeclarationInitializer(expression) {
|
|
284
|
-
return ts.isVariableDeclaration(expression.parent) && expression.parent.initializer === expression;
|
|
285
|
-
}
|
|
286
|
-
function isCredentialContainerExpression(expression) {
|
|
287
|
-
return isRequestBodyExpression(expression) || isCredentialLikeIdentifier(expression);
|
|
288
|
-
}
|
|
289
|
-
function isRequestBodyExpression(expression) {
|
|
290
|
-
return ((ts.isIdentifier(expression) && /^(body|credentials|credential|data|payload|formData)$/i.test(expression.text)) ||
|
|
291
|
-
(ts.isPropertyAccessExpression(expression) &&
|
|
292
|
-
expression.name.text === "body" &&
|
|
293
|
-
ts.isIdentifier(expression.expression) &&
|
|
294
|
-
/^(req|request)$/i.test(expression.expression.text)));
|
|
295
|
-
}
|
|
296
|
-
function isCredentialJsonExpression(expression) {
|
|
297
|
-
const unwrappedExpression = ts.isAwaitExpression(expression) ? expression.expression : expression;
|
|
298
|
-
return (ts.isCallExpression(unwrappedExpression) &&
|
|
299
|
-
ts.isPropertyAccessExpression(unwrappedExpression.expression) &&
|
|
300
|
-
unwrappedExpression.expression.name.text === "json" &&
|
|
301
|
-
ts.isIdentifier(unwrappedExpression.expression.expression) &&
|
|
302
|
-
/^(request|req)$/i.test(unwrappedExpression.expression.expression.text));
|
|
303
|
-
}
|
|
304
|
-
function isCredentialLikeIdentifier(expression) {
|
|
305
|
-
return ts.isIdentifier(expression) && /^(credentials?|user|account)$/i.test(expression.text);
|
|
306
|
-
}
|
|
307
|
-
function expressionNameText(expression) {
|
|
308
|
-
return ts.isIdentifier(expression) ? expression.text : undefined;
|
|
309
|
-
}
|
|
310
|
-
function exportedRouteHandlerName(node) {
|
|
311
|
-
if (ts.isFunctionDeclaration(node) && hasDefaultExportModifier(node)) {
|
|
312
|
-
return "DEFAULT";
|
|
313
|
-
}
|
|
314
|
-
if (ts.isFunctionDeclaration(node) && hasExportModifier(node) && node.name) {
|
|
315
|
-
return node.name.text;
|
|
316
|
-
}
|
|
317
|
-
if (!ts.isVariableStatement(node) || !hasExportModifier(node)) {
|
|
318
|
-
return undefined;
|
|
319
|
-
}
|
|
320
|
-
const [declaration] = node.declarationList.declarations;
|
|
321
|
-
return declaration && ts.isIdentifier(declaration.name) ? declaration.name.text : undefined;
|
|
322
|
-
}
|
|
323
|
-
function hasExportModifier(node) {
|
|
324
|
-
return ts.canHaveModifiers(node) && (ts.getModifiers(node) ?? []).some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
|
|
325
|
-
}
|
|
326
|
-
function hasDefaultExportModifier(node) {
|
|
327
|
-
const modifiers = ts.canHaveModifiers(node) ? (ts.getModifiers(node) ?? []) : [];
|
|
328
|
-
return (modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) &&
|
|
329
|
-
modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword));
|
|
330
|
-
}
|
|
331
|
-
function isApiRouteFilePath(filePath) {
|
|
332
|
-
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
333
|
-
return (/^app\/api\/.*\/route\.[tj]s$/i.test(normalizedPath) ||
|
|
334
|
-
/^src\/app\/api\/.*\/route\.[tj]s$/i.test(normalizedPath) ||
|
|
335
|
-
/^pages\/api\/.*\.([tj]s)$/i.test(normalizedPath) ||
|
|
336
|
-
/^apps\/[^/]+\/app\/api\/.*\/route\.[tj]s$/i.test(normalizedPath) ||
|
|
337
|
-
/^apps\/[^/]+\/src\/app\/api\/.*\/route\.[tj]s$/i.test(normalizedPath) ||
|
|
338
|
-
/^apps\/[^/]+\/pages\/api\/.*\.([tj]s)$/i.test(normalizedPath));
|
|
339
|
-
}
|
|
340
|
-
function isUploadHandlingNode(node) {
|
|
341
|
-
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
|
|
342
|
-
const methodName = node.expression.name.text;
|
|
343
|
-
if (methodName === "formData") {
|
|
344
|
-
return true;
|
|
345
|
-
}
|
|
346
|
-
if (methodName === "get") {
|
|
347
|
-
const [fieldName] = node.arguments;
|
|
348
|
-
return fieldName !== undefined && ts.isStringLiteralLike(fieldName) && /^(file|files|blob|image|avatar|media)$/i.test(fieldName.text);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
if (ts.isIdentifier(node)) {
|
|
352
|
-
return /^(file|files|blob|formData)$/i.test(node.text);
|
|
353
|
-
}
|
|
354
|
-
if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) {
|
|
355
|
-
return /^(File|Blob|FormData)$/i.test(node.typeName.text);
|
|
356
|
-
}
|
|
357
|
-
return false;
|
|
358
|
-
}
|
|
359
|
-
function isRequireChildProcessCall(node) {
|
|
360
|
-
if (!ts.isCallExpression(node) || !ts.isIdentifier(node.expression) || node.expression.text !== "require") {
|
|
361
|
-
return false;
|
|
362
|
-
}
|
|
363
|
-
const [specifier] = node.arguments;
|
|
364
|
-
return isChildProcessSpecifier(specifier);
|
|
365
|
-
}
|
|
366
|
-
function isChildProcessSpecifier(node) {
|
|
367
|
-
return node !== undefined && ts.isStringLiteralLike(node) && /^(?:node:)?child_process$/.test(node.text);
|
|
368
|
-
}
|
|
369
|
-
function bindingElementName(element) {
|
|
370
|
-
if (element.propertyName) {
|
|
371
|
-
return ts.isIdentifier(element.propertyName) || ts.isStringLiteralLike(element.propertyName) ? element.propertyName.text : undefined;
|
|
372
|
-
}
|
|
373
|
-
return ts.isIdentifier(element.name) ? element.name.text : undefined;
|
|
374
|
-
}
|
|
375
|
-
function bindingElementLocalName(element) {
|
|
376
|
-
return ts.isIdentifier(element.name) ? element.name.text : undefined;
|
|
92
|
+
const xssFacts = createXssAnalysisFacts(sourceFile);
|
|
93
|
+
return {
|
|
94
|
+
sourceFile,
|
|
95
|
+
commandIdentifiers: commandDiscovery.commandIdentifiers,
|
|
96
|
+
childProcessNamespaces: commandDiscovery.childProcessNamespaces,
|
|
97
|
+
commandDeclarationNodes: commandDiscovery.commandDeclarationNodes,
|
|
98
|
+
commandSourcePaths: commandFlow.sourcePaths,
|
|
99
|
+
safeCommandCalls: commandFlow.safeCommandCalls,
|
|
100
|
+
routeHandlerNodes,
|
|
101
|
+
sanitizerIdentifiers: xssFacts.sanitizerIdentifiers,
|
|
102
|
+
safeHtmlIdentifiers: xssFacts.safeHtmlIdentifiers,
|
|
103
|
+
hasPasswordHashing: hasPasswordHashingCall(sourceFile),
|
|
104
|
+
hasAuthIntent: hasAuthIntentInSource(sourceFile),
|
|
105
|
+
hasValidationIntent: hasValidationIntentInSource(sourceFile),
|
|
106
|
+
hasUploadHandling
|
|
107
|
+
};
|
|
377
108
|
}
|
|
378
109
|
function matchFromNode(file, sourceFile, node) {
|
|
379
110
|
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|