@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,159 @@
|
|
|
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
|
+
/// S053: Return generic error messages, hide internal details
|
|
10
|
+
/// Detect exposure of internal details in error responses
|
|
11
|
+
class S053GenericErrorMessagesAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'S053';
|
|
14
|
+
|
|
15
|
+
@override
|
|
16
|
+
List<Violation> analyze({
|
|
17
|
+
required CompilationUnit unit,
|
|
18
|
+
required String filePath,
|
|
19
|
+
required Rule rule,
|
|
20
|
+
required LineInfo lineInfo,
|
|
21
|
+
}) {
|
|
22
|
+
final violations = <Violation>[];
|
|
23
|
+
final visitor = _S053Visitor(
|
|
24
|
+
filePath: filePath,
|
|
25
|
+
lineInfo: lineInfo,
|
|
26
|
+
violations: violations,
|
|
27
|
+
analyzer: this,
|
|
28
|
+
);
|
|
29
|
+
unit.accept(visitor);
|
|
30
|
+
return violations;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
class _S053Visitor extends RecursiveAstVisitor<void> {
|
|
35
|
+
final String filePath;
|
|
36
|
+
final LineInfo lineInfo;
|
|
37
|
+
final List<Violation> violations;
|
|
38
|
+
final S053GenericErrorMessagesAnalyzer analyzer;
|
|
39
|
+
|
|
40
|
+
// Track if we're inside a catch block
|
|
41
|
+
bool _inCatchBlock = false;
|
|
42
|
+
|
|
43
|
+
_S053Visitor({
|
|
44
|
+
required this.filePath,
|
|
45
|
+
required this.lineInfo,
|
|
46
|
+
required this.violations,
|
|
47
|
+
required this.analyzer,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
@override
|
|
51
|
+
void visitCatchClause(CatchClause node) {
|
|
52
|
+
_inCatchBlock = true;
|
|
53
|
+
super.visitCatchClause(node);
|
|
54
|
+
_inCatchBlock = false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@override
|
|
58
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
59
|
+
final methodName = node.methodName.name.toLowerCase();
|
|
60
|
+
final source = node.toSource().toLowerCase();
|
|
61
|
+
|
|
62
|
+
// Check for HTTP response methods that expose error details
|
|
63
|
+
if (_inCatchBlock) {
|
|
64
|
+
// Check for Response or json methods with error details
|
|
65
|
+
if (methodName == 'json' ||
|
|
66
|
+
methodName == 'send' ||
|
|
67
|
+
methodName == 'write') {
|
|
68
|
+
// Check if error stack trace is exposed
|
|
69
|
+
if (source.contains('.stacktrace') || source.contains('.stack')) {
|
|
70
|
+
violations.add(analyzer.createViolation(
|
|
71
|
+
filePath: filePath,
|
|
72
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
73
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
74
|
+
message:
|
|
75
|
+
'Stack trace exposed in response - return generic error message',
|
|
76
|
+
));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check if error message is directly exposed
|
|
80
|
+
if (source.contains('error.message') ||
|
|
81
|
+
source.contains('exception.message') ||
|
|
82
|
+
source.contains('e.message')) {
|
|
83
|
+
violations.add(analyzer.createViolation(
|
|
84
|
+
filePath: filePath,
|
|
85
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
86
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
87
|
+
message:
|
|
88
|
+
'Internal error message exposed - return generic error message',
|
|
89
|
+
));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check for SQL/database details
|
|
93
|
+
if (source.contains('sql') ||
|
|
94
|
+
source.contains('query') ||
|
|
95
|
+
source.contains('database')) {
|
|
96
|
+
violations.add(analyzer.createViolation(
|
|
97
|
+
filePath: filePath,
|
|
98
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
99
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
100
|
+
message: 'SQL/database details may be exposed in response',
|
|
101
|
+
));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Check for toString() on exceptions in response context
|
|
107
|
+
if (methodName == 'tostring' && _inCatchBlock) {
|
|
108
|
+
final target = node.target?.toSource().toLowerCase() ?? '';
|
|
109
|
+
if (target.contains('error') ||
|
|
110
|
+
target.contains('exception') ||
|
|
111
|
+
target == 'e') {
|
|
112
|
+
// Check if this is being sent to client
|
|
113
|
+
AstNode? current = node.parent;
|
|
114
|
+
int depth = 0;
|
|
115
|
+
while (current != null && depth < 5) {
|
|
116
|
+
final parentSource = current.toSource().toLowerCase();
|
|
117
|
+
if (parentSource.contains('response') ||
|
|
118
|
+
parentSource.contains('json') ||
|
|
119
|
+
parentSource.contains('send')) {
|
|
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
|
+
'Exception toString() exposed to client - use generic message',
|
|
126
|
+
));
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
current = current.parent;
|
|
130
|
+
depth++;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
super.visitMethodInvocation(node);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@override
|
|
139
|
+
void visitThrowExpression(ThrowExpression node) {
|
|
140
|
+
final source = node.toSource().toLowerCase();
|
|
141
|
+
|
|
142
|
+
// Check for throwing exceptions with sensitive details
|
|
143
|
+
if (source.contains('file path') ||
|
|
144
|
+
source.contains('directory') ||
|
|
145
|
+
source.contains('connection string') ||
|
|
146
|
+
source.contains('password') ||
|
|
147
|
+
source.contains('secret')) {
|
|
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
|
+
'Exception may contain sensitive details - use generic message',
|
|
154
|
+
));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
super.visitThrowExpression(node);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
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
|
+
/// S054: No Default Accounts
|
|
10
|
+
/// Avoid hardcoded default accounts/credentials
|
|
11
|
+
class S054NoDefaultAccountsAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'S054';
|
|
14
|
+
|
|
15
|
+
// Common default account patterns
|
|
16
|
+
static const _defaultAccountPatterns = [
|
|
17
|
+
'admin', 'administrator', 'root', 'superuser', 'super_user',
|
|
18
|
+
'guest', 'test', 'demo', 'default', 'system',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
// Default password patterns
|
|
22
|
+
static const _defaultPasswordPatterns = [
|
|
23
|
+
'admin', 'password', '123456', 'admin123', 'password123',
|
|
24
|
+
'root', 'guest', 'default', 'changeme', 'test',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
@override
|
|
28
|
+
List<Violation> analyze({
|
|
29
|
+
required CompilationUnit unit,
|
|
30
|
+
required String filePath,
|
|
31
|
+
required Rule rule,
|
|
32
|
+
required LineInfo lineInfo,
|
|
33
|
+
}) {
|
|
34
|
+
final violations = <Violation>[];
|
|
35
|
+
final visitor = _S054Visitor(
|
|
36
|
+
filePath: filePath,
|
|
37
|
+
lineInfo: lineInfo,
|
|
38
|
+
violations: violations,
|
|
39
|
+
analyzer: this,
|
|
40
|
+
);
|
|
41
|
+
unit.accept(visitor);
|
|
42
|
+
return violations;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
class _S054Visitor extends RecursiveAstVisitor<void> {
|
|
47
|
+
final String filePath;
|
|
48
|
+
final LineInfo lineInfo;
|
|
49
|
+
final List<Violation> violations;
|
|
50
|
+
final S054NoDefaultAccountsAnalyzer analyzer;
|
|
51
|
+
|
|
52
|
+
_S054Visitor({
|
|
53
|
+
required this.filePath,
|
|
54
|
+
required this.lineInfo,
|
|
55
|
+
required this.violations,
|
|
56
|
+
required this.analyzer,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
@override
|
|
60
|
+
void visitVariableDeclaration(VariableDeclaration node) {
|
|
61
|
+
final varName = node.name.lexeme.toLowerCase();
|
|
62
|
+
final initializer = node.initializer;
|
|
63
|
+
|
|
64
|
+
// Check for default user/account names
|
|
65
|
+
if ((varName.contains('user') || varName.contains('account') || varName.contains('login')) &&
|
|
66
|
+
varName.contains('default')) {
|
|
67
|
+
if (initializer is SimpleStringLiteral) {
|
|
68
|
+
final value = initializer.value.toLowerCase();
|
|
69
|
+
bool isDefaultAccount = S054NoDefaultAccountsAnalyzer._defaultAccountPatterns
|
|
70
|
+
.any((p) => value == p);
|
|
71
|
+
|
|
72
|
+
if (isDefaultAccount) {
|
|
73
|
+
violations.add(analyzer.createViolation(
|
|
74
|
+
filePath: filePath,
|
|
75
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
76
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
77
|
+
message: 'Avoid hardcoded default account "$value"',
|
|
78
|
+
));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check for default passwords
|
|
84
|
+
if ((varName.contains('password') || varName.contains('pwd')) &&
|
|
85
|
+
varName.contains('default')) {
|
|
86
|
+
if (initializer is SimpleStringLiteral) {
|
|
87
|
+
violations.add(analyzer.createViolation(
|
|
88
|
+
filePath: filePath,
|
|
89
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
90
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
91
|
+
message: 'Hardcoded default password detected',
|
|
92
|
+
));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
super.visitVariableDeclaration(node);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@override
|
|
100
|
+
void visitMapLiteralEntry(MapLiteralEntry node) {
|
|
101
|
+
final key = node.key.toSource().toLowerCase().replaceAll("'", '').replaceAll('"', '');
|
|
102
|
+
final value = node.value;
|
|
103
|
+
|
|
104
|
+
// Check for default credentials in config maps
|
|
105
|
+
if (key.contains('user') || key.contains('account')) {
|
|
106
|
+
if (value is SimpleStringLiteral) {
|
|
107
|
+
final valStr = value.value.toLowerCase();
|
|
108
|
+
bool isDefaultAccount = S054NoDefaultAccountsAnalyzer._defaultAccountPatterns
|
|
109
|
+
.any((p) => valStr == p);
|
|
110
|
+
|
|
111
|
+
if (isDefaultAccount) {
|
|
112
|
+
violations.add(analyzer.createViolation(
|
|
113
|
+
filePath: filePath,
|
|
114
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
115
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
116
|
+
message: 'Default account in configuration',
|
|
117
|
+
));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (key.contains('password') || key.contains('pwd')) {
|
|
123
|
+
if (value is SimpleStringLiteral) {
|
|
124
|
+
final valStr = value.value.toLowerCase();
|
|
125
|
+
bool isDefaultPassword = S054NoDefaultAccountsAnalyzer._defaultPasswordPatterns
|
|
126
|
+
.any((p) => valStr == p);
|
|
127
|
+
|
|
128
|
+
if (isDefaultPassword) {
|
|
129
|
+
violations.add(analyzer.createViolation(
|
|
130
|
+
filePath: filePath,
|
|
131
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
132
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
133
|
+
message: 'Default/weak password in configuration',
|
|
134
|
+
));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
super.visitMapLiteralEntry(node);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,324 @@
|
|
|
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
|
+
/// S055: Content-Type Validation
|
|
10
|
+
/// Validate Content-Type header for uploaded files
|
|
11
|
+
/// Note: This rule should only flag upload implementations,
|
|
12
|
+
/// not callers of upload functions that already validate Content-Type
|
|
13
|
+
class S055ContentTypeValidationAnalyzer extends BaseAnalyzer {
|
|
14
|
+
@override
|
|
15
|
+
String get ruleId => 'S055';
|
|
16
|
+
|
|
17
|
+
// File upload method patterns - HTTP uploads only
|
|
18
|
+
// Note: Local file operations (save to gallery, write to disk) are NOT uploads
|
|
19
|
+
static const _uploadPatterns = [
|
|
20
|
+
'uploadfile', 'upload_file', 'processupload', 'process_upload',
|
|
21
|
+
'handlemultipart', 'handle_multipart', 'saveformdata', 'save_form_data',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// Patterns that are NOT HTTP uploads (local file operations)
|
|
25
|
+
// These should be excluded from Content-Type validation requirement
|
|
26
|
+
static const _localFileOperationPatterns = [
|
|
27
|
+
'imagegallerysaver', // Flutter plugin to save to device gallery
|
|
28
|
+
'gallerysaver', // Save to gallery
|
|
29
|
+
'saveimagetogallery', // Save image to gallery
|
|
30
|
+
'savevideoTogallery', // Save video to gallery
|
|
31
|
+
'filesaver', // File saver plugins
|
|
32
|
+
'savetodisk', // Local disk save
|
|
33
|
+
'writetofile', // Local file write
|
|
34
|
+
'localfilesystem', // Local filesystem operations
|
|
35
|
+
'pathprovider', // Flutter path provider (local storage)
|
|
36
|
+
'gettemporarydirectory', // Temp directory
|
|
37
|
+
'getapplicationdocumentsdirectory', // App documents
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
// Patterns that indicate Content-Type is being validated/set
|
|
41
|
+
static const _contentTypeValidationPatterns = [
|
|
42
|
+
'lookupmimetype', 'lookup_mime_type', // mime package
|
|
43
|
+
'getmimetype', 'get_mime_type',
|
|
44
|
+
'mimetype', 'mime_type',
|
|
45
|
+
'contenttype', 'content_type',
|
|
46
|
+
'mediatype', 'media_type',
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
// Patterns indicating upload is delegated to another function
|
|
50
|
+
static const _uploadDelegationPatterns = [
|
|
51
|
+
'uploadimage', 'upload_image', // Calling uploadImage function
|
|
52
|
+
'uploadfile', 'upload_file',
|
|
53
|
+
'repository', 'usecase', 'use_case',
|
|
54
|
+
'mediarepo', 'media_repo',
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
// Method name patterns that are NOT actual file uploads
|
|
58
|
+
// These methods create/get URLs but don't upload files themselves
|
|
59
|
+
static const _nonUploadMethodPatterns = [
|
|
60
|
+
'createuploadurl', 'create_upload_url',
|
|
61
|
+
'getuploadurl', 'get_upload_url',
|
|
62
|
+
'generateuploadurl', 'generate_upload_url',
|
|
63
|
+
'presignedurl', 'presigned_url',
|
|
64
|
+
'uploadurl', 'upload_url', // Methods that return URLs, not upload
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
// Client-side upload patterns that automatically handle Content-Type
|
|
68
|
+
// Dio's MultipartFile.fromFile() automatically infers Content-Type from file extension
|
|
69
|
+
// These don't need explicit Content-Type validation in client code
|
|
70
|
+
static const _clientSideUploadPatterns = [
|
|
71
|
+
'multipartfile.fromfile', // Dio: auto-detects Content-Type from extension
|
|
72
|
+
'multipartfile.frombytes', // Dio: requires explicit contentType but often inferred
|
|
73
|
+
'multipartfile.fromstream', // Dio: similar auto-detection
|
|
74
|
+
'formdata.frommap', // Dio FormData constructor
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
@override
|
|
78
|
+
List<Violation> analyze({
|
|
79
|
+
required CompilationUnit unit,
|
|
80
|
+
required String filePath,
|
|
81
|
+
required Rule rule,
|
|
82
|
+
required LineInfo lineInfo,
|
|
83
|
+
}) {
|
|
84
|
+
final violations = <Violation>[];
|
|
85
|
+
final visitor = _S055Visitor(
|
|
86
|
+
filePath: filePath,
|
|
87
|
+
lineInfo: lineInfo,
|
|
88
|
+
violations: violations,
|
|
89
|
+
analyzer: this,
|
|
90
|
+
);
|
|
91
|
+
unit.accept(visitor);
|
|
92
|
+
return violations;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
class _S055Visitor extends RecursiveAstVisitor<void> {
|
|
97
|
+
final String filePath;
|
|
98
|
+
final LineInfo lineInfo;
|
|
99
|
+
final List<Violation> violations;
|
|
100
|
+
final S055ContentTypeValidationAnalyzer analyzer;
|
|
101
|
+
|
|
102
|
+
_S055Visitor({
|
|
103
|
+
required this.filePath,
|
|
104
|
+
required this.lineInfo,
|
|
105
|
+
required this.violations,
|
|
106
|
+
required this.analyzer,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Patterns to detect content type checks - with word boundaries
|
|
110
|
+
static final _contentTypePatterns = [
|
|
111
|
+
RegExp(r'\bcontent[-_]?type\b', caseSensitive: false),
|
|
112
|
+
RegExp(r'\bcontenttype\b', caseSensitive: false),
|
|
113
|
+
RegExp(r'\bmime[-_]?type\b', caseSensitive: false),
|
|
114
|
+
RegExp(r'\bmimetype\b', caseSensitive: false),
|
|
115
|
+
RegExp(r'\bmedia[-_]?type\b', caseSensitive: false),
|
|
116
|
+
RegExp(r'\bmediatype\b', caseSensitive: false),
|
|
117
|
+
RegExp(r'\blookupmimetype\b', caseSensitive: false),
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
bool _hasContentTypeCheck(AstNode node) {
|
|
121
|
+
AstNode? current = node.parent;
|
|
122
|
+
int depth = 0;
|
|
123
|
+
|
|
124
|
+
// Only check immediate context (within 5 levels), not class-level
|
|
125
|
+
while (current != null && depth < 5) {
|
|
126
|
+
// Skip checking class declarations - too broad
|
|
127
|
+
if (current is ClassDeclaration) {
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
final parentSource = current.toSource().toLowerCase();
|
|
132
|
+
for (final pattern in _contentTypePatterns) {
|
|
133
|
+
if (pattern.hasMatch(parentSource)) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
current = current.parent;
|
|
138
|
+
depth++;
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check if the method body delegates to a function that handles content-type
|
|
144
|
+
bool _delegatesToValidatedUpload(AstNode node) {
|
|
145
|
+
// Get the containing method body
|
|
146
|
+
AstNode? current = node;
|
|
147
|
+
while (current != null && current is! MethodDeclaration && current is! FunctionDeclaration) {
|
|
148
|
+
current = current.parent;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (current == null) return false;
|
|
152
|
+
|
|
153
|
+
String bodySource;
|
|
154
|
+
if (current is MethodDeclaration) {
|
|
155
|
+
bodySource = current.body?.toSource().toLowerCase().replaceAll('_', '') ?? '';
|
|
156
|
+
} else if (current is FunctionDeclaration) {
|
|
157
|
+
bodySource = current.functionExpression.body?.toSource().toLowerCase().replaceAll('_', '') ?? '';
|
|
158
|
+
} else {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Check if calling a function that validates content-type
|
|
163
|
+
return S055ContentTypeValidationAnalyzer._uploadDelegationPatterns
|
|
164
|
+
.any((p) => bodySource.contains(p.replaceAll('_', '')));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Check if the containing method is a URL creation method (not actual upload)
|
|
168
|
+
bool _isUrlCreationMethod(AstNode node) {
|
|
169
|
+
// Get the containing method
|
|
170
|
+
AstNode? current = node;
|
|
171
|
+
while (current != null && current is! MethodDeclaration && current is! FunctionDeclaration) {
|
|
172
|
+
current = current.parent;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (current == null) return false;
|
|
176
|
+
|
|
177
|
+
String methodName;
|
|
178
|
+
if (current is MethodDeclaration) {
|
|
179
|
+
methodName = current.name.lexeme.toLowerCase().replaceAll('_', '');
|
|
180
|
+
} else if (current is FunctionDeclaration) {
|
|
181
|
+
methodName = current.name.lexeme.toLowerCase().replaceAll('_', '');
|
|
182
|
+
} else {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Check if method name indicates URL creation (not actual upload)
|
|
187
|
+
return S055ContentTypeValidationAnalyzer._nonUploadMethodPatterns
|
|
188
|
+
.any((p) => methodName.contains(p.replaceAll('_', '')));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Check if the operation is a local file operation (not HTTP upload)
|
|
192
|
+
bool _isLocalFileOperation(MethodInvocation node) {
|
|
193
|
+
final source = node.toSource().toLowerCase();
|
|
194
|
+
final target = node.target?.toSource().toLowerCase() ?? '';
|
|
195
|
+
|
|
196
|
+
// Check if method call target or source matches local file operation patterns
|
|
197
|
+
return S055ContentTypeValidationAnalyzer._localFileOperationPatterns
|
|
198
|
+
.any((p) => source.contains(p) || target.contains(p));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Check if using client-side upload that auto-handles Content-Type
|
|
202
|
+
// Dio's MultipartFile.fromFile() automatically infers Content-Type from file extension
|
|
203
|
+
bool _isClientSideAutoUpload(String source) {
|
|
204
|
+
final sourceLower = source.toLowerCase().replaceAll(' ', '');
|
|
205
|
+
return S055ContentTypeValidationAnalyzer._clientSideUploadPatterns
|
|
206
|
+
.any((p) => sourceLower.contains(p.replaceAll(' ', '')));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
@override
|
|
210
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
211
|
+
final methodName = node.methodName.name.toLowerCase();
|
|
212
|
+
final source = node.toSource().toLowerCase();
|
|
213
|
+
|
|
214
|
+
// Skip local file operations (save to gallery, disk, etc.) - NOT HTTP uploads
|
|
215
|
+
if (_isLocalFileOperation(node)) {
|
|
216
|
+
super.visitMethodInvocation(node);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Check for file upload constructor calls (parsed as method invocations in Dart)
|
|
221
|
+
// FormData(...) or MultipartFile(...) without 'new' keyword
|
|
222
|
+
if (methodName == 'formdata' || methodName == 'multipartfile') {
|
|
223
|
+
// Skip if using Dio's auto Content-Type detection (fromFile, fromMap, etc.)
|
|
224
|
+
if (_isClientSideAutoUpload(source)) {
|
|
225
|
+
super.visitMethodInvocation(node);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!_hasContentTypeCheck(node) && !_delegatesToValidatedUpload(node)) {
|
|
230
|
+
violations.add(analyzer.createViolation(
|
|
231
|
+
filePath: filePath,
|
|
232
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
233
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
234
|
+
message: 'File upload without Content-Type validation',
|
|
235
|
+
));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Only flag actual upload IMPLEMENTATIONS, not callers of upload functions
|
|
240
|
+
// A caller like `uploadVideoToChat(...)` should not be flagged
|
|
241
|
+
// Only flag if this method directly creates FormData/MultipartFile or uses HTTP upload APIs
|
|
242
|
+
|
|
243
|
+
// Check if this is a CALLER of an upload function (not implementation)
|
|
244
|
+
// Callers just invoke methods like `notifier.uploadVideo()` - these are OK
|
|
245
|
+
bool isCallingUploadFunction = methodName.contains('upload') &&
|
|
246
|
+
node.target != null && // Has a target (e.g., notifier.uploadX)
|
|
247
|
+
!source.contains('formdata') &&
|
|
248
|
+
!source.contains('multipartfile') &&
|
|
249
|
+
!source.contains('.post(') &&
|
|
250
|
+
!source.contains('.put(');
|
|
251
|
+
|
|
252
|
+
if (isCallingUploadFunction) {
|
|
253
|
+
// This is just calling an upload function, not implementing upload logic
|
|
254
|
+
super.visitMethodInvocation(node);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Check for actual HTTP upload operations (dio.post with FormData, etc.)
|
|
259
|
+
// Only match FormData() constructor call or FormData.fromMap() - NOT variable names like formData
|
|
260
|
+
// Use case-sensitive match to distinguish:
|
|
261
|
+
// - FormData() or FormData.fromMap() → Dio HTTP upload class (MUST check)
|
|
262
|
+
// - formData → local variable name (skip)
|
|
263
|
+
final formDataClassPattern = RegExp(r'FormData\s*[.(]'); // FormData() or FormData.fromMap(
|
|
264
|
+
final multipartFilePattern = RegExp(r'MultipartFile\s*[.(]'); // MultipartFile() or MultipartFile.fromFile(
|
|
265
|
+
|
|
266
|
+
bool hasFormData = formDataClassPattern.hasMatch(node.toSource());
|
|
267
|
+
bool hasMultipartFile = multipartFilePattern.hasMatch(node.toSource());
|
|
268
|
+
|
|
269
|
+
bool isActualUploadImplementation =
|
|
270
|
+
(hasFormData || hasMultipartFile) ||
|
|
271
|
+
((methodName == 'post' || methodName == 'put') &&
|
|
272
|
+
(hasFormData || hasMultipartFile));
|
|
273
|
+
|
|
274
|
+
if (isActualUploadImplementation) {
|
|
275
|
+
// Skip if using Dio's auto Content-Type detection (fromFile, fromMap, etc.)
|
|
276
|
+
if (_isClientSideAutoUpload(source)) {
|
|
277
|
+
super.visitMethodInvocation(node);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Skip if this is a call to a function that handles content-type validation
|
|
282
|
+
if (_delegatesToValidatedUpload(node)) {
|
|
283
|
+
super.visitMethodInvocation(node);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Skip if this is inside a URL creation method (not actual file upload)
|
|
288
|
+
if (_isUrlCreationMethod(node)) {
|
|
289
|
+
super.visitMethodInvocation(node);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (!_hasContentTypeCheck(node)) {
|
|
294
|
+
violations.add(analyzer.createViolation(
|
|
295
|
+
filePath: filePath,
|
|
296
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
297
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
298
|
+
message: 'File upload without Content-Type validation',
|
|
299
|
+
));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
super.visitMethodInvocation(node);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
@override
|
|
307
|
+
void visitInstanceCreationExpression(InstanceCreationExpression node) {
|
|
308
|
+
final typeName = node.constructorName.type.name2.lexeme.toLowerCase();
|
|
309
|
+
|
|
310
|
+
// Check for FormData or MultipartFile creation (when 'new' keyword is used)
|
|
311
|
+
if (typeName == 'formdata' || typeName == 'multipartfile') {
|
|
312
|
+
if (!_hasContentTypeCheck(node) && !_delegatesToValidatedUpload(node)) {
|
|
313
|
+
violations.add(analyzer.createViolation(
|
|
314
|
+
filePath: filePath,
|
|
315
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
316
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
317
|
+
message: 'File upload creation without Content-Type validation',
|
|
318
|
+
));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
super.visitInstanceCreationExpression(node);
|
|
323
|
+
}
|
|
324
|
+
}
|