@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,253 @@
|
|
|
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
|
+
/// S045: Brute Force Protection
|
|
10
|
+
/// Implement rate limiting and account lockout for authentication
|
|
11
|
+
/// Note: For mobile apps using Auth SDKs (Auth0, Firebase, etc.),
|
|
12
|
+
/// rate limiting is handled server-side by the SDK provider
|
|
13
|
+
class S045BruteForceProtectionAnalyzer extends BaseAnalyzer {
|
|
14
|
+
@override
|
|
15
|
+
String get ruleId => 'S045';
|
|
16
|
+
|
|
17
|
+
// Authentication method indicators
|
|
18
|
+
static const _authMethods = [
|
|
19
|
+
'login', 'signin', 'sign_in', 'authenticate', 'verifypassword',
|
|
20
|
+
'verify_password', 'checkpassword', 'check_password',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
// Method name patterns that contain auth words but are NOT actual authentication
|
|
24
|
+
// These are helper methods, UI methods, or unrelated functionality
|
|
25
|
+
static const _nonAuthMethodPatterns = [
|
|
26
|
+
// URL/navigation checkers
|
|
27
|
+
'isopen', 'is_open', // isOpenLogin, isOpenLoginIntro - URL checks
|
|
28
|
+
// Login bonus/reward related (not authentication)
|
|
29
|
+
'loginbonus', 'login_bonus', // resetLoginBonus, getDailyLoginBonus
|
|
30
|
+
'dailylogin', 'daily_login',
|
|
31
|
+
// UI builders
|
|
32
|
+
'build', // _buildLoginWidget, buildLoginScreen
|
|
33
|
+
// State getters/setters (checking login state, not performing auth)
|
|
34
|
+
'getlogin', 'get_login', 'setlogin', 'set_login',
|
|
35
|
+
'isloggedin', 'is_logged_in',
|
|
36
|
+
// Check/validate UI state (not actual auth)
|
|
37
|
+
'checklogin', 'check_login', // checkLogin() - UI state check
|
|
38
|
+
'checkrelogin', 'check_relogin', // checkReLogin() - UI state check
|
|
39
|
+
'checkuserlogin', 'check_user_login', // checkUserLogin() - check login state
|
|
40
|
+
'checkuserlogintime', 'check_user_login_time', // checkUserLoginTime() - session check
|
|
41
|
+
'validatepassword', 'validate_password', // validatePassword() - input validation
|
|
42
|
+
// Reset/clear operations (not auth attempts)
|
|
43
|
+
'resetlogin', 'reset_login', 'clearlogin', 'clear_login',
|
|
44
|
+
// Get from local/cache
|
|
45
|
+
'fromlocal', 'from_local', 'fromcache', 'from_cache',
|
|
46
|
+
// Notification related
|
|
47
|
+
'notification', 'push', 'fcm', 'sendlog', 'send_log',
|
|
48
|
+
// Update/show dialog (UI related)
|
|
49
|
+
'updateshow', 'update_show', 'showdialog', 'show_dialog',
|
|
50
|
+
// Badge related (app notification badges, not auth)
|
|
51
|
+
'badge', 'removebadge', 'remove_badge', 'updatebadge', 'update_badge',
|
|
52
|
+
'clearbadge', 'clear_badge', 'appbadge', 'app_badge',
|
|
53
|
+
// Logging utilities (log writing, not login)
|
|
54
|
+
'loginfo', 'log_info', 'logerror', 'log_error', 'logdebug', 'log_debug',
|
|
55
|
+
'logutils', 'log_utils', 'writelog', 'write_log',
|
|
56
|
+
'logupload', 'log_upload', 'uploadinfo', 'upload_info',
|
|
57
|
+
// "notlogin" or "notloggedin" checks - checking if user is NOT logged in
|
|
58
|
+
'notlogin', 'not_login', 'notloggedin', 'not_logged_in',
|
|
59
|
+
'whennotlogin', 'when_not_login',
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
// Rate limiting indicators
|
|
63
|
+
static const _rateLimitIndicators = [
|
|
64
|
+
'ratelimit', 'rate_limit', 'throttle', 'lockout', 'lock_out',
|
|
65
|
+
'maxattempts', 'max_attempts', 'failedattempts', 'failed_attempts',
|
|
66
|
+
'cooldown', 'cool_down', 'delay', 'backoff',
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
// Auth SDKs that handle rate limiting server-side
|
|
70
|
+
// Mobile apps using these don't need client-side rate limiting
|
|
71
|
+
static const _authSdkPatterns = [
|
|
72
|
+
// Auth0
|
|
73
|
+
'auth0', 'webauthentication', 'credentialsmanager',
|
|
74
|
+
// Firebase Auth
|
|
75
|
+
'firebaseauth', 'firebase_auth',
|
|
76
|
+
// Google Sign-In
|
|
77
|
+
'googlesignin', 'google_sign_in',
|
|
78
|
+
// Apple Sign-In
|
|
79
|
+
'signinwithapple', 'sign_in_with_apple',
|
|
80
|
+
// AWS Amplify
|
|
81
|
+
'amplify', 'cognitoauth',
|
|
82
|
+
// Generic OAuth
|
|
83
|
+
'oauth', 'oidc',
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
// Delegation patterns - method delegates to another auth method
|
|
87
|
+
static const _delegationPatterns = [
|
|
88
|
+
'usecase', 'use_case', // Clean architecture use case
|
|
89
|
+
'repository', 'service', 'manager', 'provider',
|
|
90
|
+
'authmanager', 'auth_manager', 'authservice', 'auth_service',
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
// Repository/data layer patterns - rate limiting handled by backend
|
|
94
|
+
// These classes only call APIs, rate limiting is server-side
|
|
95
|
+
static const _repositoryClassPatterns = [
|
|
96
|
+
'repositoryimpl', 'repository_impl',
|
|
97
|
+
'datasource', 'data_source',
|
|
98
|
+
'remotedatasource', 'remote_data_source',
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
// GraphQL/API call patterns - backend handles rate limiting
|
|
102
|
+
static const _apiCallPatterns = [
|
|
103
|
+
'greq', 'gql', 'graphql', // GraphQL requests
|
|
104
|
+
'clienthelper', 'client_helper',
|
|
105
|
+
'dio.', 'http.', 'request(',
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
@override
|
|
109
|
+
List<Violation> analyze({
|
|
110
|
+
required CompilationUnit unit,
|
|
111
|
+
required String filePath,
|
|
112
|
+
required Rule rule,
|
|
113
|
+
required LineInfo lineInfo,
|
|
114
|
+
}) {
|
|
115
|
+
final violations = <Violation>[];
|
|
116
|
+
final visitor = _S045Visitor(
|
|
117
|
+
filePath: filePath,
|
|
118
|
+
lineInfo: lineInfo,
|
|
119
|
+
violations: violations,
|
|
120
|
+
analyzer: this,
|
|
121
|
+
);
|
|
122
|
+
unit.accept(visitor);
|
|
123
|
+
return violations;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
class _S045Visitor extends RecursiveAstVisitor<void> {
|
|
128
|
+
final String filePath;
|
|
129
|
+
final LineInfo lineInfo;
|
|
130
|
+
final List<Violation> violations;
|
|
131
|
+
final S045BruteForceProtectionAnalyzer analyzer;
|
|
132
|
+
|
|
133
|
+
_S045Visitor({
|
|
134
|
+
required this.filePath,
|
|
135
|
+
required this.lineInfo,
|
|
136
|
+
required this.violations,
|
|
137
|
+
required this.analyzer,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Track if the containing class has rate limiting
|
|
141
|
+
bool _classHasRateLimit = false;
|
|
142
|
+
// Track if class is a repository/data layer (rate limiting handled by backend)
|
|
143
|
+
bool _isRepositoryClass = false;
|
|
144
|
+
|
|
145
|
+
@override
|
|
146
|
+
void visitClassDeclaration(ClassDeclaration node) {
|
|
147
|
+
final className = node.name.lexeme.toLowerCase().replaceAll('_', '');
|
|
148
|
+
|
|
149
|
+
// Check class level for rate limit indicators
|
|
150
|
+
final classSource = node.toSource().toLowerCase().replaceAll('_', '');
|
|
151
|
+
_classHasRateLimit = S045BruteForceProtectionAnalyzer._rateLimitIndicators
|
|
152
|
+
.any((r) => classSource.contains(r.replaceAll('_', '')));
|
|
153
|
+
|
|
154
|
+
// Check if this is a repository/data layer class
|
|
155
|
+
_isRepositoryClass = S045BruteForceProtectionAnalyzer._repositoryClassPatterns
|
|
156
|
+
.any((p) => className.contains(p.replaceAll('_', '')));
|
|
157
|
+
|
|
158
|
+
super.visitClassDeclaration(node);
|
|
159
|
+
|
|
160
|
+
// Reset after processing class
|
|
161
|
+
_classHasRateLimit = false;
|
|
162
|
+
_isRepositoryClass = false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@override
|
|
166
|
+
void visitMethodDeclaration(MethodDeclaration node) {
|
|
167
|
+
final methodName = node.name.lexeme.toLowerCase();
|
|
168
|
+
final methodNameNormalized = methodName.replaceAll('_', '');
|
|
169
|
+
|
|
170
|
+
bool isAuthMethod = S045BruteForceProtectionAnalyzer._authMethods
|
|
171
|
+
.any((m) => methodName.contains(m));
|
|
172
|
+
|
|
173
|
+
// Skip methods that contain auth words but are not actual authentication
|
|
174
|
+
bool isNonAuthMethod = S045BruteForceProtectionAnalyzer._nonAuthMethodPatterns
|
|
175
|
+
.any((p) => methodNameNormalized.contains(p.replaceAll('_', '')));
|
|
176
|
+
|
|
177
|
+
if (isNonAuthMethod) {
|
|
178
|
+
super.visitMethodDeclaration(node);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (isAuthMethod) {
|
|
183
|
+
final body = node.body;
|
|
184
|
+
if (body != null) {
|
|
185
|
+
// Skip arrow functions (helper functions like `=> true`)
|
|
186
|
+
if (body is ExpressionFunctionBody) {
|
|
187
|
+
super.visitMethodDeclaration(node);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
final bodySource = body.toSource().toLowerCase().replaceAll('_', '');
|
|
192
|
+
|
|
193
|
+
// Skip very short helper functions
|
|
194
|
+
if (bodySource.length < 30) {
|
|
195
|
+
super.visitMethodDeclaration(node);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Skip repository/data layer classes - rate limiting is backend responsibility
|
|
200
|
+
if (_isRepositoryClass) {
|
|
201
|
+
super.visitMethodDeclaration(node);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Check if using Auth SDK that handles rate limiting server-side
|
|
206
|
+
bool usesAuthSdk = S045BruteForceProtectionAnalyzer._authSdkPatterns
|
|
207
|
+
.any((p) => bodySource.contains(p.replaceAll('_', '')));
|
|
208
|
+
|
|
209
|
+
if (usesAuthSdk) {
|
|
210
|
+
// Auth SDK handles rate limiting on server-side
|
|
211
|
+
super.visitMethodDeclaration(node);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Check if method makes API/GraphQL calls - rate limiting handled by backend
|
|
216
|
+
bool makesApiCall = S045BruteForceProtectionAnalyzer._apiCallPatterns
|
|
217
|
+
.any((p) => bodySource.contains(p.replaceAll('_', '')));
|
|
218
|
+
|
|
219
|
+
if (makesApiCall) {
|
|
220
|
+
// API calls - rate limiting is server-side responsibility
|
|
221
|
+
super.visitMethodDeclaration(node);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Check if method delegates to another auth service/use case
|
|
226
|
+
bool delegatesToAuth = S045BruteForceProtectionAnalyzer._delegationPatterns
|
|
227
|
+
.any((p) => bodySource.contains(p.replaceAll('_', '')));
|
|
228
|
+
|
|
229
|
+
if (delegatesToAuth) {
|
|
230
|
+
// Delegating to another layer that may handle rate limiting
|
|
231
|
+
super.visitMethodDeclaration(node);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Check method body for rate limiting
|
|
236
|
+
bool hasRateLimit = S045BruteForceProtectionAnalyzer._rateLimitIndicators
|
|
237
|
+
.any((r) => bodySource.contains(r.replaceAll('_', '')));
|
|
238
|
+
|
|
239
|
+
// Also consider class-level rate limiting
|
|
240
|
+
if (!hasRateLimit && !_classHasRateLimit) {
|
|
241
|
+
violations.add(analyzer.createViolation(
|
|
242
|
+
filePath: filePath,
|
|
243
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
244
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
245
|
+
message: 'Authentication should have brute force protection (rate limiting/lockout)',
|
|
246
|
+
));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
super.visitMethodDeclaration(node);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
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
|
+
/// S046: Use algorithm allowlist for self-contained tokens
|
|
10
|
+
/// Detect JWT/token verification without algorithm restriction
|
|
11
|
+
class S046JwtAlgorithmAllowlistAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'S046';
|
|
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 = _S046Visitor(
|
|
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 _S046Visitor extends RecursiveAstVisitor<void> {
|
|
35
|
+
final String filePath;
|
|
36
|
+
final LineInfo lineInfo;
|
|
37
|
+
final List<Violation> violations;
|
|
38
|
+
final S046JwtAlgorithmAllowlistAnalyzer analyzer;
|
|
39
|
+
|
|
40
|
+
_S046Visitor({
|
|
41
|
+
required this.filePath,
|
|
42
|
+
required this.lineInfo,
|
|
43
|
+
required this.violations,
|
|
44
|
+
required this.analyzer,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
@override
|
|
48
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
49
|
+
final methodName = node.methodName.name.toLowerCase();
|
|
50
|
+
final source = node.toSource().toLowerCase();
|
|
51
|
+
|
|
52
|
+
// Check for JWT decode without verification (security risk)
|
|
53
|
+
if (methodName == 'decode' && source.contains('jwt')) {
|
|
54
|
+
// Check if verify is false or not specified
|
|
55
|
+
if (!source.contains('verify') || source.contains('verify: false')) {
|
|
56
|
+
violations.add(analyzer.createViolation(
|
|
57
|
+
filePath: filePath,
|
|
58
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
59
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
60
|
+
message:
|
|
61
|
+
'JWT decode without verification - use verify() with algorithm allowlist',
|
|
62
|
+
));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check for JWT verify without algorithm specification
|
|
67
|
+
if (methodName == 'verify' && source.contains('jwt')) {
|
|
68
|
+
// Look for algorithm specification in arguments
|
|
69
|
+
if (!source.contains('algorithm') && !source.contains('alg')) {
|
|
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
|
+
'JWT verify without algorithm specification - specify allowed algorithms',
|
|
76
|
+
));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
super.visitMethodInvocation(node);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@override
|
|
84
|
+
void visitSimpleStringLiteral(SimpleStringLiteral node) {
|
|
85
|
+
final value = node.value.toLowerCase();
|
|
86
|
+
|
|
87
|
+
// Check for 'none' algorithm in JWT context
|
|
88
|
+
if (value == 'none') {
|
|
89
|
+
// Check surrounding context for JWT-related code
|
|
90
|
+
AstNode? current = node.parent;
|
|
91
|
+
int depth = 0;
|
|
92
|
+
while (current != null && depth < 5) {
|
|
93
|
+
final parentSource = current.toSource().toLowerCase();
|
|
94
|
+
if (parentSource.contains('jwt') ||
|
|
95
|
+
parentSource.contains('algorithm') ||
|
|
96
|
+
parentSource.contains('token')) {
|
|
97
|
+
violations.add(analyzer.createViolation(
|
|
98
|
+
filePath: filePath,
|
|
99
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
100
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
101
|
+
message:
|
|
102
|
+
"Algorithm 'none' detected - this disables signature verification",
|
|
103
|
+
));
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
current = current.parent;
|
|
107
|
+
depth++;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
super.visitSimpleStringLiteral(node);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
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
|
+
/// S047: Protect OAuth code flow against CSRF attacks
|
|
10
|
+
/// Detect OAuth implementations without PKCE or state parameter
|
|
11
|
+
class S047OAuthPkceProtectionAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'S047';
|
|
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 = _S047Visitor(
|
|
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 _S047Visitor extends RecursiveAstVisitor<void> {
|
|
35
|
+
final String filePath;
|
|
36
|
+
final LineInfo lineInfo;
|
|
37
|
+
final List<Violation> violations;
|
|
38
|
+
final S047OAuthPkceProtectionAnalyzer analyzer;
|
|
39
|
+
|
|
40
|
+
// Track OAuth-related context
|
|
41
|
+
bool _inOAuthContext = false;
|
|
42
|
+
|
|
43
|
+
_S047Visitor({
|
|
44
|
+
required this.filePath,
|
|
45
|
+
required this.lineInfo,
|
|
46
|
+
required this.violations,
|
|
47
|
+
required this.analyzer,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
@override
|
|
51
|
+
void visitSimpleStringLiteral(SimpleStringLiteral node) {
|
|
52
|
+
final value = node.value.toLowerCase();
|
|
53
|
+
|
|
54
|
+
// Check for OAuth authorization URLs
|
|
55
|
+
if (value.contains('authorize') && value.contains('client_id')) {
|
|
56
|
+
// Check if URL has code_challenge (PKCE) or state parameter
|
|
57
|
+
final hasCodeChallenge = value.contains('code_challenge');
|
|
58
|
+
final hasState = value.contains('state=');
|
|
59
|
+
|
|
60
|
+
if (!hasCodeChallenge && !hasState) {
|
|
61
|
+
violations.add(analyzer.createViolation(
|
|
62
|
+
filePath: filePath,
|
|
63
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
64
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
65
|
+
message:
|
|
66
|
+
'OAuth URL missing PKCE code_challenge or state parameter for CSRF protection',
|
|
67
|
+
));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check for hardcoded state values (weak protection)
|
|
72
|
+
if (value.contains('state=')) {
|
|
73
|
+
// Extract state value and check if it looks hardcoded
|
|
74
|
+
final stateMatch = RegExp(r'state=([^&\s]+)').firstMatch(value);
|
|
75
|
+
if (stateMatch != null) {
|
|
76
|
+
final stateValue = stateMatch.group(1) ?? '';
|
|
77
|
+
// Short, simple state values are likely hardcoded
|
|
78
|
+
if (stateValue.length < 16 &&
|
|
79
|
+
!stateValue.contains(r'$') &&
|
|
80
|
+
!stateValue.contains('{')) {
|
|
81
|
+
violations.add(analyzer.createViolation(
|
|
82
|
+
filePath: filePath,
|
|
83
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
84
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
85
|
+
message:
|
|
86
|
+
'OAuth state parameter appears hardcoded - use cryptographically random value',
|
|
87
|
+
));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
super.visitSimpleStringLiteral(node);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@override
|
|
96
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
97
|
+
final methodName = node.methodName.name.toLowerCase();
|
|
98
|
+
final source = node.toSource().toLowerCase();
|
|
99
|
+
|
|
100
|
+
// Check for OAuth/OpenID connect methods without PKCE
|
|
101
|
+
if ((methodName.contains('oauth') ||
|
|
102
|
+
methodName.contains('authorize') ||
|
|
103
|
+
methodName.contains('authenticate')) &&
|
|
104
|
+
source.contains('client')) {
|
|
105
|
+
// Check if PKCE parameters are present
|
|
106
|
+
if (!source.contains('code_challenge') &&
|
|
107
|
+
!source.contains('codechallenge') &&
|
|
108
|
+
!source.contains('pkce')) {
|
|
109
|
+
// Also check for state parameter
|
|
110
|
+
if (!source.contains('state')) {
|
|
111
|
+
violations.add(analyzer.createViolation(
|
|
112
|
+
filePath: filePath,
|
|
113
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
114
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
115
|
+
message:
|
|
116
|
+
'OAuth flow without PKCE or state parameter - vulnerable to CSRF',
|
|
117
|
+
));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
super.visitMethodInvocation(node);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
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
|
+
/// S048: Validate OAuth redirect URIs with exact string comparison
|
|
10
|
+
/// Detect insecure OAuth redirect URI validation
|
|
11
|
+
class S048OAuthRedirectUriValidationAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'S048';
|
|
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 = _S048Visitor(
|
|
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 _S048Visitor extends RecursiveAstVisitor<void> {
|
|
35
|
+
final String filePath;
|
|
36
|
+
final LineInfo lineInfo;
|
|
37
|
+
final List<Violation> violations;
|
|
38
|
+
final S048OAuthRedirectUriValidationAnalyzer analyzer;
|
|
39
|
+
|
|
40
|
+
_S048Visitor({
|
|
41
|
+
required this.filePath,
|
|
42
|
+
required this.lineInfo,
|
|
43
|
+
required this.violations,
|
|
44
|
+
required this.analyzer,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
@override
|
|
48
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
49
|
+
final methodName = node.methodName.name.toLowerCase();
|
|
50
|
+
final source = node.toSource().toLowerCase();
|
|
51
|
+
|
|
52
|
+
// Check for redirect URI with partial matching (insecure)
|
|
53
|
+
if (source.contains('redirect') || source.contains('callback')) {
|
|
54
|
+
// Check for startsWith validation (insecure)
|
|
55
|
+
if (methodName == 'startswith') {
|
|
56
|
+
violations.add(analyzer.createViolation(
|
|
57
|
+
filePath: filePath,
|
|
58
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
59
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
60
|
+
message:
|
|
61
|
+
'Redirect URI validated with startsWith() - use exact string comparison',
|
|
62
|
+
));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check for contains validation (insecure)
|
|
66
|
+
if (methodName == 'contains' && source.contains('uri')) {
|
|
67
|
+
violations.add(analyzer.createViolation(
|
|
68
|
+
filePath: filePath,
|
|
69
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
70
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
71
|
+
message:
|
|
72
|
+
'Redirect URI validated with contains() - use exact string comparison',
|
|
73
|
+
));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check for regex matching (insecure)
|
|
77
|
+
if (methodName == 'hasmatch' || methodName == 'firstmatch') {
|
|
78
|
+
if (source.contains('redirect') || source.contains('callback')) {
|
|
79
|
+
violations.add(analyzer.createViolation(
|
|
80
|
+
filePath: filePath,
|
|
81
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
82
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
83
|
+
message:
|
|
84
|
+
'Redirect URI validated with regex - use exact string comparison',
|
|
85
|
+
));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
super.visitMethodInvocation(node);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@override
|
|
94
|
+
void visitSimpleStringLiteral(SimpleStringLiteral node) {
|
|
95
|
+
final value = node.value;
|
|
96
|
+
|
|
97
|
+
// Check for wildcard patterns in redirect URIs
|
|
98
|
+
if (value.contains('*.') &&
|
|
99
|
+
(value.contains('redirect') || value.contains('callback'))) {
|
|
100
|
+
violations.add(analyzer.createViolation(
|
|
101
|
+
filePath: filePath,
|
|
102
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
103
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
104
|
+
message:
|
|
105
|
+
'Wildcard domain pattern for redirect URI - use exact URIs only',
|
|
106
|
+
));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check for wildcard in URI list context
|
|
110
|
+
if (value.startsWith('*.')) {
|
|
111
|
+
AstNode? current = node.parent;
|
|
112
|
+
int depth = 0;
|
|
113
|
+
while (current != null && depth < 5) {
|
|
114
|
+
final parentSource = current.toSource().toLowerCase();
|
|
115
|
+
if (parentSource.contains('redirect') ||
|
|
116
|
+
parentSource.contains('callback') ||
|
|
117
|
+
parentSource.contains('alloweduri') ||
|
|
118
|
+
parentSource.contains('whitelist')) {
|
|
119
|
+
violations.add(analyzer.createViolation(
|
|
120
|
+
filePath: filePath,
|
|
121
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
122
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
123
|
+
message: 'Wildcard in allowed redirect URIs - use exact URIs only',
|
|
124
|
+
));
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
current = current.parent;
|
|
128
|
+
depth++;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
super.visitSimpleStringLiteral(node);
|
|
133
|
+
}
|
|
134
|
+
}
|