@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,315 @@
|
|
|
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
|
+
/// S015: Insecure TLS Certificate
|
|
10
|
+
/// Only accept trusted TLS certificates and reject weak ciphers
|
|
11
|
+
/// - Do not accept untrusted/self-signed certificates automatically
|
|
12
|
+
/// - Disable weak cipher suites (RC4, 3DES, NULL, MD5, SHA1)
|
|
13
|
+
/// - Use modern ciphers (AES-GCM, ChaCha20-Poly1305, TLS_ECDHE)
|
|
14
|
+
class S015InsecureTlsCertificateAnalyzer extends BaseAnalyzer {
|
|
15
|
+
@override
|
|
16
|
+
String get ruleId => 'S015';
|
|
17
|
+
|
|
18
|
+
// Patterns indicating certificate verification bypass
|
|
19
|
+
static const _insecureCertPatterns = [
|
|
20
|
+
'badcertificatecallback',
|
|
21
|
+
'allowbadcertificates',
|
|
22
|
+
'trustbadcertificate',
|
|
23
|
+
'onbadcertificate',
|
|
24
|
+
'allowselfsigned',
|
|
25
|
+
'allow_self_signed',
|
|
26
|
+
'skipverify',
|
|
27
|
+
'skip_verify',
|
|
28
|
+
'verifycertificate: false',
|
|
29
|
+
'verify_certificate: false',
|
|
30
|
+
'ignorecertificate',
|
|
31
|
+
'ignore_certificate',
|
|
32
|
+
'trustallcertificates',
|
|
33
|
+
'trust_all_certificates',
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// Weak cipher patterns
|
|
37
|
+
static const _weakCipherPatterns = [
|
|
38
|
+
'rc4',
|
|
39
|
+
'des',
|
|
40
|
+
'3des',
|
|
41
|
+
'tripledes',
|
|
42
|
+
'null_cipher',
|
|
43
|
+
'nullcipher',
|
|
44
|
+
'export_cipher',
|
|
45
|
+
'exportcipher',
|
|
46
|
+
'md5',
|
|
47
|
+
'sha1',
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
// Insecure TLS versions
|
|
51
|
+
static const _insecureTlsVersions = [
|
|
52
|
+
'sslv2',
|
|
53
|
+
'sslv3',
|
|
54
|
+
'ssl_v2',
|
|
55
|
+
'ssl_v3',
|
|
56
|
+
'tlsv1.0',
|
|
57
|
+
'tlsv1_0',
|
|
58
|
+
'tls_v1_0',
|
|
59
|
+
'tlsv1.1',
|
|
60
|
+
'tlsv1_1',
|
|
61
|
+
'tls_v1_1',
|
|
62
|
+
'tls1.0',
|
|
63
|
+
'tls1.1',
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
@override
|
|
67
|
+
List<Violation> analyze({
|
|
68
|
+
required CompilationUnit unit,
|
|
69
|
+
required String filePath,
|
|
70
|
+
required Rule rule,
|
|
71
|
+
required LineInfo lineInfo,
|
|
72
|
+
}) {
|
|
73
|
+
final violations = <Violation>[];
|
|
74
|
+
final visitor = _S015Visitor(
|
|
75
|
+
filePath: filePath,
|
|
76
|
+
lineInfo: lineInfo,
|
|
77
|
+
violations: violations,
|
|
78
|
+
analyzer: this,
|
|
79
|
+
);
|
|
80
|
+
unit.accept(visitor);
|
|
81
|
+
return violations;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
class _S015Visitor extends RecursiveAstVisitor<void> {
|
|
86
|
+
final String filePath;
|
|
87
|
+
final LineInfo lineInfo;
|
|
88
|
+
final List<Violation> violations;
|
|
89
|
+
final S015InsecureTlsCertificateAnalyzer analyzer;
|
|
90
|
+
|
|
91
|
+
_S015Visitor({
|
|
92
|
+
required this.filePath,
|
|
93
|
+
required this.lineInfo,
|
|
94
|
+
required this.violations,
|
|
95
|
+
required this.analyzer,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
@override
|
|
99
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
100
|
+
final source = node.toSource().toLowerCase();
|
|
101
|
+
final methodName = node.methodName.name.toLowerCase();
|
|
102
|
+
|
|
103
|
+
// Check for certificate verification bypass
|
|
104
|
+
_checkInsecureCertPatterns(source, node.offset);
|
|
105
|
+
|
|
106
|
+
// Check for weak ciphers in method calls
|
|
107
|
+
_checkWeakCiphers(source, node.offset);
|
|
108
|
+
|
|
109
|
+
// Check for insecure TLS versions
|
|
110
|
+
_checkInsecureTlsVersions(source, node.offset);
|
|
111
|
+
|
|
112
|
+
// Check for badCertificateCallback that always returns true
|
|
113
|
+
if (methodName == 'badcertificatecallback' ||
|
|
114
|
+
source.contains('onbadcertificate')) {
|
|
115
|
+
// Check if callback returns true (accepts all certs)
|
|
116
|
+
if (source.contains('=> true') ||
|
|
117
|
+
source.contains('return true') ||
|
|
118
|
+
source.contains('(_) => true') ||
|
|
119
|
+
source.contains('(_, __, ___) => true')) {
|
|
120
|
+
violations.add(analyzer.createViolation(
|
|
121
|
+
filePath: filePath,
|
|
122
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
123
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
124
|
+
message:
|
|
125
|
+
'Certificate callback accepts all certificates - only trust valid certificates from trusted CAs',
|
|
126
|
+
));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
super.visitMethodInvocation(node);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@override
|
|
134
|
+
void visitNamedExpression(NamedExpression node) {
|
|
135
|
+
final paramName = node.name.label.name.toLowerCase();
|
|
136
|
+
final expression = node.expression;
|
|
137
|
+
final source = node.toSource().toLowerCase();
|
|
138
|
+
|
|
139
|
+
// Check for certificate verification parameters set to false
|
|
140
|
+
if (_isCertVerificationParam(paramName)) {
|
|
141
|
+
if (expression is BooleanLiteral && !expression.value) {
|
|
142
|
+
violations.add(analyzer.createViolation(
|
|
143
|
+
filePath: filePath,
|
|
144
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
145
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
146
|
+
message:
|
|
147
|
+
'Certificate verification disabled - this allows man-in-the-middle attacks',
|
|
148
|
+
));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check for badCertificateCallback parameter
|
|
153
|
+
if (paramName.contains('badcertificate') ||
|
|
154
|
+
paramName.contains('onbadcertificate')) {
|
|
155
|
+
if (source.contains('=> true') ||
|
|
156
|
+
source.contains('(_) => true') ||
|
|
157
|
+
source.contains('return true')) {
|
|
158
|
+
violations.add(analyzer.createViolation(
|
|
159
|
+
filePath: filePath,
|
|
160
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
161
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
162
|
+
message:
|
|
163
|
+
'Bad certificate callback accepts all certificates - only trust valid certificates',
|
|
164
|
+
));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Check for weak ciphers in parameters
|
|
169
|
+
_checkWeakCiphers(source, node.offset);
|
|
170
|
+
|
|
171
|
+
// Check for insecure TLS versions in parameters
|
|
172
|
+
_checkInsecureTlsVersions(source, node.offset);
|
|
173
|
+
|
|
174
|
+
super.visitNamedExpression(node);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@override
|
|
178
|
+
void visitInstanceCreationExpression(InstanceCreationExpression node) {
|
|
179
|
+
final source = node.toSource().toLowerCase();
|
|
180
|
+
final typeName = node.constructorName.type.name2.lexeme.toLowerCase();
|
|
181
|
+
|
|
182
|
+
// Check SecurityContext or HttpClient configuration
|
|
183
|
+
if (typeName == 'securitycontext' ||
|
|
184
|
+
typeName == 'httpclient' ||
|
|
185
|
+
typeName == 'httpsclient') {
|
|
186
|
+
_checkInsecureCertPatterns(source, node.offset);
|
|
187
|
+
_checkWeakCiphers(source, node.offset);
|
|
188
|
+
_checkInsecureTlsVersions(source, node.offset);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
super.visitInstanceCreationExpression(node);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
@override
|
|
195
|
+
void visitSimpleStringLiteral(SimpleStringLiteral node) {
|
|
196
|
+
final value = node.value.toLowerCase();
|
|
197
|
+
|
|
198
|
+
// Check for weak cipher strings
|
|
199
|
+
_checkWeakCiphers(value, node.offset);
|
|
200
|
+
|
|
201
|
+
// Check for insecure TLS version strings
|
|
202
|
+
_checkInsecureTlsVersions(value, node.offset);
|
|
203
|
+
|
|
204
|
+
super.visitSimpleStringLiteral(node);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
@override
|
|
208
|
+
void visitFunctionExpression(FunctionExpression node) {
|
|
209
|
+
// Check if this is a callback that always returns true (bad certificate handler)
|
|
210
|
+
final parent = node.parent;
|
|
211
|
+
if (parent is NamedExpression) {
|
|
212
|
+
final paramName = parent.name.label.name.toLowerCase();
|
|
213
|
+
if (paramName.contains('badcertificate') ||
|
|
214
|
+
paramName.contains('certificate')) {
|
|
215
|
+
final body = node.body;
|
|
216
|
+
if (body != null) {
|
|
217
|
+
final bodySource = body.toSource().toLowerCase();
|
|
218
|
+
if (bodySource.contains('return true') ||
|
|
219
|
+
bodySource == '=> true' ||
|
|
220
|
+
bodySource.contains('=> true')) {
|
|
221
|
+
violations.add(analyzer.createViolation(
|
|
222
|
+
filePath: filePath,
|
|
223
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
224
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
225
|
+
message:
|
|
226
|
+
'Certificate callback blindly trusts all certificates - validate certificates properly',
|
|
227
|
+
));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
super.visitFunctionExpression(node);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
bool _isCertVerificationParam(String paramName) {
|
|
237
|
+
return paramName.contains('verify') ||
|
|
238
|
+
paramName.contains('validate') ||
|
|
239
|
+
paramName.contains('check') && paramName.contains('cert') ||
|
|
240
|
+
paramName.contains('ssl') ||
|
|
241
|
+
paramName.contains('tls') ||
|
|
242
|
+
paramName == 'secure';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
void _checkInsecureCertPatterns(String source, int offset) {
|
|
246
|
+
for (final pattern
|
|
247
|
+
in S015InsecureTlsCertificateAnalyzer._insecureCertPatterns) {
|
|
248
|
+
if (source.contains(pattern.replaceAll('_', ''))) {
|
|
249
|
+
violations.add(analyzer.createViolation(
|
|
250
|
+
filePath: filePath,
|
|
251
|
+
line: analyzer.getLine(lineInfo, offset),
|
|
252
|
+
column: analyzer.getColumn(lineInfo, offset),
|
|
253
|
+
message:
|
|
254
|
+
'Insecure certificate handling detected - only trust valid certificates from trusted CAs',
|
|
255
|
+
));
|
|
256
|
+
return; // Avoid duplicate violations
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
void _checkWeakCiphers(String source, int offset) {
|
|
262
|
+
// Only flag if in crypto/cipher context
|
|
263
|
+
if (!_isInCipherContext(source)) return;
|
|
264
|
+
|
|
265
|
+
for (final cipher
|
|
266
|
+
in S015InsecureTlsCertificateAnalyzer._weakCipherPatterns) {
|
|
267
|
+
if (source.contains(cipher)) {
|
|
268
|
+
violations.add(analyzer.createViolation(
|
|
269
|
+
filePath: filePath,
|
|
270
|
+
line: analyzer.getLine(lineInfo, offset),
|
|
271
|
+
column: analyzer.getColumn(lineInfo, offset),
|
|
272
|
+
message:
|
|
273
|
+
'Weak cipher "$cipher" detected - use AES-GCM, ChaCha20-Poly1305, or other modern ciphers',
|
|
274
|
+
));
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
void _checkInsecureTlsVersions(String source, int offset) {
|
|
281
|
+
// Only flag if in TLS/SSL context
|
|
282
|
+
if (!_isInTlsContext(source)) return;
|
|
283
|
+
|
|
284
|
+
for (final version
|
|
285
|
+
in S015InsecureTlsCertificateAnalyzer._insecureTlsVersions) {
|
|
286
|
+
if (source.contains(version)) {
|
|
287
|
+
violations.add(analyzer.createViolation(
|
|
288
|
+
filePath: filePath,
|
|
289
|
+
line: analyzer.getLine(lineInfo, offset),
|
|
290
|
+
column: analyzer.getColumn(lineInfo, offset),
|
|
291
|
+
message:
|
|
292
|
+
'Insecure TLS version detected - use TLS 1.2 or TLS 1.3 only',
|
|
293
|
+
));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
bool _isInCipherContext(String source) {
|
|
300
|
+
return source.contains('cipher') ||
|
|
301
|
+
source.contains('encrypt') ||
|
|
302
|
+
source.contains('ssl') ||
|
|
303
|
+
source.contains('tls') ||
|
|
304
|
+
source.contains('security');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
bool _isInTlsContext(String source) {
|
|
308
|
+
return source.contains('tls') ||
|
|
309
|
+
source.contains('ssl') ||
|
|
310
|
+
source.contains('https') ||
|
|
311
|
+
source.contains('security') ||
|
|
312
|
+
source.contains('protocol') ||
|
|
313
|
+
source.contains('version');
|
|
314
|
+
}
|
|
315
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
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
|
+
/// S016: No Sensitive Data in Query Strings
|
|
10
|
+
/// Sensitive data should not be passed in URL query strings
|
|
11
|
+
class S016NoSensitiveQuerystringAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'S016';
|
|
14
|
+
|
|
15
|
+
// Sensitive parameter names (truly sensitive data only)
|
|
16
|
+
static const _sensitiveParams = [
|
|
17
|
+
'password', 'passwd', 'pwd', 'secret', 'apikey', 'api_key',
|
|
18
|
+
'accesskey', 'access_key', 'sessionid', 'session_id',
|
|
19
|
+
'credit_card', 'creditcard', 'cardnumber', 'card_number', 'cvv', 'ssn',
|
|
20
|
+
'otp', 'pin', 'credential', 'bearer', 'private_key',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
// Non-sensitive keys that may contain "key" or "token" but are not secrets
|
|
24
|
+
// These are business domain identifiers, not security tokens
|
|
25
|
+
static const _nonSensitiveKeys = [
|
|
26
|
+
'objective_key', 'objectivekey', // Business objective identifier
|
|
27
|
+
'device_token', 'devicetoken', // Push notification token (not auth)
|
|
28
|
+
'fcm_token', 'fcmtoken', // Firebase Cloud Messaging token
|
|
29
|
+
'apns_token', 'apnstoken', // Apple Push Notification token
|
|
30
|
+
'primary_key', 'primarykey', // Database key
|
|
31
|
+
'foreign_key', 'foreignkey', // Database key
|
|
32
|
+
'sort_key', 'sortkey', // Sorting key
|
|
33
|
+
'cache_key', 'cachekey', // Cache identifier
|
|
34
|
+
'lookup_key', 'lookupkey', // Lookup identifier
|
|
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
|
+
final violations = <Violation>[];
|
|
45
|
+
final visitor = _S016Visitor(
|
|
46
|
+
filePath: filePath,
|
|
47
|
+
lineInfo: lineInfo,
|
|
48
|
+
violations: violations,
|
|
49
|
+
analyzer: this,
|
|
50
|
+
);
|
|
51
|
+
unit.accept(visitor);
|
|
52
|
+
return violations;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class _S016Visitor extends RecursiveAstVisitor<void> {
|
|
57
|
+
final String filePath;
|
|
58
|
+
final LineInfo lineInfo;
|
|
59
|
+
final List<Violation> violations;
|
|
60
|
+
final S016NoSensitiveQuerystringAnalyzer analyzer;
|
|
61
|
+
|
|
62
|
+
_S016Visitor({
|
|
63
|
+
required this.filePath,
|
|
64
|
+
required this.lineInfo,
|
|
65
|
+
required this.violations,
|
|
66
|
+
required this.analyzer,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
@override
|
|
70
|
+
void visitSimpleStringLiteral(SimpleStringLiteral node) {
|
|
71
|
+
final value = node.value.toLowerCase();
|
|
72
|
+
|
|
73
|
+
// Check for URLs with sensitive query params
|
|
74
|
+
if (value.contains('?') || value.contains('&')) {
|
|
75
|
+
for (final param in S016NoSensitiveQuerystringAnalyzer._sensitiveParams) {
|
|
76
|
+
// Match patterns like ?password= or &token=
|
|
77
|
+
if (RegExp('[?&]$param=').hasMatch(value)) {
|
|
78
|
+
violations.add(analyzer.createViolation(
|
|
79
|
+
filePath: filePath,
|
|
80
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
81
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
82
|
+
message: 'Sensitive data "$param" should not be in query string - use POST body or headers',
|
|
83
|
+
));
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
super.visitSimpleStringLiteral(node);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@override
|
|
93
|
+
void visitStringInterpolation(StringInterpolation node) {
|
|
94
|
+
final source = node.toSource().toLowerCase();
|
|
95
|
+
|
|
96
|
+
// Check for query string patterns with sensitive data
|
|
97
|
+
if (source.contains('?') || source.contains('&')) {
|
|
98
|
+
for (final param in S016NoSensitiveQuerystringAnalyzer._sensitiveParams) {
|
|
99
|
+
if (source.contains('$param=') || source.contains('${param}=')) {
|
|
100
|
+
violations.add(analyzer.createViolation(
|
|
101
|
+
filePath: filePath,
|
|
102
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
103
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
104
|
+
message: 'Sensitive data should not be in query string - use POST body or headers',
|
|
105
|
+
));
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
super.visitStringInterpolation(node);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@override
|
|
115
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
116
|
+
final methodName = node.methodName.name.toLowerCase();
|
|
117
|
+
|
|
118
|
+
// Check for Uri construction with sensitive query parameters
|
|
119
|
+
if (methodName == 'queryparameters' || methodName == 'replace') {
|
|
120
|
+
final source = node.toSource().toLowerCase();
|
|
121
|
+
for (final param in S016NoSensitiveQuerystringAnalyzer._sensitiveParams) {
|
|
122
|
+
if (source.contains("'$param'") || source.contains('"$param"')) {
|
|
123
|
+
violations.add(analyzer.createViolation(
|
|
124
|
+
filePath: filePath,
|
|
125
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
126
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
127
|
+
message: 'Sensitive parameter "$param" should not be in query parameters',
|
|
128
|
+
));
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
super.visitMethodInvocation(node);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@override
|
|
138
|
+
void visitMapLiteralEntry(MapLiteralEntry node) {
|
|
139
|
+
final key = node.key.toSource().toLowerCase().replaceAll("'", '').replaceAll('"', '');
|
|
140
|
+
|
|
141
|
+
// Skip non-sensitive keys (business identifiers, not secrets)
|
|
142
|
+
bool isNonSensitive = S016NoSensitiveQuerystringAnalyzer._nonSensitiveKeys
|
|
143
|
+
.any((p) => key.contains(p.replaceAll('_', '')));
|
|
144
|
+
|
|
145
|
+
if (isNonSensitive) {
|
|
146
|
+
super.visitMapLiteralEntry(node);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Check if this is a query parameter map entry
|
|
151
|
+
bool isSensitive = S016NoSensitiveQuerystringAnalyzer._sensitiveParams
|
|
152
|
+
.any((p) => key.contains(p));
|
|
153
|
+
|
|
154
|
+
if (isSensitive) {
|
|
155
|
+
// Check parent context to see if it's for query parameters (NOT headers or POST body)
|
|
156
|
+
AstNode? current = node.parent;
|
|
157
|
+
bool isQueryParam = false;
|
|
158
|
+
bool isHeader = false;
|
|
159
|
+
bool isPostBody = false;
|
|
160
|
+
int depth = 0;
|
|
161
|
+
const maxDepth = 15;
|
|
162
|
+
|
|
163
|
+
while (current != null && depth < maxDepth) {
|
|
164
|
+
final source = current.toSource().toLowerCase();
|
|
165
|
+
|
|
166
|
+
// Check if this is a header context - skip these
|
|
167
|
+
if (source.contains('headers') ||
|
|
168
|
+
source.contains('header') ||
|
|
169
|
+
source.contains('authorization')) {
|
|
170
|
+
isHeader = true;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Check if this is a POST/PUT request body (data: {}) - skip these
|
|
175
|
+
// POST body is secure when using HTTPS
|
|
176
|
+
if (current is NamedExpression && current.name.label.name == 'data') {
|
|
177
|
+
final grandParent = current.parent?.parent;
|
|
178
|
+
if (grandParent != null) {
|
|
179
|
+
final gpSource = grandParent.toSource().toLowerCase();
|
|
180
|
+
if (gpSource.contains('.post(') || gpSource.contains('.put(')) {
|
|
181
|
+
isPostBody = true;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Check for variable assignment that is used in POST/PUT request body
|
|
188
|
+
// Pattern: final data = {...}; then _api.post(path, data: data)
|
|
189
|
+
if (current is VariableDeclaration) {
|
|
190
|
+
final varName = current.name.lexeme;
|
|
191
|
+
// Check if parent function/block contains POST/PUT with this variable
|
|
192
|
+
final enclosingScope = _findEnclosingScope(current);
|
|
193
|
+
if (enclosingScope != null) {
|
|
194
|
+
final scopeSource = enclosingScope.toSource().toLowerCase();
|
|
195
|
+
// Check if there's a .post() or .put() call with data: varName
|
|
196
|
+
if ((scopeSource.contains('.post(') || scopeSource.contains('.put(')) &&
|
|
197
|
+
scopeSource.contains('data: ${varName.toLowerCase()}')) {
|
|
198
|
+
isPostBody = true;
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Check if this is a query parameter context
|
|
205
|
+
if (source.contains('queryparameters') ||
|
|
206
|
+
source.contains('query_parameters') ||
|
|
207
|
+
(source.contains('uri(') && !source.contains('headers'))) {
|
|
208
|
+
isQueryParam = true;
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
current = current.parent;
|
|
213
|
+
depth++;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Only report if it's query parameters and NOT headers/POST body
|
|
217
|
+
if (isQueryParam && !isHeader && !isPostBody) {
|
|
218
|
+
violations.add(analyzer.createViolation(
|
|
219
|
+
filePath: filePath,
|
|
220
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
221
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
222
|
+
message: 'Sensitive data "$key" should not be in query parameters',
|
|
223
|
+
));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
super.visitMapLiteralEntry(node);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/// Find the enclosing function or block scope
|
|
231
|
+
AstNode? _findEnclosingScope(AstNode node) {
|
|
232
|
+
AstNode? current = node.parent;
|
|
233
|
+
while (current != null) {
|
|
234
|
+
if (current is FunctionBody ||
|
|
235
|
+
current is MethodDeclaration ||
|
|
236
|
+
current is FunctionDeclaration ||
|
|
237
|
+
current is Block) {
|
|
238
|
+
return current;
|
|
239
|
+
}
|
|
240
|
+
current = current.parent;
|
|
241
|
+
}
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}
|