@sun-asterisk/sunlint 1.3.18 → 1.3.19
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/config/rules/enhanced-rules-registry.json +77 -18
- package/core/cli-program.js +2 -1
- package/core/github-annotate-service.js +89 -0
- package/core/output-service.js +25 -0
- package/core/summary-report-service.js +30 -30
- package/package.json +3 -2
- package/rules/common/C014_dependency_injection/symbol-based-analyzer.js +392 -280
- package/rules/common/C017_constructor_logic/analyzer.js +137 -503
- package/rules/common/C017_constructor_logic/config.json +50 -0
- package/rules/common/C017_constructor_logic/symbol-based-analyzer.js +463 -0
- package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +463 -21
- package/rules/security/S011_secure_guid_generation/README.md +255 -0
- package/rules/security/S011_secure_guid_generation/analyzer.js +135 -0
- package/rules/security/S011_secure_guid_generation/config.json +56 -0
- package/rules/security/S011_secure_guid_generation/symbol-based-analyzer.js +609 -0
- package/rules/security/S028_file_upload_size_limits/README.md +537 -0
- package/rules/security/S028_file_upload_size_limits/analyzer.js +202 -0
- package/rules/security/S028_file_upload_size_limits/config.json +186 -0
- package/rules/security/S028_file_upload_size_limits/symbol-based-analyzer.js +530 -0
- package/rules/security/S041_session_token_invalidation/README.md +303 -0
- package/rules/security/S041_session_token_invalidation/analyzer.js +242 -0
- package/rules/security/S041_session_token_invalidation/config.json +175 -0
- package/rules/security/S041_session_token_invalidation/regex-based-analyzer.js +411 -0
- package/rules/security/S041_session_token_invalidation/symbol-based-analyzer.js +674 -0
- package/rules/security/S044_re_authentication_required/README.md +136 -0
- package/rules/security/S044_re_authentication_required/analyzer.js +242 -0
- package/rules/security/S044_re_authentication_required/config.json +161 -0
- package/rules/security/S044_re_authentication_required/regex-based-analyzer.js +329 -0
- package/rules/security/S044_re_authentication_required/symbol-based-analyzer.js +537 -0
- package/rules/security/S045_brute_force_protection/README.md +345 -0
- package/rules/security/S045_brute_force_protection/analyzer.js +336 -0
- package/rules/security/S045_brute_force_protection/config.json +139 -0
- package/rules/security/S045_brute_force_protection/symbol-based-analyzer.js +646 -0
- package/rules/common/C017_constructor_logic/semantic-analyzer.js +0 -340
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S028 Main Analyzer - Limit upload file size and number of files per user
|
|
3
|
+
* Primary: Symbol-based analysis (when available)
|
|
4
|
+
* Fallback: Regex-based for all other cases
|
|
5
|
+
* Command: node cli.js --rule=S028 --input=examples/rule-test-fixtures/rules/S028_file_upload_size_limits --engine=heuristic
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const S028SymbolBasedAnalyzer = require("./symbol-based-analyzer.js");
|
|
9
|
+
|
|
10
|
+
class S028Analyzer {
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
13
|
+
console.log(`🔧 [S028] Constructor called with options:`, !!options);
|
|
14
|
+
console.log(
|
|
15
|
+
`🔧 [S028] Options type:`,
|
|
16
|
+
typeof options,
|
|
17
|
+
Object.keys(options || {})
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
this.ruleId = "S028";
|
|
22
|
+
this.ruleName = "Limit upload file size and number of files per user";
|
|
23
|
+
this.description =
|
|
24
|
+
"File uploads must enforce size limits and file quantity limits to prevent resource exhaustion and DoS attacks. Both file size and number of files should be limited at the server-side.";
|
|
25
|
+
this.semanticEngine = options.semanticEngine || null;
|
|
26
|
+
this.verbose = options.verbose || false;
|
|
27
|
+
|
|
28
|
+
// Configuration
|
|
29
|
+
this.config = {
|
|
30
|
+
useSymbolBased: true, // Primary approach
|
|
31
|
+
maxFileSize: 10 * 1024 * 1024, // 10MB recommended
|
|
32
|
+
maxFiles: 10, // 10 files recommended
|
|
33
|
+
highRiskThreshold: 50 * 1024 * 1024, // 50MB
|
|
34
|
+
mediumRiskThreshold: 20 * 1024 * 1024, // 20MB
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Initialize analyzer
|
|
38
|
+
try {
|
|
39
|
+
this.symbolAnalyzer = new S028SymbolBasedAnalyzer(this.semanticEngine);
|
|
40
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
41
|
+
console.log(`🔧 [S028] Symbol analyzer created successfully`);
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error(`🔧 [S028] Error creating symbol analyzer:`, error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async initialize(semanticEngine = null) {
|
|
49
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
50
|
+
console.log(`🔧 [S028] Initialize called`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (semanticEngine) {
|
|
54
|
+
this.semanticEngine = semanticEngine;
|
|
55
|
+
if (this.symbolAnalyzer) {
|
|
56
|
+
await this.symbolAnalyzer.initialize(semanticEngine);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Analyze array of files (legacy interface)
|
|
65
|
+
*/
|
|
66
|
+
async analyze(files, language, options = {}) {
|
|
67
|
+
const violations = [];
|
|
68
|
+
const fileArray = Array.isArray(files) ? files : [files];
|
|
69
|
+
|
|
70
|
+
for (const filePath of fileArray) {
|
|
71
|
+
try {
|
|
72
|
+
const vs = await this.analyzeFile(filePath, options);
|
|
73
|
+
violations.push(...vs);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.warn(
|
|
76
|
+
`⚠️ [S028] Analysis error for ${filePath}: ${error.message}`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return violations;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Main analyze method - analyzes a single file
|
|
86
|
+
*/
|
|
87
|
+
async analyzeFile(filePath, options = {}) {
|
|
88
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
89
|
+
console.log(`🔧 [S028] Analyzing file:`, filePath);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Skip test files, node_modules, etc.
|
|
93
|
+
if (this.shouldSkipFile(filePath)) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
if (!this.symbolAnalyzer) {
|
|
99
|
+
console.warn(`⚠️ [S028] Symbol analyzer not initialized`);
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let sourceFile = null;
|
|
104
|
+
|
|
105
|
+
// Try to get from semantic engine first
|
|
106
|
+
if (this.semanticEngine?.project) {
|
|
107
|
+
sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Fallback: create temporary ts-morph project
|
|
111
|
+
if (!sourceFile) {
|
|
112
|
+
const fs = require("fs");
|
|
113
|
+
const path = require("path");
|
|
114
|
+
const { Project } = require("ts-morph");
|
|
115
|
+
|
|
116
|
+
if (!fs.existsSync(filePath)) {
|
|
117
|
+
throw new Error(`File not found: ${filePath}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
121
|
+
const tmpProject = new Project({
|
|
122
|
+
useInMemoryFileSystem: true,
|
|
123
|
+
compilerOptions: { allowJs: true },
|
|
124
|
+
});
|
|
125
|
+
sourceFile = tmpProject.createSourceFile(
|
|
126
|
+
path.basename(filePath),
|
|
127
|
+
content
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!sourceFile) {
|
|
132
|
+
console.warn(`⚠️ [S028] Could not load source file: ${filePath}`);
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Analyze with symbol-based analyzer
|
|
137
|
+
const violations = await this.symbolAnalyzer.analyze(
|
|
138
|
+
sourceFile,
|
|
139
|
+
filePath
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
143
|
+
console.log(
|
|
144
|
+
`🔧 [S028] Found ${violations.length} violations in ${filePath}`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return violations;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error(`❌ [S028] Analysis error:`, error.message);
|
|
151
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
152
|
+
console.error(error.stack);
|
|
153
|
+
}
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Analyze a single file (compatibility method)
|
|
160
|
+
*/
|
|
161
|
+
async analyzeSingle(filePath, options = {}) {
|
|
162
|
+
return this.analyzeFile(filePath, options);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Should skip file check
|
|
167
|
+
*/
|
|
168
|
+
shouldSkipFile(filePath) {
|
|
169
|
+
// Skip test files, node_modules, etc.
|
|
170
|
+
const skipPatterns = [
|
|
171
|
+
/node_modules/,
|
|
172
|
+
/\.test\./,
|
|
173
|
+
/\.spec\./,
|
|
174
|
+
/test\//,
|
|
175
|
+
/__tests__\//,
|
|
176
|
+
/\.min\./,
|
|
177
|
+
/\.bundle\./,
|
|
178
|
+
/dist\//,
|
|
179
|
+
/build\//,
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
return skipPatterns.some((pattern) => pattern.test(filePath));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Cleanup method
|
|
187
|
+
*/
|
|
188
|
+
async cleanup() {
|
|
189
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
190
|
+
console.log(`🔧 [S028] Cleanup called`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (
|
|
194
|
+
this.symbolAnalyzer &&
|
|
195
|
+
typeof this.symbolAnalyzer.cleanup === "function"
|
|
196
|
+
) {
|
|
197
|
+
await this.symbolAnalyzer.cleanup();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
module.exports = S028Analyzer;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ruleId": "S028",
|
|
3
|
+
"name": "Limit upload file size and number of files per user",
|
|
4
|
+
"description": "File uploads must enforce size limits and file quantity limits to prevent resource exhaustion and DoS attacks. Both file size and number of files should be limited at the server-side.",
|
|
5
|
+
"category": "security",
|
|
6
|
+
"severity": "medium",
|
|
7
|
+
"languages": ["typescript", "javascript", "java"],
|
|
8
|
+
"tags": [
|
|
9
|
+
"security",
|
|
10
|
+
"file-upload",
|
|
11
|
+
"dos-prevention",
|
|
12
|
+
"resource-limits",
|
|
13
|
+
"owasp"
|
|
14
|
+
],
|
|
15
|
+
"enabled": true,
|
|
16
|
+
"fixable": false,
|
|
17
|
+
"engine": "heuristic",
|
|
18
|
+
"metadata": {
|
|
19
|
+
"owaspCategory": "A04:2021 - Insecure Design",
|
|
20
|
+
"cweId": "CWE-400",
|
|
21
|
+
"description": "File uploads without proper size and quantity limits can lead to Denial of Service (DoS) attacks, resource exhaustion, and storage abuse. Attackers can upload extremely large files or numerous files to consume server resources.",
|
|
22
|
+
"impact": "High - DoS attacks, storage exhaustion, service disruption",
|
|
23
|
+
"likelihood": "Medium",
|
|
24
|
+
"remediation": "Implement server-side file size limits (≤ 10MB recommended) and file quantity limits (≤ 10 files recommended) using framework-specific middleware or configuration"
|
|
25
|
+
},
|
|
26
|
+
"limits": {
|
|
27
|
+
"recommended": {
|
|
28
|
+
"maxFileSize": "10MB",
|
|
29
|
+
"maxFileSizeBytes": 10485760,
|
|
30
|
+
"maxFiles": 10,
|
|
31
|
+
"maxRequestSize": "20MB"
|
|
32
|
+
},
|
|
33
|
+
"thresholds": {
|
|
34
|
+
"highRisk": {
|
|
35
|
+
"fileSize": 52428800,
|
|
36
|
+
"files": 50,
|
|
37
|
+
"description": "File size > 50MB or > 50 files - High risk"
|
|
38
|
+
},
|
|
39
|
+
"mediumRisk": {
|
|
40
|
+
"fileSize": 20971520,
|
|
41
|
+
"files": 20,
|
|
42
|
+
"description": "File size > 20MB or > 20 files - Medium risk"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"patterns": {
|
|
47
|
+
"nodejs": {
|
|
48
|
+
"multer": {
|
|
49
|
+
"required": ["limits.fileSize", "limits.files"],
|
|
50
|
+
"violations": [
|
|
51
|
+
"Missing limits object",
|
|
52
|
+
"Missing limits.fileSize",
|
|
53
|
+
"Missing limits.files",
|
|
54
|
+
"File size exceeds 10MB threshold",
|
|
55
|
+
"File count exceeds 10 files threshold"
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
"express": {
|
|
59
|
+
"required": [
|
|
60
|
+
"express.json({ limit })",
|
|
61
|
+
"express.urlencoded({ limit })"
|
|
62
|
+
],
|
|
63
|
+
"violations": [
|
|
64
|
+
"Missing body size limit",
|
|
65
|
+
"Body size limit exceeds 10MB"
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"nestjs": {
|
|
70
|
+
"fileInterceptor": {
|
|
71
|
+
"required": ["limits.fileSize", "limits.files"],
|
|
72
|
+
"violations": [
|
|
73
|
+
"FileInterceptor missing limits option",
|
|
74
|
+
"File size exceeds 10MB threshold"
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"java": {
|
|
79
|
+
"spring": {
|
|
80
|
+
"required": [
|
|
81
|
+
"spring.servlet.multipart.max-file-size",
|
|
82
|
+
"spring.servlet.multipart.max-request-size"
|
|
83
|
+
],
|
|
84
|
+
"violations": [
|
|
85
|
+
"Missing multipart configuration",
|
|
86
|
+
"File size exceeds 10MB threshold"
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"validationIndicators": {
|
|
92
|
+
"nodejs": [
|
|
93
|
+
"multer",
|
|
94
|
+
"limits.fileSize",
|
|
95
|
+
"limits.files",
|
|
96
|
+
"express.json({ limit })",
|
|
97
|
+
"express.urlencoded({ limit })"
|
|
98
|
+
],
|
|
99
|
+
"nestjs": [
|
|
100
|
+
"FileInterceptor",
|
|
101
|
+
"FilesInterceptor",
|
|
102
|
+
"FileFieldsInterceptor",
|
|
103
|
+
"limits"
|
|
104
|
+
],
|
|
105
|
+
"java": [
|
|
106
|
+
"spring.servlet.multipart.max-file-size",
|
|
107
|
+
"spring.servlet.multipart.max-request-size",
|
|
108
|
+
"MultipartConfigElement"
|
|
109
|
+
]
|
|
110
|
+
},
|
|
111
|
+
"examples": {
|
|
112
|
+
"violations": [
|
|
113
|
+
{
|
|
114
|
+
"code": "const upload = multer({ dest: 'uploads/' });",
|
|
115
|
+
"issue": "Multer configuration missing size limits - vulnerable to DoS",
|
|
116
|
+
"severity": "high"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"code": "const upload = multer({ limits: { fileSize: 100 * 1024 * 1024 } });",
|
|
120
|
+
"issue": "File size limit too high (100MB) - recommend ≤ 10MB",
|
|
121
|
+
"severity": "medium"
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"code": "@UseInterceptors(FileInterceptor('file')) uploadFile(@UploadedFile() file) {}",
|
|
125
|
+
"issue": "FileInterceptor missing size limits",
|
|
126
|
+
"severity": "high"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"code": "app.use(express.json());",
|
|
130
|
+
"issue": "Express middleware missing body size limit",
|
|
131
|
+
"severity": "medium"
|
|
132
|
+
}
|
|
133
|
+
],
|
|
134
|
+
"fixes": [
|
|
135
|
+
{
|
|
136
|
+
"code": "const upload = multer({ limits: { fileSize: 10 * 1024 * 1024, files: 5 } });",
|
|
137
|
+
"description": "Add size and quantity limits to multer (10MB, 5 files)"
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"code": "@UseInterceptors(FileInterceptor('file', { limits: { fileSize: 5 * 1024 * 1024 } })) uploadFile(@UploadedFile() file) {}",
|
|
141
|
+
"description": "Add size limit to FileInterceptor (5MB)"
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"code": "app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ limit: '10mb', extended: true }));",
|
|
145
|
+
"description": "Add body size limit to Express middleware (10MB)"
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"code": "spring.servlet.multipart.max-file-size=10MB\nspring.servlet.multipart.max-request-size=20MB",
|
|
149
|
+
"description": "Configure Spring Boot multipart limits"
|
|
150
|
+
}
|
|
151
|
+
]
|
|
152
|
+
},
|
|
153
|
+
"frameworkSupport": {
|
|
154
|
+
"multer": {
|
|
155
|
+
"patterns": ["multer(", "multer({"],
|
|
156
|
+
"limitKeys": ["limits.fileSize", "limits.files", "limits"],
|
|
157
|
+
"configPath": "First argument object"
|
|
158
|
+
},
|
|
159
|
+
"fileInterceptor": {
|
|
160
|
+
"patterns": [
|
|
161
|
+
"@UseInterceptors(FileInterceptor",
|
|
162
|
+
"@UseInterceptors(FilesInterceptor",
|
|
163
|
+
"@UseInterceptors(FileFieldsInterceptor"
|
|
164
|
+
],
|
|
165
|
+
"limitKeys": ["limits.fileSize", "limits.files"],
|
|
166
|
+
"configPath": "Second argument object"
|
|
167
|
+
},
|
|
168
|
+
"express": {
|
|
169
|
+
"patterns": ["express.json(", "express.urlencoded("],
|
|
170
|
+
"limitKeys": ["limit"],
|
|
171
|
+
"configPath": "First argument object"
|
|
172
|
+
},
|
|
173
|
+
"spring": {
|
|
174
|
+
"patterns": [
|
|
175
|
+
"spring.servlet.multipart.max-file-size",
|
|
176
|
+
"spring.servlet.multipart.max-request-size"
|
|
177
|
+
],
|
|
178
|
+
"configFiles": ["application.properties", "application.yml"]
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
"owaspMapping": {
|
|
182
|
+
"category": "A04:2021 – Insecure Design",
|
|
183
|
+
"subcategories": ["A05:2021 – Security Misconfiguration"],
|
|
184
|
+
"description": "Validates that file upload endpoints have proper size and quantity limits to prevent resource exhaustion attacks"
|
|
185
|
+
}
|
|
186
|
+
}
|