@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,191 @@
|
|
|
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
|
+
/// S017: Use Parameterized Queries
|
|
10
|
+
/// Detect SQL injection vulnerabilities - use parameterized queries
|
|
11
|
+
class S017UseParameterizedQueriesAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'S017';
|
|
14
|
+
|
|
15
|
+
// SQL statement patterns that strongly indicate a query (not just keywords)
|
|
16
|
+
static const _sqlStatementPatterns = [
|
|
17
|
+
'select * from', 'select ', 'insert into', 'update ',
|
|
18
|
+
'delete from', 'drop table', 'create table', 'alter table',
|
|
19
|
+
'truncate table', 'exec ', 'execute ',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
// Raw query method names in common Dart/Flutter DB packages
|
|
23
|
+
static const _rawQueryMethods = [
|
|
24
|
+
'rawquery', 'rawinsert', 'rawupdate', 'rawdelete',
|
|
25
|
+
'execute', 'query', 'executesql',
|
|
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
|
+
// Skip UI files - they don't have SQL
|
|
36
|
+
final fileName = filePath.toLowerCase();
|
|
37
|
+
if (fileName.contains('screen') ||
|
|
38
|
+
fileName.contains('widget') ||
|
|
39
|
+
fileName.contains('view') ||
|
|
40
|
+
fileName.contains('page') ||
|
|
41
|
+
fileName.contains('component')) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
final violations = <Violation>[];
|
|
46
|
+
final visitor = _S017Visitor(
|
|
47
|
+
filePath: filePath,
|
|
48
|
+
lineInfo: lineInfo,
|
|
49
|
+
violations: violations,
|
|
50
|
+
analyzer: this,
|
|
51
|
+
);
|
|
52
|
+
unit.accept(visitor);
|
|
53
|
+
return violations;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Check if source looks like actual SQL
|
|
57
|
+
static bool looksLikeSql(String source) {
|
|
58
|
+
final lowerSource = source.toLowerCase();
|
|
59
|
+
return _sqlStatementPatterns.any((p) => lowerSource.contains(p));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
class _S017Visitor extends RecursiveAstVisitor<void> {
|
|
64
|
+
final String filePath;
|
|
65
|
+
final LineInfo lineInfo;
|
|
66
|
+
final List<Violation> violations;
|
|
67
|
+
final S017UseParameterizedQueriesAnalyzer analyzer;
|
|
68
|
+
|
|
69
|
+
_S017Visitor({
|
|
70
|
+
required this.filePath,
|
|
71
|
+
required this.lineInfo,
|
|
72
|
+
required this.violations,
|
|
73
|
+
required this.analyzer,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
@override
|
|
77
|
+
void visitStringInterpolation(StringInterpolation node) {
|
|
78
|
+
// Skip if this is inside a logging call - not SQL context
|
|
79
|
+
if (_isInLoggingContext(node)) {
|
|
80
|
+
super.visitStringInterpolation(node);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
final source = node.toSource();
|
|
85
|
+
|
|
86
|
+
// Only check if it looks like actual SQL (not just contains common words)
|
|
87
|
+
if (S017UseParameterizedQueriesAnalyzer.looksLikeSql(source)) {
|
|
88
|
+
// Check if there are interpolation elements
|
|
89
|
+
bool hasInterpolation = node.elements.any((e) => e is InterpolationExpression);
|
|
90
|
+
|
|
91
|
+
if (hasInterpolation) {
|
|
92
|
+
violations.add(analyzer.createViolation(
|
|
93
|
+
filePath: filePath,
|
|
94
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
95
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
96
|
+
message: 'SQL injection risk - use parameterized queries instead of string interpolation',
|
|
97
|
+
));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
super.visitStringInterpolation(node);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// Check if the node is inside a logging/debug context
|
|
105
|
+
bool _isInLoggingContext(AstNode node) {
|
|
106
|
+
AstNode? current = node.parent;
|
|
107
|
+
int depth = 0;
|
|
108
|
+
|
|
109
|
+
while (current != null && depth < 5) {
|
|
110
|
+
if (current is MethodInvocation) {
|
|
111
|
+
final methodName = current.methodName.name.toLowerCase();
|
|
112
|
+
// Logging method patterns
|
|
113
|
+
if (methodName.contains('log') ||
|
|
114
|
+
methodName == 'print' ||
|
|
115
|
+
methodName == 'debugprint' ||
|
|
116
|
+
methodName == 'info' ||
|
|
117
|
+
methodName == 'debug' ||
|
|
118
|
+
methodName == 'warn' ||
|
|
119
|
+
methodName == 'error' ||
|
|
120
|
+
methodName == 'trace') {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
// Check if target is a logging utility
|
|
124
|
+
final target = current.target?.toSource().toLowerCase() ?? '';
|
|
125
|
+
if (target.contains('log') || target.contains('logger')) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
current = current.parent;
|
|
130
|
+
depth++;
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@override
|
|
136
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
137
|
+
final methodName = node.methodName.name.toLowerCase();
|
|
138
|
+
|
|
139
|
+
// Check for raw query methods
|
|
140
|
+
bool isRawQueryMethod = S017UseParameterizedQueriesAnalyzer._rawQueryMethods
|
|
141
|
+
.any((m) => methodName.contains(m));
|
|
142
|
+
|
|
143
|
+
if (isRawQueryMethod && node.argumentList.arguments.isNotEmpty) {
|
|
144
|
+
final firstArg = node.argumentList.arguments.first;
|
|
145
|
+
|
|
146
|
+
if (firstArg is StringInterpolation) {
|
|
147
|
+
violations.add(analyzer.createViolation(
|
|
148
|
+
filePath: filePath,
|
|
149
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
150
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
151
|
+
message: 'SQL injection risk in raw query - use parameterized version',
|
|
152
|
+
));
|
|
153
|
+
} else if (firstArg is BinaryExpression &&
|
|
154
|
+
firstArg.operator.type.lexeme == '+') {
|
|
155
|
+
violations.add(analyzer.createViolation(
|
|
156
|
+
filePath: filePath,
|
|
157
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
158
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
159
|
+
message: 'SQL injection risk - avoid string concatenation in raw queries',
|
|
160
|
+
));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
super.visitMethodInvocation(node);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@override
|
|
168
|
+
void visitBinaryExpression(BinaryExpression node) {
|
|
169
|
+
// Only check string concatenation in database-related contexts
|
|
170
|
+
if (node.operator.type.lexeme == '+') {
|
|
171
|
+
final combined = node.toSource();
|
|
172
|
+
|
|
173
|
+
// Only flag if it clearly looks like SQL
|
|
174
|
+
if (S017UseParameterizedQueriesAnalyzer.looksLikeSql(combined)) {
|
|
175
|
+
bool concatenatingVariable = node.rightOperand is SimpleIdentifier ||
|
|
176
|
+
node.rightOperand is PrefixedIdentifier;
|
|
177
|
+
|
|
178
|
+
if (concatenatingVariable) {
|
|
179
|
+
violations.add(analyzer.createViolation(
|
|
180
|
+
filePath: filePath,
|
|
181
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
182
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
183
|
+
message: 'SQL injection risk - use parameterized queries instead of string concatenation',
|
|
184
|
+
));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
super.visitBinaryExpression(node);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
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
|
+
/// S018: Do not store sensitive data in browser storage
|
|
10
|
+
/// Detect sensitive data being stored in localStorage, sessionStorage, IndexedDB
|
|
11
|
+
class S018NoSensitiveBrowserStorageAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'S018';
|
|
14
|
+
|
|
15
|
+
// Browser storage patterns
|
|
16
|
+
static const _browserStoragePatterns = [
|
|
17
|
+
'localstorage',
|
|
18
|
+
'sessionstorage',
|
|
19
|
+
'indexeddb',
|
|
20
|
+
'sharedpreferences', // Flutter equivalent
|
|
21
|
+
'securestorage', // Should use this instead
|
|
22
|
+
'setstring',
|
|
23
|
+
'setitem',
|
|
24
|
+
'put(',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// Sensitive data patterns that should NOT be stored
|
|
28
|
+
static const _sensitiveDataPatterns = [
|
|
29
|
+
'password',
|
|
30
|
+
'passwd',
|
|
31
|
+
'secret',
|
|
32
|
+
'apikey',
|
|
33
|
+
'api_key',
|
|
34
|
+
'accesstoken',
|
|
35
|
+
'access_token',
|
|
36
|
+
'refreshtoken',
|
|
37
|
+
'refresh_token',
|
|
38
|
+
'privatekey',
|
|
39
|
+
'private_key',
|
|
40
|
+
'creditcard',
|
|
41
|
+
'credit_card',
|
|
42
|
+
'cardnumber',
|
|
43
|
+
'card_number',
|
|
44
|
+
'cvv',
|
|
45
|
+
'ssn',
|
|
46
|
+
'socialsecurity',
|
|
47
|
+
'bankaccount',
|
|
48
|
+
'bank_account',
|
|
49
|
+
'encryptionkey',
|
|
50
|
+
'encryption_key',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
// Allowed patterns (session token is acceptable with proper handling)
|
|
54
|
+
static const _allowedPatterns = [
|
|
55
|
+
'encrypted',
|
|
56
|
+
'hashed',
|
|
57
|
+
'securestorage',
|
|
58
|
+
'flutter_secure_storage',
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
@override
|
|
62
|
+
List<Violation> analyze({
|
|
63
|
+
required CompilationUnit unit,
|
|
64
|
+
required String filePath,
|
|
65
|
+
required Rule rule,
|
|
66
|
+
required LineInfo lineInfo,
|
|
67
|
+
}) {
|
|
68
|
+
final violations = <Violation>[];
|
|
69
|
+
final visitor = _S018Visitor(
|
|
70
|
+
filePath: filePath,
|
|
71
|
+
lineInfo: lineInfo,
|
|
72
|
+
violations: violations,
|
|
73
|
+
analyzer: this,
|
|
74
|
+
);
|
|
75
|
+
unit.accept(visitor);
|
|
76
|
+
return violations;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
class _S018Visitor extends RecursiveAstVisitor<void> {
|
|
81
|
+
final String filePath;
|
|
82
|
+
final LineInfo lineInfo;
|
|
83
|
+
final List<Violation> violations;
|
|
84
|
+
final S018NoSensitiveBrowserStorageAnalyzer analyzer;
|
|
85
|
+
|
|
86
|
+
_S018Visitor({
|
|
87
|
+
required this.filePath,
|
|
88
|
+
required this.lineInfo,
|
|
89
|
+
required this.violations,
|
|
90
|
+
required this.analyzer,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
@override
|
|
94
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
95
|
+
final source = node.toSource().toLowerCase();
|
|
96
|
+
final methodName = node.methodName.name.toLowerCase();
|
|
97
|
+
|
|
98
|
+
// Check if this is a storage operation
|
|
99
|
+
bool isStorageOp =
|
|
100
|
+
S018NoSensitiveBrowserStorageAnalyzer._browserStoragePatterns
|
|
101
|
+
.any((p) => source.contains(p) || methodName.contains(p));
|
|
102
|
+
|
|
103
|
+
if (isStorageOp) {
|
|
104
|
+
// Check if storing sensitive data
|
|
105
|
+
bool hasSensitiveData =
|
|
106
|
+
S018NoSensitiveBrowserStorageAnalyzer._sensitiveDataPatterns
|
|
107
|
+
.any((p) => source.contains(p));
|
|
108
|
+
|
|
109
|
+
// Check if using secure storage or encryption
|
|
110
|
+
bool isSecureUsage =
|
|
111
|
+
S018NoSensitiveBrowserStorageAnalyzer._allowedPatterns
|
|
112
|
+
.any((p) => source.contains(p));
|
|
113
|
+
|
|
114
|
+
if (hasSensitiveData && !isSecureUsage) {
|
|
115
|
+
violations.add(analyzer.createViolation(
|
|
116
|
+
filePath: filePath,
|
|
117
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
118
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
119
|
+
message:
|
|
120
|
+
'Sensitive data should not be stored in browser storage - use flutter_secure_storage or server-side sessions',
|
|
121
|
+
));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
super.visitMethodInvocation(node);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@override
|
|
129
|
+
void visitMapLiteralEntry(MapLiteralEntry node) {
|
|
130
|
+
final key = node.key.toSource().toLowerCase();
|
|
131
|
+
final value = node.value.toSource().toLowerCase();
|
|
132
|
+
final fullSource = node.toSource().toLowerCase();
|
|
133
|
+
|
|
134
|
+
// Check if key indicates sensitive data
|
|
135
|
+
bool hasSensitiveKey =
|
|
136
|
+
S018NoSensitiveBrowserStorageAnalyzer._sensitiveDataPatterns
|
|
137
|
+
.any((p) => key.contains(p));
|
|
138
|
+
|
|
139
|
+
if (hasSensitiveKey) {
|
|
140
|
+
// Check context - is this being stored in browser storage?
|
|
141
|
+
bool isStorageContext = _isInStorageContext(node);
|
|
142
|
+
|
|
143
|
+
// Check if encrypted
|
|
144
|
+
bool isSecure = S018NoSensitiveBrowserStorageAnalyzer._allowedPatterns
|
|
145
|
+
.any((p) => fullSource.contains(p));
|
|
146
|
+
|
|
147
|
+
if (isStorageContext && !isSecure) {
|
|
148
|
+
violations.add(analyzer.createViolation(
|
|
149
|
+
filePath: filePath,
|
|
150
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
151
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
152
|
+
message:
|
|
153
|
+
'Sensitive data in storage context - encrypt before storing or use secure storage',
|
|
154
|
+
));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
super.visitMapLiteralEntry(node);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
bool _isInStorageContext(AstNode node) {
|
|
162
|
+
AstNode? current = node.parent;
|
|
163
|
+
int depth = 0;
|
|
164
|
+
while (current != null && depth < 10) {
|
|
165
|
+
final source = current.toSource().toLowerCase();
|
|
166
|
+
if (S018NoSensitiveBrowserStorageAnalyzer._browserStoragePatterns
|
|
167
|
+
.any((p) => source.contains(p))) {
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
current = current.parent;
|
|
171
|
+
depth++;
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
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
|
+
/// S019: SMTP Injection Protection
|
|
10
|
+
/// Prevent SMTP header injection attacks in email functionality
|
|
11
|
+
class S019SmtpInjectionProtectionAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'S019';
|
|
14
|
+
|
|
15
|
+
// Email header fields that can be injected
|
|
16
|
+
// Note: 'x-' removed because it causes false positives with format strings like "index 1-5"
|
|
17
|
+
// X-headers are less critical anyway and often legitimately contain dynamic content
|
|
18
|
+
static const _headerFields = [
|
|
19
|
+
'to:', 'from:', 'cc:', 'bcc:', 'subject:', 'reply-to:',
|
|
20
|
+
'content-type:', 'mime-version:',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
// Dangerous characters for SMTP injection
|
|
24
|
+
static const _dangerousChars = ['\r', '\n', '\r\n', '%0d', '%0a'];
|
|
25
|
+
|
|
26
|
+
@override
|
|
27
|
+
List<Violation> analyze({
|
|
28
|
+
required CompilationUnit unit,
|
|
29
|
+
required String filePath,
|
|
30
|
+
required Rule rule,
|
|
31
|
+
required LineInfo lineInfo,
|
|
32
|
+
}) {
|
|
33
|
+
final violations = <Violation>[];
|
|
34
|
+
final visitor = _S019Visitor(
|
|
35
|
+
filePath: filePath,
|
|
36
|
+
lineInfo: lineInfo,
|
|
37
|
+
violations: violations,
|
|
38
|
+
analyzer: this,
|
|
39
|
+
);
|
|
40
|
+
unit.accept(visitor);
|
|
41
|
+
return violations;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class _S019Visitor extends RecursiveAstVisitor<void> {
|
|
46
|
+
final String filePath;
|
|
47
|
+
final LineInfo lineInfo;
|
|
48
|
+
final List<Violation> violations;
|
|
49
|
+
final S019SmtpInjectionProtectionAnalyzer analyzer;
|
|
50
|
+
|
|
51
|
+
_S019Visitor({
|
|
52
|
+
required this.filePath,
|
|
53
|
+
required this.lineInfo,
|
|
54
|
+
required this.violations,
|
|
55
|
+
required this.analyzer,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
@override
|
|
59
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
60
|
+
final methodName = node.methodName.name.toLowerCase();
|
|
61
|
+
final source = node.toSource().toLowerCase();
|
|
62
|
+
|
|
63
|
+
// Check for email sending methods
|
|
64
|
+
if (methodName.contains('send') &&
|
|
65
|
+
(source.contains('mail') || source.contains('email') || source.contains('smtp'))) {
|
|
66
|
+
// Check if user input is used in email headers without sanitization
|
|
67
|
+
for (final arg in node.argumentList.arguments) {
|
|
68
|
+
if (arg is NamedExpression) {
|
|
69
|
+
final paramName = arg.name.label.name.toLowerCase();
|
|
70
|
+
|
|
71
|
+
// Check if it's a header field
|
|
72
|
+
bool isHeaderField = ['to', 'from', 'cc', 'bcc', 'subject', 'replyto', 'reply_to']
|
|
73
|
+
.any((h) => paramName.contains(h));
|
|
74
|
+
|
|
75
|
+
if (isHeaderField) {
|
|
76
|
+
final value = arg.expression;
|
|
77
|
+
// Check if value comes from user input (simple heuristic)
|
|
78
|
+
if (value is SimpleIdentifier || value is StringInterpolation) {
|
|
79
|
+
violations.add(analyzer.createViolation(
|
|
80
|
+
filePath: filePath,
|
|
81
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
82
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
83
|
+
message: 'SMTP injection risk - sanitize email headers to remove CRLF characters',
|
|
84
|
+
));
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
super.visitMethodInvocation(node);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@override
|
|
96
|
+
void visitStringInterpolation(StringInterpolation node) {
|
|
97
|
+
// Skip debug/logging statements - these are not email contexts
|
|
98
|
+
final parent = node.parent;
|
|
99
|
+
if (parent is ArgumentList) {
|
|
100
|
+
final grandParent = parent.parent;
|
|
101
|
+
if (grandParent is MethodInvocation) {
|
|
102
|
+
final methodName = grandParent.methodName.name.toLowerCase();
|
|
103
|
+
if (methodName == 'debugprint' || methodName == 'print' ||
|
|
104
|
+
methodName == 'log' || methodName.startsWith('log')) {
|
|
105
|
+
super.visitStringInterpolation(node);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
final source = node.toSource().toLowerCase();
|
|
112
|
+
|
|
113
|
+
// Check if building email headers with user input
|
|
114
|
+
// Must be in email-related context, not just any string with header-like patterns
|
|
115
|
+
bool isEmailHeader = S019SmtpInjectionProtectionAnalyzer._headerFields
|
|
116
|
+
.any((h) => source.contains(h));
|
|
117
|
+
|
|
118
|
+
// Additional check: should also have email-related context
|
|
119
|
+
bool isEmailContext = source.contains('email') || source.contains('mail') ||
|
|
120
|
+
source.contains('smtp') || source.contains('message');
|
|
121
|
+
|
|
122
|
+
if (isEmailHeader && isEmailContext) {
|
|
123
|
+
// Check for interpolation (user input)
|
|
124
|
+
bool hasInterpolation = node.elements.any((e) => e is InterpolationExpression);
|
|
125
|
+
|
|
126
|
+
if (hasInterpolation) {
|
|
127
|
+
violations.add(analyzer.createViolation(
|
|
128
|
+
filePath: filePath,
|
|
129
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
130
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
131
|
+
message: 'SMTP header injection risk - sanitize user input in email headers',
|
|
132
|
+
));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
super.visitStringInterpolation(node);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@override
|
|
140
|
+
void visitInstanceCreationExpression(InstanceCreationExpression node) {
|
|
141
|
+
final typeName = node.constructorName.type.name2.lexeme.toLowerCase();
|
|
142
|
+
|
|
143
|
+
// Check for Message/Email class construction
|
|
144
|
+
if (typeName.contains('message') || typeName.contains('email') || typeName.contains('mail')) {
|
|
145
|
+
for (final arg in node.argumentList.arguments) {
|
|
146
|
+
if (arg is NamedExpression) {
|
|
147
|
+
final paramName = arg.name.label.name.toLowerCase();
|
|
148
|
+
final isHeaderField = ['to', 'from', 'cc', 'bcc', 'subject', 'replyto']
|
|
149
|
+
.any((h) => paramName == h);
|
|
150
|
+
|
|
151
|
+
if (isHeaderField && arg.expression is StringInterpolation) {
|
|
152
|
+
violations.add(analyzer.createViolation(
|
|
153
|
+
filePath: filePath,
|
|
154
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
155
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
156
|
+
message: 'SMTP injection risk in email construction - sanitize header values',
|
|
157
|
+
));
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
super.visitInstanceCreationExpression(node);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
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
|
+
/// S020: No Eval or Dynamic Code Execution
|
|
10
|
+
/// Prevent code injection through eval() and similar dynamic code execution
|
|
11
|
+
class S020NoEvalDynamicCodeAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'S020';
|
|
14
|
+
|
|
15
|
+
// Dangerous dynamic code execution methods - use exact match to avoid false positives
|
|
16
|
+
static const _dangerousMethods = [
|
|
17
|
+
'eval', 'exec', 'execute', 'runcode', 'run_code',
|
|
18
|
+
'dynamiccode', 'dynamic_code', 'compile',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
// Check if method name exactly matches or ends with a dangerous pattern
|
|
22
|
+
static bool _isDangerousMethod(String methodName) {
|
|
23
|
+
final lowerName = methodName.toLowerCase().replaceAll('_', '');
|
|
24
|
+
// Exact match
|
|
25
|
+
for (final m in _dangerousMethods) {
|
|
26
|
+
final pattern = m.replaceAll('_', '');
|
|
27
|
+
if (lowerName == pattern) return true;
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Dart-specific dynamic execution patterns
|
|
33
|
+
static const _dartDangerousPatterns = [
|
|
34
|
+
'mirror', 'reflect', 'invoke', 'dynamicinvoke',
|
|
35
|
+
'noSuchMethod', // Can be abused for dynamic invocation
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
@override
|
|
39
|
+
List<Violation> analyze({
|
|
40
|
+
required CompilationUnit unit,
|
|
41
|
+
required String filePath,
|
|
42
|
+
required Rule rule,
|
|
43
|
+
required LineInfo lineInfo,
|
|
44
|
+
}) {
|
|
45
|
+
final violations = <Violation>[];
|
|
46
|
+
final visitor = _S020Visitor(
|
|
47
|
+
filePath: filePath,
|
|
48
|
+
lineInfo: lineInfo,
|
|
49
|
+
violations: violations,
|
|
50
|
+
analyzer: this,
|
|
51
|
+
);
|
|
52
|
+
unit.accept(visitor);
|
|
53
|
+
return violations;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
class _S020Visitor extends RecursiveAstVisitor<void> {
|
|
58
|
+
final String filePath;
|
|
59
|
+
final LineInfo lineInfo;
|
|
60
|
+
final List<Violation> violations;
|
|
61
|
+
final S020NoEvalDynamicCodeAnalyzer analyzer;
|
|
62
|
+
|
|
63
|
+
_S020Visitor({
|
|
64
|
+
required this.filePath,
|
|
65
|
+
required this.lineInfo,
|
|
66
|
+
required this.violations,
|
|
67
|
+
required this.analyzer,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
@override
|
|
71
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
72
|
+
final methodName = node.methodName.name;
|
|
73
|
+
|
|
74
|
+
// Check for dangerous method names - use exact match to avoid false positives
|
|
75
|
+
bool isDangerous = S020NoEvalDynamicCodeAnalyzer._isDangerousMethod(methodName);
|
|
76
|
+
|
|
77
|
+
if (isDangerous) {
|
|
78
|
+
// Check if argument is user input (string interpolation or variable)
|
|
79
|
+
for (final arg in node.argumentList.arguments) {
|
|
80
|
+
if (arg is StringInterpolation || arg is SimpleIdentifier) {
|
|
81
|
+
violations.add(analyzer.createViolation(
|
|
82
|
+
filePath: filePath,
|
|
83
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
84
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
85
|
+
message: 'Dynamic code execution is dangerous - avoid eval/exec with user input',
|
|
86
|
+
));
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check for reflection with user input
|
|
93
|
+
final lowerMethodName = methodName.toLowerCase();
|
|
94
|
+
if (lowerMethodName == 'invoke' || lowerMethodName == 'invokemember') {
|
|
95
|
+
final source = node.toSource().toLowerCase();
|
|
96
|
+
if (source.contains('mirror') || source.contains('reflect')) {
|
|
97
|
+
for (final arg in node.argumentList.arguments) {
|
|
98
|
+
if (arg is StringInterpolation || arg is SimpleIdentifier) {
|
|
99
|
+
violations.add(analyzer.createViolation(
|
|
100
|
+
filePath: filePath,
|
|
101
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
102
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
103
|
+
message: 'Reflection with dynamic input is dangerous - validate input strictly',
|
|
104
|
+
));
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
super.visitMethodInvocation(node);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@override
|
|
115
|
+
void visitImportDirective(ImportDirective node) {
|
|
116
|
+
final uri = node.uri.stringValue?.toLowerCase() ?? '';
|
|
117
|
+
|
|
118
|
+
// Check for mirrors import
|
|
119
|
+
if (uri.contains('mirrors')) {
|
|
120
|
+
violations.add(analyzer.createViolation(
|
|
121
|
+
filePath: filePath,
|
|
122
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
123
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
124
|
+
message: 'dart:mirrors enables reflection - use with caution, can enable code injection',
|
|
125
|
+
));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
super.visitImportDirective(node);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@override
|
|
132
|
+
void visitFunctionExpression(FunctionExpression node) {
|
|
133
|
+
// Check for Function.apply with dynamic arguments
|
|
134
|
+
final parent = node.parent;
|
|
135
|
+
if (parent is MethodInvocation) {
|
|
136
|
+
final methodName = parent.methodName.name.toLowerCase();
|
|
137
|
+
if (methodName == 'apply') {
|
|
138
|
+
violations.add(analyzer.createViolation(
|
|
139
|
+
filePath: filePath,
|
|
140
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
141
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
142
|
+
message: 'Function.apply with dynamic arguments can be dangerous',
|
|
143
|
+
));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
super.visitFunctionExpression(node);
|
|
148
|
+
}
|
|
149
|
+
}
|