@sun-asterisk/sunlint 1.3.43 → 1.3.44
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/dart_analyzer/README.md +226 -0
- package/dart_analyzer/analysis_options.yaml +66 -0
- package/dart_analyzer/bin/sunlint-dart-macos +0 -0
- package/dart_analyzer/bin/sunlint_dart_analyzer.dart +124 -0
- package/dart_analyzer/lib/analyzer_service.dart +625 -0
- package/dart_analyzer/lib/json_rpc_server.dart +275 -0
- package/dart_analyzer/lib/models/rule.dart +67 -0
- package/dart_analyzer/lib/models/symbol_table.dart +607 -0
- package/dart_analyzer/lib/models/violation.dart +69 -0
- package/dart_analyzer/lib/rules/base_analyzer.dart +52 -0
- package/dart_analyzer/lib/rules/common/C002_no_duplicate_code.dart +344 -0
- package/dart_analyzer/lib/rules/common/C003_no_vague_abbreviations.dart +318 -0
- package/dart_analyzer/lib/rules/common/C006_function_naming.dart +219 -0
- package/dart_analyzer/lib/rules/common/C008_variable_declaration_locality.dart +205 -0
- package/dart_analyzer/lib/rules/common/C010_limit_block_nesting.dart +162 -0
- package/dart_analyzer/lib/rules/common/C012_command_query_separation.dart +214 -0
- package/dart_analyzer/lib/rules/common/C013_no_dead_code.dart +225 -0
- package/dart_analyzer/lib/rules/common/C014_dependency_injection.dart +249 -0
- package/dart_analyzer/lib/rules/common/C017_constructor_logic.dart +158 -0
- package/dart_analyzer/lib/rules/common/C018_no_throw_generic_error.dart +141 -0
- package/dart_analyzer/lib/rules/common/C019_log_level_usage.dart +165 -0
- package/dart_analyzer/lib/rules/common/C020_unused_imports.dart +128 -0
- package/dart_analyzer/lib/rules/common/C021_import_organization.dart +86 -0
- package/dart_analyzer/lib/rules/common/C023_no_duplicate_variable.dart +112 -0
- package/dart_analyzer/lib/rules/common/C024_no_scatter_hardcoded_constants.dart +79 -0
- package/dart_analyzer/lib/rules/common/C029_catch_block_logging.dart +81 -0
- package/dart_analyzer/lib/rules/common/C030_use_custom_error_classes.dart +77 -0
- package/dart_analyzer/lib/rules/common/C031_validation_separation.dart +90 -0
- package/dart_analyzer/lib/rules/common/C033_separate_service_repository.dart +80 -0
- package/dart_analyzer/lib/rules/common/C035_error_logging_context.dart +148 -0
- package/dart_analyzer/lib/rules/common/C040_centralized_validation.dart +84 -0
- package/dart_analyzer/lib/rules/common/C041_no_sensitive_hardcode.dart +103 -0
- package/dart_analyzer/lib/rules/common/C042_boolean_name_prefix.dart +105 -0
- package/dart_analyzer/lib/rules/common/C043_no_console_or_print.dart +101 -0
- package/dart_analyzer/lib/rules/common/C047_no_duplicate_retry_logic.dart +94 -0
- package/dart_analyzer/lib/rules/common/C048_no_bypass_architectural_layers.dart +132 -0
- package/dart_analyzer/lib/rules/common/C052_parsing_or_data_transformation.dart +95 -0
- package/dart_analyzer/lib/rules/common/C060_no_override_superclass.dart +81 -0
- package/dart_analyzer/lib/rules/common/C065_one_behavior_per_test.dart +83 -0
- package/dart_analyzer/lib/rules/common/C067_no_hardcoded_config.dart +89 -0
- package/dart_analyzer/lib/rules/common/C070_no_real_time_tests.dart +99 -0
- package/dart_analyzer/lib/rules/common/C072_single_test_behavior.dart +78 -0
- package/dart_analyzer/lib/rules/common/C073_validate_required_config_on_startup.dart +82 -0
- package/dart_analyzer/lib/rules/common/C075_explicit_return_types.dart +85 -0
- package/dart_analyzer/lib/rules/common/C076_explicit_function_types.dart +104 -0
- package/dart_analyzer/lib/rules/dart/D001_recommended_lint_rules.dart +309 -0
- package/dart_analyzer/lib/rules/dart/D002_dispose_resources.dart +338 -0
- package/dart_analyzer/lib/rules/dart/D003_prefer_widgets_over_methods.dart +273 -0
- package/dart_analyzer/lib/rules/dart/D004_avoid_shrinkwrap_listview.dart +154 -0
- package/dart_analyzer/lib/rules/dart/D005_limit_widget_nesting.dart +265 -0
- package/dart_analyzer/lib/rules/dart/D006_prefer_extracting_large_callbacks.dart +135 -0
- package/dart_analyzer/lib/rules/dart/D007_prefer_init_first_dispose_last.dart +150 -0
- package/dart_analyzer/lib/rules/dart/D008_avoid_long_functions.dart +394 -0
- package/dart_analyzer/lib/rules/dart/D009_limit_function_parameters.dart +179 -0
- package/dart_analyzer/lib/rules/dart/D010_limit_cyclomatic_complexity.dart +257 -0
- package/dart_analyzer/lib/rules/dart/D011_prefer_named_parameters.dart +152 -0
- package/dart_analyzer/lib/rules/dart/D012_prefer_named_boolean_parameters.dart +156 -0
- package/dart_analyzer/lib/rules/dart/D013_single_public_class.dart +246 -0
- package/dart_analyzer/lib/rules/dart/D014_unsafe_collection_access.dart +202 -0
- package/dart_analyzer/lib/rules/dart/D015_copywith_all_parameters.dart +125 -0
- package/dart_analyzer/lib/rules/dart/D016_project_should_have_tests.dart +134 -0
- package/dart_analyzer/lib/rules/dart/D017_pubspec_dependencies_review.dart +187 -0
- package/dart_analyzer/lib/rules/dart/D018_remove_commented_code.dart +196 -0
- package/dart_analyzer/lib/rules/dart/D019_avoid_single_child_multi_child_widget.dart +161 -0
- package/dart_analyzer/lib/rules/dart/D020_limit_if_else_branches.dart +125 -0
- package/dart_analyzer/lib/rules/dart/D021_avoid_negated_boolean_checks.dart +227 -0
- package/dart_analyzer/lib/rules/dart/D022_use_setstate_correctly.dart +269 -0
- package/dart_analyzer/lib/rules/dart/D023_avoid_unnecessary_method_overrides.dart +191 -0
- package/dart_analyzer/lib/rules/dart/D024_avoid_unnecessary_stateful_widget.dart +194 -0
- package/dart_analyzer/lib/rules/dart/D025_avoid_nested_conditional_expressions.dart +90 -0
- package/dart_analyzer/lib/rules/security/S001_backend_auth_communications.dart +155 -0
- package/dart_analyzer/lib/rules/security/S002_os_command_injection.dart +159 -0
- package/dart_analyzer/lib/rules/security/S003_open_redirect_protection.dart +208 -0
- package/dart_analyzer/lib/rules/security/S004_sensitive_data_logging.dart +391 -0
- package/dart_analyzer/lib/rules/security/S005_trusted_service_authorization.dart +182 -0
- package/dart_analyzer/lib/rules/security/S006_no_default_credentials.dart +208 -0
- package/dart_analyzer/lib/rules/security/S007_output_encoding.dart +224 -0
- package/dart_analyzer/lib/rules/security/S008_svg_content_sanitization.dart +211 -0
- package/dart_analyzer/lib/rules/security/S009_no_insecure_encryption.dart +160 -0
- package/dart_analyzer/lib/rules/security/S010_use_csprng.dart +184 -0
- package/dart_analyzer/lib/rules/security/S011_ech_tls_config.dart +175 -0
- package/dart_analyzer/lib/rules/security/S012_hardcoded_secrets.dart +255 -0
- package/dart_analyzer/lib/rules/security/S013_tls_enforcement.dart +148 -0
- package/dart_analyzer/lib/rules/security/S014_tls_version_enforcement.dart +117 -0
- package/dart_analyzer/lib/rules/security/S015_insecure_tls_certificate.dart +315 -0
- package/dart_analyzer/lib/rules/security/S016_no_sensitive_querystring.dart +244 -0
- package/dart_analyzer/lib/rules/security/S017_use_parameterized_queries.dart +191 -0
- package/dart_analyzer/lib/rules/security/S018_no_sensitive_browser_storage.dart +175 -0
- package/dart_analyzer/lib/rules/security/S019_smtp_injection_protection.dart +166 -0
- package/dart_analyzer/lib/rules/security/S020_no_eval_dynamic_code.dart +149 -0
- package/dart_analyzer/lib/rules/security/S021_referrer_policy.dart +146 -0
- package/dart_analyzer/lib/rules/security/S022_escape_output_context.dart +111 -0
- package/dart_analyzer/lib/rules/security/S023_no_json_injection.dart +550 -0
- package/dart_analyzer/lib/rules/security/S024_xpath_xxe_protection.dart +299 -0
- package/dart_analyzer/lib/rules/security/S025_server_side_validation.dart +140 -0
- package/dart_analyzer/lib/rules/security/S026_tls_all_connections.dart +196 -0
- package/dart_analyzer/lib/rules/security/S027_mtls_certificate_validation.dart +195 -0
- package/dart_analyzer/lib/rules/security/S028_file_upload_size_limits.dart +186 -0
- package/dart_analyzer/lib/rules/security/S029_csrf_protection.dart +171 -0
- package/dart_analyzer/lib/rules/security/S030_directory_browsing_protection.dart +144 -0
- package/dart_analyzer/lib/rules/security/S031_secure_session_cookies.dart +118 -0
- package/dart_analyzer/lib/rules/security/S032_httponly_session_cookies.dart +114 -0
- package/dart_analyzer/lib/rules/security/S033_samesite_session_cookies.dart +120 -0
- package/dart_analyzer/lib/rules/security/S034_host_prefix_session_cookies.dart +160 -0
- package/dart_analyzer/lib/rules/security/S035_separate_app_hostnames.dart +117 -0
- package/dart_analyzer/lib/rules/security/S036_lfi_rfi_protection.dart +188 -0
- package/dart_analyzer/lib/rules/security/S037_cache_headers.dart +113 -0
- package/dart_analyzer/lib/rules/security/S038_no_version_headers.dart +114 -0
- package/dart_analyzer/lib/rules/security/S039_tls_certificate_validation.dart +131 -0
- package/dart_analyzer/lib/rules/security/S040_session_fixation_protection.dart +155 -0
- package/dart_analyzer/lib/rules/security/S041_session_token_invalidation.dart +201 -0
- package/dart_analyzer/lib/rules/security/S042_require_re_authentication_for_long_lived.dart +158 -0
- package/dart_analyzer/lib/rules/security/S043_password_changes_invalidate_all_sessions.dart +88 -0
- package/dart_analyzer/lib/rules/security/S044_re_authentication_required.dart +119 -0
- package/dart_analyzer/lib/rules/security/S045_brute_force_protection.dart +253 -0
- package/dart_analyzer/lib/rules/security/S046_jwt_algorithm_allowlist.dart +113 -0
- package/dart_analyzer/lib/rules/security/S047_oauth_pkce_protection.dart +124 -0
- package/dart_analyzer/lib/rules/security/S048_oauth_redirect_uri_validation.dart +134 -0
- package/dart_analyzer/lib/rules/security/S049_short_validity_tokens.dart +145 -0
- package/dart_analyzer/lib/rules/security/S050_reference_tokens_entropy.dart +234 -0
- package/dart_analyzer/lib/rules/security/S051_password_length_policy.dart +171 -0
- package/dart_analyzer/lib/rules/security/S052_weak_otp_entropy.dart +107 -0
- package/dart_analyzer/lib/rules/security/S053_generic_error_messages.dart +159 -0
- package/dart_analyzer/lib/rules/security/S054_no_default_accounts.dart +141 -0
- package/dart_analyzer/lib/rules/security/S055_content_type_validation.dart +324 -0
- package/dart_analyzer/lib/rules/security/S056_log_injection_protection.dart +119 -0
- package/dart_analyzer/lib/rules/security/S057_utc_logging.dart +114 -0
- package/dart_analyzer/lib/rules/security/S058_no_ssrf.dart +175 -0
- package/dart_analyzer/lib/rules/security/S059_disable_debug_mode.dart +172 -0
- package/dart_analyzer/lib/rules/security/S060_password_minimum_length.dart +170 -0
- package/dart_analyzer/lib/symbol_table_extractor.dart +510 -0
- package/dart_analyzer/lib/utils/common_utils.dart +26 -0
- package/dart_analyzer/pubspec.lock +557 -0
- package/dart_analyzer/pubspec.yaml +39 -0
- package/dart_analyzer/test/fixtures/complex_code.dart +95 -0
- package/docs/GENERATED_FILE_HANDLING_SUMMARY.md +2 -2
- package/package.json +3 -2
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import 'package:analyzer/dart/ast/ast.dart';
|
|
2
|
+
import 'package:analyzer/dart/ast/visitor.dart';
|
|
3
|
+
import 'package:analyzer/source/line_info.dart';
|
|
4
|
+
|
|
5
|
+
import '../../models/rule.dart';
|
|
6
|
+
import '../../models/violation.dart';
|
|
7
|
+
import '../base_analyzer.dart';
|
|
8
|
+
|
|
9
|
+
/// S027: Validate mTLS client certificates before authentication
|
|
10
|
+
/// Ensure mTLS client certificates are properly validated before trusting certificate identity
|
|
11
|
+
class S027MtlsCertificateValidationAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'S027';
|
|
14
|
+
|
|
15
|
+
// Patterns for excluded URLs/paths
|
|
16
|
+
static final _excludedUrlPatterns = [
|
|
17
|
+
RegExp(r'localhost', caseSensitive: false),
|
|
18
|
+
RegExp(r'127\.0\.0\.1'),
|
|
19
|
+
RegExp(r'\[::1\]'),
|
|
20
|
+
RegExp(r'0\.0\.0\.0'),
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
@override
|
|
24
|
+
List<Violation> analyze({
|
|
25
|
+
required CompilationUnit unit,
|
|
26
|
+
required String filePath,
|
|
27
|
+
required Rule rule,
|
|
28
|
+
required LineInfo lineInfo,
|
|
29
|
+
}) {
|
|
30
|
+
final violations = <Violation>[];
|
|
31
|
+
final visitor = _S027Visitor(
|
|
32
|
+
filePath: filePath,
|
|
33
|
+
lineInfo: lineInfo,
|
|
34
|
+
violations: violations,
|
|
35
|
+
analyzer: this,
|
|
36
|
+
);
|
|
37
|
+
unit.accept(visitor);
|
|
38
|
+
return violations;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class _S027Visitor extends RecursiveAstVisitor<void> {
|
|
43
|
+
final String filePath;
|
|
44
|
+
final LineInfo lineInfo;
|
|
45
|
+
final List<Violation> violations;
|
|
46
|
+
final S027MtlsCertificateValidationAnalyzer analyzer;
|
|
47
|
+
|
|
48
|
+
_S027Visitor({
|
|
49
|
+
required this.filePath,
|
|
50
|
+
required this.lineInfo,
|
|
51
|
+
required this.violations,
|
|
52
|
+
required this.analyzer,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
@override
|
|
56
|
+
void visitAssignmentExpression(AssignmentExpression node) {
|
|
57
|
+
final source = node.toSource().toLowerCase();
|
|
58
|
+
|
|
59
|
+
// Check for badCertificateCallback = (args) => true (disables cert validation)
|
|
60
|
+
if (source.contains('badcertificatecallback') && source.contains('=> true')) {
|
|
61
|
+
violations.add(analyzer.createViolation(
|
|
62
|
+
filePath: filePath,
|
|
63
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
64
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
65
|
+
message: 'mTLS certificate validation disabled. badCertificateCallback should validate certificates properly',
|
|
66
|
+
));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
super.visitAssignmentExpression(node);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@override
|
|
73
|
+
void visitNamedExpression(NamedExpression node) {
|
|
74
|
+
final name = node.name.label.name.toLowerCase();
|
|
75
|
+
final expression = node.expression;
|
|
76
|
+
|
|
77
|
+
// Check for badCertificateCallback returning true (disabling validation)
|
|
78
|
+
if (name == 'badcertificatecallback' && expression is FunctionExpression) {
|
|
79
|
+
final body = expression.body;
|
|
80
|
+
if (body is ExpressionFunctionBody) {
|
|
81
|
+
final bodySource = body.expression.toSource().toLowerCase();
|
|
82
|
+
if (bodySource == 'true') {
|
|
83
|
+
violations.add(analyzer.createViolation(
|
|
84
|
+
filePath: filePath,
|
|
85
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
86
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
87
|
+
message: 'mTLS certificate validation disabled. Implement proper certificate validation',
|
|
88
|
+
));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check for onBadCertificate: (cert) => true
|
|
94
|
+
if (name == 'onbadcertificate' && expression is FunctionExpression) {
|
|
95
|
+
final body = expression.body;
|
|
96
|
+
if (body is ExpressionFunctionBody) {
|
|
97
|
+
final bodySource = body.expression.toSource().toLowerCase();
|
|
98
|
+
if (bodySource == 'true') {
|
|
99
|
+
violations.add(analyzer.createViolation(
|
|
100
|
+
filePath: filePath,
|
|
101
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
102
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
103
|
+
message: 'Certificate validation bypassed in onBadCertificate. Validate certificates properly',
|
|
104
|
+
));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check for context.setTrustedCertificatesBytes with allowInvalidCertificates: true
|
|
110
|
+
if (name == 'allowinvalidcertificates' &&
|
|
111
|
+
expression is BooleanLiteral &&
|
|
112
|
+
expression.value) {
|
|
113
|
+
violations.add(analyzer.createViolation(
|
|
114
|
+
filePath: filePath,
|
|
115
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
116
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
117
|
+
message: 'allowInvalidCertificates enabled. This bypasses certificate validation',
|
|
118
|
+
));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check for insecure SecurityContext options
|
|
122
|
+
if (name == 'onbadcertificate' || name == 'badcertificatecallback') {
|
|
123
|
+
// Check if the callback always returns true (inline)
|
|
124
|
+
if (expression is SimpleIdentifier) {
|
|
125
|
+
// Could be a function reference - need deeper analysis
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
super.visitNamedExpression(node);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@override
|
|
133
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
134
|
+
final methodName = node.methodName.name.toLowerCase();
|
|
135
|
+
|
|
136
|
+
// Check for SecurityContext methods that bypass validation
|
|
137
|
+
if (methodName == 'settrustanchorsfrombytes' ||
|
|
138
|
+
methodName == 'settrustedcertificatesbytes') {
|
|
139
|
+
// Check if used with allowInvalidCertificates: true in arguments
|
|
140
|
+
for (final arg in node.argumentList.arguments) {
|
|
141
|
+
if (arg is NamedExpression) {
|
|
142
|
+
final argName = arg.name.label.name.toLowerCase();
|
|
143
|
+
if (argName == 'allowinvalidcertificates') {
|
|
144
|
+
final expr = arg.expression;
|
|
145
|
+
if (expr is BooleanLiteral && expr.value) {
|
|
146
|
+
violations.add(analyzer.createViolation(
|
|
147
|
+
filePath: filePath,
|
|
148
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
149
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
150
|
+
message: 'Certificate validation bypassed with allowInvalidCertificates: true',
|
|
151
|
+
));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
super.visitMethodInvocation(node);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@override
|
|
162
|
+
void visitInstanceCreationExpression(InstanceCreationExpression node) {
|
|
163
|
+
final typeName = node.constructorName.type.name2.lexeme.toLowerCase();
|
|
164
|
+
|
|
165
|
+
// Check for HttpClient or SecurityContext creation with insecure options
|
|
166
|
+
if (typeName == 'httpclient' || typeName == 'securitycontext') {
|
|
167
|
+
for (final arg in node.argumentList.arguments) {
|
|
168
|
+
if (arg is NamedExpression) {
|
|
169
|
+
final argName = arg.name.label.name.toLowerCase();
|
|
170
|
+
|
|
171
|
+
// Check for badCertificateCallback: (cert, host, port) => true
|
|
172
|
+
if (argName == 'badcertificatecallback' || argName == 'onbadcertificate') {
|
|
173
|
+
final expr = arg.expression;
|
|
174
|
+
if (expr is FunctionExpression) {
|
|
175
|
+
final body = expr.body;
|
|
176
|
+
if (body is ExpressionFunctionBody) {
|
|
177
|
+
final bodySource = body.expression.toSource().toLowerCase();
|
|
178
|
+
if (bodySource == 'true') {
|
|
179
|
+
violations.add(analyzer.createViolation(
|
|
180
|
+
filePath: filePath,
|
|
181
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
182
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
183
|
+
message: 'Insecure $typeName created with certificate validation disabled',
|
|
184
|
+
));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
super.visitInstanceCreationExpression(node);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import 'package:analyzer/dart/ast/ast.dart';
|
|
2
|
+
import 'package:analyzer/dart/ast/visitor.dart';
|
|
3
|
+
import 'package:analyzer/source/line_info.dart';
|
|
4
|
+
|
|
5
|
+
import '../../models/rule.dart';
|
|
6
|
+
import '../../models/violation.dart';
|
|
7
|
+
import '../base_analyzer.dart';
|
|
8
|
+
|
|
9
|
+
/// S028: File Upload Size Limits
|
|
10
|
+
/// Ensure file upload size limits are enforced
|
|
11
|
+
class S028FileUploadSizeLimitsAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'S028';
|
|
14
|
+
|
|
15
|
+
// File upload methods - ONLY actual implementations that create FormData/MultipartFile
|
|
16
|
+
// Note: We now focus on FormData/MultipartFile creation, not method names
|
|
17
|
+
// because many methods call upload functions without implementing the actual upload
|
|
18
|
+
// This list is kept for special cases where the upload is done differently
|
|
19
|
+
static const _uploadMethods = <String>[]; // Empty - rely on FormData/MultipartFile detection
|
|
20
|
+
|
|
21
|
+
// Patterns that are NOT HTTP uploads (local file operations)
|
|
22
|
+
// These should be excluded from file size limit requirement
|
|
23
|
+
static const _localFileOperationPatterns = [
|
|
24
|
+
'imagegallerysaver', // Flutter plugin to save to device gallery
|
|
25
|
+
'gallerysaver', // Save to gallery
|
|
26
|
+
'saveimagetogallery', // Save image to gallery
|
|
27
|
+
'savevideotogallery', // Save video to gallery
|
|
28
|
+
'filesaver', // File saver plugins
|
|
29
|
+
'savetodisk', // Local disk save
|
|
30
|
+
'writetofile', // Local file write
|
|
31
|
+
'localfilesystem', // Local filesystem operations
|
|
32
|
+
'pathprovider', // Flutter path provider (local storage)
|
|
33
|
+
'gettemporarydirectory', // Temp directory
|
|
34
|
+
'getapplicationdocumentsdirectory', // App documents
|
|
35
|
+
'addpositionlistener', // Audio/video player listeners
|
|
36
|
+
'addcompletelistener', // Completion listeners
|
|
37
|
+
'startvideorecording', // Camera recording (local)
|
|
38
|
+
'stopvideorecording', // Camera recording (local)
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
@override
|
|
42
|
+
List<Violation> analyze({
|
|
43
|
+
required CompilationUnit unit,
|
|
44
|
+
required String filePath,
|
|
45
|
+
required Rule rule,
|
|
46
|
+
required LineInfo lineInfo,
|
|
47
|
+
}) {
|
|
48
|
+
final violations = <Violation>[];
|
|
49
|
+
final visitor = _S028Visitor(
|
|
50
|
+
filePath: filePath,
|
|
51
|
+
lineInfo: lineInfo,
|
|
52
|
+
violations: violations,
|
|
53
|
+
analyzer: this,
|
|
54
|
+
);
|
|
55
|
+
unit.accept(visitor);
|
|
56
|
+
return violations;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
class _S028Visitor extends RecursiveAstVisitor<void> {
|
|
61
|
+
final String filePath;
|
|
62
|
+
final LineInfo lineInfo;
|
|
63
|
+
final List<Violation> violations;
|
|
64
|
+
final S028FileUploadSizeLimitsAnalyzer analyzer;
|
|
65
|
+
|
|
66
|
+
_S028Visitor({
|
|
67
|
+
required this.filePath,
|
|
68
|
+
required this.lineInfo,
|
|
69
|
+
required this.violations,
|
|
70
|
+
required this.analyzer,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Patterns to detect size limit checks - with word boundaries
|
|
74
|
+
static final _sizeLimitPatterns = [
|
|
75
|
+
RegExp(r'\bsize\b', caseSensitive: false),
|
|
76
|
+
RegExp(r'\blength\b', caseSensitive: false),
|
|
77
|
+
RegExp(r'\blimit\b', caseSensitive: false),
|
|
78
|
+
RegExp(r'\bmaxsize\b', caseSensitive: false),
|
|
79
|
+
RegExp(r'\bmax_size\b', caseSensitive: false),
|
|
80
|
+
RegExp(r'\bbytes\b', caseSensitive: false),
|
|
81
|
+
RegExp(r'\bsizelimit\b', caseSensitive: false),
|
|
82
|
+
RegExp(r'\bfilesize\b', caseSensitive: false),
|
|
83
|
+
RegExp(r'\bcontentlength\b', caseSensitive: false),
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
bool _hasSizeCheck(AstNode node) {
|
|
87
|
+
AstNode? current = node.parent;
|
|
88
|
+
int depth = 0;
|
|
89
|
+
|
|
90
|
+
// Only check immediate context (within 5 levels), not class-level
|
|
91
|
+
while (current != null && depth < 5) {
|
|
92
|
+
// Skip checking class declarations - too broad
|
|
93
|
+
if (current is ClassDeclaration) {
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
final parentSource = current.toSource().toLowerCase();
|
|
98
|
+
for (final pattern in _sizeLimitPatterns) {
|
|
99
|
+
if (pattern.hasMatch(parentSource)) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
current = current.parent;
|
|
104
|
+
depth++;
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check if the operation is a local file operation (not HTTP upload)
|
|
110
|
+
bool _isLocalFileOperation(MethodInvocation node) {
|
|
111
|
+
final source = node.toSource().toLowerCase();
|
|
112
|
+
final target = node.target?.toSource().toLowerCase() ?? '';
|
|
113
|
+
|
|
114
|
+
// Check if method call target or source matches local file operation patterns
|
|
115
|
+
return S028FileUploadSizeLimitsAnalyzer._localFileOperationPatterns
|
|
116
|
+
.any((p) => source.contains(p) || target.contains(p));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@override
|
|
120
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
121
|
+
final methodName = node.methodName.name.toLowerCase();
|
|
122
|
+
final source = node.toSource().toLowerCase();
|
|
123
|
+
|
|
124
|
+
// Skip local file operations (save to gallery, disk, etc.) - NOT HTTP uploads
|
|
125
|
+
if (_isLocalFileOperation(node)) {
|
|
126
|
+
super.visitMethodInvocation(node);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check for file upload constructor calls (parsed as method invocations in Dart)
|
|
131
|
+
// MultipartFile(...) or FormData(...) without 'new' keyword
|
|
132
|
+
if (methodName == 'multipartfile' || methodName == 'formdata') {
|
|
133
|
+
if (!_hasSizeCheck(node)) {
|
|
134
|
+
violations.add(analyzer.createViolation(
|
|
135
|
+
filePath: filePath,
|
|
136
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
137
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
138
|
+
message: 'File upload without size limit - enforce maximum file size',
|
|
139
|
+
));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Skip debug/logging statements - they just mention "upload" in strings
|
|
144
|
+
if (methodName == 'debugprint' || methodName == 'print' ||
|
|
145
|
+
methodName == 'log' || methodName.startsWith('log')) {
|
|
146
|
+
super.visitMethodInvocation(node);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Check for file upload operations by method name only (not string content)
|
|
151
|
+
bool isUploadOp = S028FileUploadSizeLimitsAnalyzer._uploadMethods
|
|
152
|
+
.any((m) => methodName.contains(m));
|
|
153
|
+
|
|
154
|
+
if (isUploadOp) {
|
|
155
|
+
if (!_hasSizeCheck(node)) {
|
|
156
|
+
violations.add(analyzer.createViolation(
|
|
157
|
+
filePath: filePath,
|
|
158
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
159
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
160
|
+
message: 'File upload without size limit check - enforce maximum file size',
|
|
161
|
+
));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
super.visitMethodInvocation(node);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@override
|
|
169
|
+
void visitInstanceCreationExpression(InstanceCreationExpression node) {
|
|
170
|
+
final typeName = node.constructorName.type.name2.lexeme.toLowerCase();
|
|
171
|
+
|
|
172
|
+
// Check for MultipartFile or FormData creation (when 'new' keyword is used)
|
|
173
|
+
if (typeName == 'multipartfile' || typeName == 'formdata') {
|
|
174
|
+
if (!_hasSizeCheck(node)) {
|
|
175
|
+
violations.add(analyzer.createViolation(
|
|
176
|
+
filePath: filePath,
|
|
177
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
178
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
179
|
+
message: 'File upload creation without size limit',
|
|
180
|
+
));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
super.visitInstanceCreationExpression(node);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import 'package:analyzer/dart/ast/ast.dart';
|
|
2
|
+
import 'package:analyzer/dart/ast/visitor.dart';
|
|
3
|
+
import 'package:analyzer/source/line_info.dart';
|
|
4
|
+
|
|
5
|
+
import '../../models/rule.dart';
|
|
6
|
+
import '../../models/violation.dart';
|
|
7
|
+
import '../base_analyzer.dart';
|
|
8
|
+
|
|
9
|
+
/// S029: CSRF Protection
|
|
10
|
+
/// Ensure CSRF protection is implemented for state-changing HTTP operations
|
|
11
|
+
class S029CsrfProtectionAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'S029';
|
|
14
|
+
|
|
15
|
+
// HTTP client methods that indicate actual network requests
|
|
16
|
+
static const _httpClientMethods = [
|
|
17
|
+
'http.post', 'http.put', 'http.patch', 'http.delete',
|
|
18
|
+
'dio.post', 'dio.put', 'dio.patch', 'dio.delete',
|
|
19
|
+
'client.post', 'client.put', 'client.patch', 'client.delete',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
// HTTP method names when called on HTTP clients
|
|
23
|
+
static const _httpMethods = ['post', 'put', 'patch', 'delete'];
|
|
24
|
+
|
|
25
|
+
// HTTP client class names
|
|
26
|
+
static const _httpClients = [
|
|
27
|
+
'http', 'dio', 'client', 'httpclient', 'http_client',
|
|
28
|
+
'apiservice', 'api_service', 'apiclient', 'api_client',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
// CSRF protection indicators
|
|
32
|
+
static const _csrfIndicators = [
|
|
33
|
+
'csrf', 'xsrf', '_token', 'csrftoken', 'csrf_token',
|
|
34
|
+
'x-csrf-token', 'x-xsrf-token', 'antiforgery',
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
@override
|
|
38
|
+
List<Violation> analyze({
|
|
39
|
+
required CompilationUnit unit,
|
|
40
|
+
required String filePath,
|
|
41
|
+
required Rule rule,
|
|
42
|
+
required LineInfo lineInfo,
|
|
43
|
+
}) {
|
|
44
|
+
// Skip Flutter UI files - they don't make direct HTTP calls
|
|
45
|
+
final fileName = filePath.toLowerCase();
|
|
46
|
+
if (fileName.contains('screen') ||
|
|
47
|
+
fileName.contains('widget') ||
|
|
48
|
+
fileName.contains('view') ||
|
|
49
|
+
fileName.contains('page') ||
|
|
50
|
+
fileName.contains('component') ||
|
|
51
|
+
fileName.contains('_item') ||
|
|
52
|
+
fileName.contains('effect_item') ||
|
|
53
|
+
fileName.contains('stepper') ||
|
|
54
|
+
fileName.contains('step_')) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
final violations = <Violation>[];
|
|
59
|
+
final visitor = _S029Visitor(
|
|
60
|
+
filePath: filePath,
|
|
61
|
+
lineInfo: lineInfo,
|
|
62
|
+
violations: violations,
|
|
63
|
+
analyzer: this,
|
|
64
|
+
);
|
|
65
|
+
unit.accept(visitor);
|
|
66
|
+
return violations;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class _S029Visitor extends RecursiveAstVisitor<void> {
|
|
71
|
+
final String filePath;
|
|
72
|
+
final LineInfo lineInfo;
|
|
73
|
+
final List<Violation> violations;
|
|
74
|
+
final S029CsrfProtectionAnalyzer analyzer;
|
|
75
|
+
|
|
76
|
+
_S029Visitor({
|
|
77
|
+
required this.filePath,
|
|
78
|
+
required this.lineInfo,
|
|
79
|
+
required this.violations,
|
|
80
|
+
required this.analyzer,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
@override
|
|
84
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
85
|
+
final methodName = node.methodName.name.toLowerCase();
|
|
86
|
+
|
|
87
|
+
// Only check if it's an HTTP method
|
|
88
|
+
if (!S029CsrfProtectionAnalyzer._httpMethods.contains(methodName)) {
|
|
89
|
+
super.visitMethodInvocation(node);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check if the method is called on an HTTP client
|
|
94
|
+
final target = node.target;
|
|
95
|
+
if (target == null) {
|
|
96
|
+
super.visitMethodInvocation(node);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
final targetSource = target.toSource().toLowerCase();
|
|
101
|
+
bool isHttpClient = S029CsrfProtectionAnalyzer._httpClients
|
|
102
|
+
.any((c) => targetSource.contains(c));
|
|
103
|
+
|
|
104
|
+
if (!isHttpClient) {
|
|
105
|
+
super.visitMethodInvocation(node);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// This is a state-changing HTTP call - check for CSRF protection
|
|
110
|
+
final source = node.toSource().toLowerCase();
|
|
111
|
+
|
|
112
|
+
bool hasCsrfProtection = S029CsrfProtectionAnalyzer._csrfIndicators
|
|
113
|
+
.any((i) => source.contains(i));
|
|
114
|
+
|
|
115
|
+
if (!hasCsrfProtection) {
|
|
116
|
+
// Check in surrounding context (headers, options, etc.)
|
|
117
|
+
AstNode? current = node.parent;
|
|
118
|
+
int depth = 0;
|
|
119
|
+
|
|
120
|
+
while (current != null && depth < 5) {
|
|
121
|
+
final parentSource = current.toSource().toLowerCase();
|
|
122
|
+
if (S029CsrfProtectionAnalyzer._csrfIndicators.any((i) => parentSource.contains(i))) {
|
|
123
|
+
hasCsrfProtection = true;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
current = current.parent;
|
|
127
|
+
depth++;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!hasCsrfProtection) {
|
|
131
|
+
violations.add(analyzer.createViolation(
|
|
132
|
+
filePath: filePath,
|
|
133
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
134
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
135
|
+
message: 'CSRF protection may be missing for HTTP ${methodName.toUpperCase()} request',
|
|
136
|
+
));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
super.visitMethodInvocation(node);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@override
|
|
144
|
+
void visitAnnotation(Annotation node) {
|
|
145
|
+
final name = node.name.toSource().toLowerCase();
|
|
146
|
+
|
|
147
|
+
// Check for route annotations with state-changing methods
|
|
148
|
+
if (name.contains('route') || name.contains('mapping')) {
|
|
149
|
+
final source = node.toSource().toLowerCase();
|
|
150
|
+
|
|
151
|
+
bool isStateChanging = S029CsrfProtectionAnalyzer._httpMethods
|
|
152
|
+
.any((m) => source.contains("'$m'") || source.contains('"$m"'));
|
|
153
|
+
|
|
154
|
+
if (isStateChanging) {
|
|
155
|
+
bool hasCsrfProtection = S029CsrfProtectionAnalyzer._csrfIndicators
|
|
156
|
+
.any((i) => source.contains(i));
|
|
157
|
+
|
|
158
|
+
if (!hasCsrfProtection) {
|
|
159
|
+
violations.add(analyzer.createViolation(
|
|
160
|
+
filePath: filePath,
|
|
161
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
162
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
163
|
+
message: 'Route with state-changing method should have CSRF protection',
|
|
164
|
+
));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
super.visitAnnotation(node);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import 'package:analyzer/dart/ast/ast.dart';
|
|
2
|
+
import 'package:analyzer/dart/ast/visitor.dart';
|
|
3
|
+
import 'package:analyzer/source/line_info.dart';
|
|
4
|
+
|
|
5
|
+
import '../../models/rule.dart';
|
|
6
|
+
import '../../models/violation.dart';
|
|
7
|
+
import '../base_analyzer.dart';
|
|
8
|
+
|
|
9
|
+
/// S030: Directory Browsing Protection
|
|
10
|
+
/// Prevent directory listing/browsing attacks on web servers
|
|
11
|
+
class S030DirectoryBrowsingProtectionAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'S030';
|
|
14
|
+
|
|
15
|
+
// Actual static file serving methods in web frameworks
|
|
16
|
+
static const _webServerMethods = [
|
|
17
|
+
'createstatichandler', 'staticfilehandler', 'statichandler',
|
|
18
|
+
'servedir', 'serve_dir', 'servestaticfiles',
|
|
19
|
+
'staticfilemiddleware', 'staticfiles',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
// Web server frameworks/packages
|
|
23
|
+
static const _webServerPackages = [
|
|
24
|
+
'shelf_static', 'shelf', 'dart_frog', 'angel',
|
|
25
|
+
'conduit', 'aqueduct', 'express',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
@override
|
|
29
|
+
List<Violation> analyze({
|
|
30
|
+
required CompilationUnit unit,
|
|
31
|
+
required String filePath,
|
|
32
|
+
required Rule rule,
|
|
33
|
+
required LineInfo lineInfo,
|
|
34
|
+
}) {
|
|
35
|
+
// Only analyze files that are likely web server related
|
|
36
|
+
final fileName = filePath.toLowerCase();
|
|
37
|
+
final isWebServerFile = fileName.contains('server') ||
|
|
38
|
+
fileName.contains('handler') ||
|
|
39
|
+
fileName.contains('middleware') ||
|
|
40
|
+
fileName.contains('route') ||
|
|
41
|
+
fileName.contains('api');
|
|
42
|
+
|
|
43
|
+
// Skip mobile/Flutter UI files
|
|
44
|
+
if (fileName.contains('screen') ||
|
|
45
|
+
fileName.contains('widget') ||
|
|
46
|
+
fileName.contains('view') ||
|
|
47
|
+
fileName.contains('page') ||
|
|
48
|
+
fileName.contains('notifier') ||
|
|
49
|
+
fileName.contains('provider') ||
|
|
50
|
+
fileName.contains('state')) {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
final violations = <Violation>[];
|
|
55
|
+
final visitor = _S030Visitor(
|
|
56
|
+
filePath: filePath,
|
|
57
|
+
lineInfo: lineInfo,
|
|
58
|
+
violations: violations,
|
|
59
|
+
analyzer: this,
|
|
60
|
+
);
|
|
61
|
+
unit.accept(visitor);
|
|
62
|
+
return violations;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
class _S030Visitor extends RecursiveAstVisitor<void> {
|
|
67
|
+
final String filePath;
|
|
68
|
+
final LineInfo lineInfo;
|
|
69
|
+
final List<Violation> violations;
|
|
70
|
+
final S030DirectoryBrowsingProtectionAnalyzer analyzer;
|
|
71
|
+
|
|
72
|
+
_S030Visitor({
|
|
73
|
+
required this.filePath,
|
|
74
|
+
required this.lineInfo,
|
|
75
|
+
required this.violations,
|
|
76
|
+
required this.analyzer,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
@override
|
|
80
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
81
|
+
final methodName = node.methodName.name.toLowerCase();
|
|
82
|
+
final source = node.toSource().toLowerCase();
|
|
83
|
+
|
|
84
|
+
// Only check actual web server static file serving methods
|
|
85
|
+
bool isWebServerStaticServe = S030DirectoryBrowsingProtectionAnalyzer._webServerMethods
|
|
86
|
+
.any((m) => methodName.contains(m) || source.contains(m));
|
|
87
|
+
|
|
88
|
+
// Also check if using web server packages
|
|
89
|
+
bool usesWebServerPackage = S030DirectoryBrowsingProtectionAnalyzer._webServerPackages
|
|
90
|
+
.any((p) => source.contains(p));
|
|
91
|
+
|
|
92
|
+
if (isWebServerStaticServe || usesWebServerPackage) {
|
|
93
|
+
// Check for listing/browsing configuration
|
|
94
|
+
bool hasListingConfig = source.contains('listdirectory') ||
|
|
95
|
+
source.contains('list_directory') ||
|
|
96
|
+
source.contains('directorylisting') ||
|
|
97
|
+
source.contains('directory_listing') ||
|
|
98
|
+
source.contains('allowlisting') ||
|
|
99
|
+
source.contains('allow_listing');
|
|
100
|
+
|
|
101
|
+
// Check if listing is explicitly enabled
|
|
102
|
+
if (hasListingConfig && source.contains('true')) {
|
|
103
|
+
violations.add(analyzer.createViolation(
|
|
104
|
+
filePath: filePath,
|
|
105
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
106
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
107
|
+
message: 'Directory listing enabled on static file server - security risk',
|
|
108
|
+
));
|
|
109
|
+
} else if (!hasListingConfig && isWebServerStaticServe) {
|
|
110
|
+
// Only warn if it's definitely a static file handler without config
|
|
111
|
+
violations.add(analyzer.createViolation(
|
|
112
|
+
filePath: filePath,
|
|
113
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
114
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
115
|
+
message: 'Static file handler - ensure directory listing is disabled',
|
|
116
|
+
));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
super.visitMethodInvocation(node);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@override
|
|
124
|
+
void visitNamedExpression(NamedExpression node) {
|
|
125
|
+
final paramName = node.name.label.name.toLowerCase();
|
|
126
|
+
|
|
127
|
+
// Check for directory listing parameter in web server context
|
|
128
|
+
if (paramName.contains('listing') || paramName.contains('browse')) {
|
|
129
|
+
if (node.expression is BooleanLiteral) {
|
|
130
|
+
final value = (node.expression as BooleanLiteral).value;
|
|
131
|
+
if (value) {
|
|
132
|
+
violations.add(analyzer.createViolation(
|
|
133
|
+
filePath: filePath,
|
|
134
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
135
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
136
|
+
message: 'Directory listing enabled - security risk',
|
|
137
|
+
));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
super.visitNamedExpression(node);
|
|
143
|
+
}
|
|
144
|
+
}
|