@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,550 @@
|
|
|
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
|
+
/// S023: Use output encoding when building dynamic JavaScript/JSON
|
|
10
|
+
/// Prevent JavaScript and JSON injection by applying proper output encoding
|
|
11
|
+
/// when dynamically building JavaScript content or JSON data.
|
|
12
|
+
///
|
|
13
|
+
/// Detects:
|
|
14
|
+
/// - Dynamic code execution patterns (Function.apply, mirrors, runZoned)
|
|
15
|
+
/// - JavaScript injection via WebView/InAppWebView
|
|
16
|
+
/// - String interpolation/concatenation to build JavaScript code
|
|
17
|
+
/// - JSON construction without proper encoding
|
|
18
|
+
/// - innerHTML-like patterns in Flutter web
|
|
19
|
+
/// - Inline event handlers with user data
|
|
20
|
+
class S023NoJsonInjectionAnalyzer extends BaseAnalyzer {
|
|
21
|
+
@override
|
|
22
|
+
String get ruleId => 'S023';
|
|
23
|
+
|
|
24
|
+
// Methods that are safe for string interpolation (logging, debugging)
|
|
25
|
+
static const _safeLoggingMethods = [
|
|
26
|
+
'debugprint', 'print', 'log', 'info', 'warn', 'warning',
|
|
27
|
+
'error', 'debug', 'trace', 'verbose', 'loginfo', 'logerror',
|
|
28
|
+
'logwarning', 'logdebug', 'logger', 'console',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
// Patterns that indicate actual JSON construction (not just logging)
|
|
32
|
+
static const _jsonConstructionPatterns = [
|
|
33
|
+
'response.write', 'response.send', 'jsonresponse',
|
|
34
|
+
'res.json', 'res.send', 'http.post', 'http.put',
|
|
35
|
+
'dio.post', 'dio.put', 'request.body',
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
// Dangerous JavaScript execution methods in WebView
|
|
39
|
+
static const _jsExecutionMethods = [
|
|
40
|
+
'evaluatejavascript',
|
|
41
|
+
'runjavascript',
|
|
42
|
+
'runjavascriptreturningresult',
|
|
43
|
+
'callasyncdart',
|
|
44
|
+
'injectjavascriptfilefrompublicassetfile',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// User input sources to track
|
|
48
|
+
static const _userInputPatterns = [
|
|
49
|
+
'textcontroller',
|
|
50
|
+
'texteditingcontroller',
|
|
51
|
+
'formfield',
|
|
52
|
+
'inputdecoration',
|
|
53
|
+
'sharedpreferences',
|
|
54
|
+
'securestorage',
|
|
55
|
+
'getstring',
|
|
56
|
+
'request.body',
|
|
57
|
+
'request.query',
|
|
58
|
+
'uri.queryparameters',
|
|
59
|
+
'platformmessenger',
|
|
60
|
+
'methodchannel',
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
// Safe encoding functions
|
|
64
|
+
static const _safeEncodingFunctions = [
|
|
65
|
+
'jsonencode',
|
|
66
|
+
'json.encode',
|
|
67
|
+
'htmlescape',
|
|
68
|
+
'uriencode',
|
|
69
|
+
'uri.encodefull',
|
|
70
|
+
'uri.encodecomponent',
|
|
71
|
+
'uri.encodequerycomponent',
|
|
72
|
+
'sanitize',
|
|
73
|
+
'escape',
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
// Variable names that typically indicate safe/encoded data
|
|
77
|
+
static const _safeVariablePatterns = [
|
|
78
|
+
'safejson',
|
|
79
|
+
'encodedjson',
|
|
80
|
+
'jsonstr',
|
|
81
|
+
'jsonstring',
|
|
82
|
+
'encoded',
|
|
83
|
+
'sanitized',
|
|
84
|
+
'escaped',
|
|
85
|
+
'safe',
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
// Patterns that indicate constant/config values (not user input)
|
|
89
|
+
static const _constantPatterns = [
|
|
90
|
+
'constants.', // Constants.fieldName
|
|
91
|
+
'config.', // Config.value
|
|
92
|
+
'appconfig.', // AppConfig.value
|
|
93
|
+
'env.', // Environment variables
|
|
94
|
+
'settings.', // Settings values
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
@override
|
|
98
|
+
List<Violation> analyze({
|
|
99
|
+
required CompilationUnit unit,
|
|
100
|
+
required String filePath,
|
|
101
|
+
required Rule rule,
|
|
102
|
+
required LineInfo lineInfo,
|
|
103
|
+
}) {
|
|
104
|
+
final violations = <Violation>[];
|
|
105
|
+
final visitor = _S023Visitor(
|
|
106
|
+
filePath: filePath,
|
|
107
|
+
lineInfo: lineInfo,
|
|
108
|
+
violations: violations,
|
|
109
|
+
analyzer: this,
|
|
110
|
+
);
|
|
111
|
+
unit.accept(visitor);
|
|
112
|
+
return violations;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// Check if method is a safe logging method
|
|
116
|
+
static bool isSafeLoggingMethod(String methodName) {
|
|
117
|
+
return _safeLoggingMethods.any((m) => methodName.contains(m));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// Check if method is a JavaScript execution method (WebView)
|
|
121
|
+
static bool isJsExecutionMethod(String methodName) {
|
|
122
|
+
return _jsExecutionMethods.any((m) => methodName.contains(m));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Check if expression contains user input
|
|
126
|
+
static bool containsUserInput(String source) {
|
|
127
|
+
final lowerSource = source.toLowerCase();
|
|
128
|
+
return _userInputPatterns.any((p) => lowerSource.contains(p));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// Check if expression has safe encoding
|
|
132
|
+
static bool hasSafeEncoding(String source) {
|
|
133
|
+
final lowerSource = source.toLowerCase();
|
|
134
|
+
return _safeEncodingFunctions.any((f) => lowerSource.contains(f));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Check if variable name indicates it's already encoded/safe
|
|
138
|
+
static bool hasSafeVariableName(String source) {
|
|
139
|
+
final lowerSource = source.toLowerCase();
|
|
140
|
+
return _safeVariablePatterns.any((p) => lowerSource.contains(p));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// Check if interpolation contains only constant values (not user input)
|
|
144
|
+
static bool containsOnlyConstants(String source) {
|
|
145
|
+
final lowerSource = source.toLowerCase();
|
|
146
|
+
|
|
147
|
+
// Check for known constant patterns
|
|
148
|
+
if (_constantPatterns.any((p) => lowerSource.contains(p))) {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check for all-uppercase variable names which typically indicate constants
|
|
153
|
+
// Pattern: ${CONSTANT_NAME} or $CONSTANT_NAME
|
|
154
|
+
final constPattern = RegExp(r'\$\{?[A-Z][A-Z0-9_]*\}?');
|
|
155
|
+
if (constPattern.hasMatch(source)) {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// Check if the interpolation elements appear to be dynamic user data
|
|
163
|
+
/// Returns true if it looks like user-controlled input, false if it looks like safe data
|
|
164
|
+
static bool looksLikeUserInput(String source) {
|
|
165
|
+
final lowerSource = source.toLowerCase();
|
|
166
|
+
|
|
167
|
+
// Check for user input patterns
|
|
168
|
+
if (containsUserInput(source)) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Check for common patterns that indicate runtime/dynamic data
|
|
173
|
+
final dynamicPatterns = [
|
|
174
|
+
'widget.', // Widget properties (often from user)
|
|
175
|
+
'state.', // State values
|
|
176
|
+
'.text', // TextField values
|
|
177
|
+
'input', // Input variables
|
|
178
|
+
'param', // Parameters that could be user input
|
|
179
|
+
'data[', // Array/map access
|
|
180
|
+
'body[', // Request body access
|
|
181
|
+
'query[', // Query parameter access
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
return dynamicPatterns.any((p) => lowerSource.contains(p));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
class _S023Visitor extends RecursiveAstVisitor<void> {
|
|
189
|
+
final String filePath;
|
|
190
|
+
final LineInfo lineInfo;
|
|
191
|
+
final List<Violation> violations;
|
|
192
|
+
final S023NoJsonInjectionAnalyzer analyzer;
|
|
193
|
+
|
|
194
|
+
_S023Visitor({
|
|
195
|
+
required this.filePath,
|
|
196
|
+
required this.lineInfo,
|
|
197
|
+
required this.violations,
|
|
198
|
+
required this.analyzer,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
/// Track if we're inside a logging method call or toString method
|
|
202
|
+
bool _insideLoggingMethod = false;
|
|
203
|
+
bool _insideToString = false;
|
|
204
|
+
|
|
205
|
+
@override
|
|
206
|
+
void visitMethodDeclaration(MethodDeclaration node) {
|
|
207
|
+
final methodName = node.name.lexeme.toLowerCase();
|
|
208
|
+
|
|
209
|
+
// Skip toString methods - they're not JSON construction
|
|
210
|
+
if (methodName == 'tostring') {
|
|
211
|
+
_insideToString = true;
|
|
212
|
+
super.visitMethodDeclaration(node);
|
|
213
|
+
_insideToString = false;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
super.visitMethodDeclaration(node);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
@override
|
|
221
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
222
|
+
final methodName = node.methodName.name.toLowerCase();
|
|
223
|
+
|
|
224
|
+
// Check if this is a logging method
|
|
225
|
+
bool isLoggingMethod = S023NoJsonInjectionAnalyzer.isSafeLoggingMethod(methodName);
|
|
226
|
+
|
|
227
|
+
if (isLoggingMethod) {
|
|
228
|
+
_insideLoggingMethod = true;
|
|
229
|
+
super.visitMethodInvocation(node);
|
|
230
|
+
_insideLoggingMethod = false;
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 1. Check for JavaScript execution in WebView with user data
|
|
235
|
+
if (S023NoJsonInjectionAnalyzer.isJsExecutionMethod(methodName)) {
|
|
236
|
+
_checkJsExecutionMethod(node);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 2. Check for Function.apply with dynamic data (Dart's equivalent to eval)
|
|
240
|
+
if (methodName == 'apply' && _isFunctionApply(node)) {
|
|
241
|
+
_checkFunctionApply(node);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 3. Check for dart:mirrors usage with user data
|
|
245
|
+
if (methodName == 'invoke' || methodName == 'getfield' || methodName == 'setfield') {
|
|
246
|
+
_checkMirrorsUsage(node);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 4. Check for manual JSON construction in non-logging methods
|
|
250
|
+
if (methodName == 'write' || methodName == 'writeln' || methodName == 'add') {
|
|
251
|
+
for (final arg in node.argumentList.arguments) {
|
|
252
|
+
if (arg is StringInterpolation) {
|
|
253
|
+
final source = arg.toSource();
|
|
254
|
+
// Only flag if it looks like structured JSON (key-value pairs)
|
|
255
|
+
if (_looksLikeJsonStructure(source)) {
|
|
256
|
+
violations.add(analyzer.createViolation(
|
|
257
|
+
filePath: filePath,
|
|
258
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
259
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
260
|
+
message: 'JSON injection risk - use jsonEncode() for proper JSON encoding',
|
|
261
|
+
));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 5. Check for setInnerHtml or similar HTML injection methods
|
|
268
|
+
if (methodName == 'setinnerhtml' || methodName == 'appendhtml') {
|
|
269
|
+
_checkHtmlInjection(node);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
super.visitMethodInvocation(node);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/// Check JavaScript execution methods for user data injection
|
|
276
|
+
void _checkJsExecutionMethod(MethodInvocation node) {
|
|
277
|
+
for (final arg in node.argumentList.arguments) {
|
|
278
|
+
final source = arg.toSource();
|
|
279
|
+
|
|
280
|
+
// Skip if source indicates safe encoding or safe variable names
|
|
281
|
+
if (S023NoJsonInjectionAnalyzer.hasSafeEncoding(source) ||
|
|
282
|
+
S023NoJsonInjectionAnalyzer.hasSafeVariableName(source)) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Check if argument contains interpolation or user input
|
|
287
|
+
if (arg is StringInterpolation || source.contains('+')) {
|
|
288
|
+
violations.add(analyzer.createViolation(
|
|
289
|
+
filePath: filePath,
|
|
290
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
291
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
292
|
+
message: 'JavaScript injection risk - encode user data before passing to WebView JavaScript execution',
|
|
293
|
+
));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Check for user input patterns
|
|
298
|
+
if (S023NoJsonInjectionAnalyzer.containsUserInput(source)) {
|
|
299
|
+
violations.add(analyzer.createViolation(
|
|
300
|
+
filePath: filePath,
|
|
301
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
302
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
303
|
+
message: 'JavaScript injection risk - sanitize user input before WebView JavaScript execution',
|
|
304
|
+
));
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/// Check if this is Function.apply call
|
|
311
|
+
bool _isFunctionApply(MethodInvocation node) {
|
|
312
|
+
final target = node.target;
|
|
313
|
+
if (target is Identifier) {
|
|
314
|
+
// Check if target type might be Function
|
|
315
|
+
final name = target.name.toLowerCase();
|
|
316
|
+
return name.contains('function') || name.contains('callback') || name.contains('handler');
|
|
317
|
+
}
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/// Check Function.apply for dynamic code execution risks
|
|
322
|
+
void _checkFunctionApply(MethodInvocation node) {
|
|
323
|
+
final source = node.toSource();
|
|
324
|
+
|
|
325
|
+
if (S023NoJsonInjectionAnalyzer.containsUserInput(source)) {
|
|
326
|
+
violations.add(analyzer.createViolation(
|
|
327
|
+
filePath: filePath,
|
|
328
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
329
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
330
|
+
message: 'Dynamic code execution risk - avoid Function.apply with user-controlled data',
|
|
331
|
+
));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/// Check dart:mirrors usage for reflection-based injection
|
|
336
|
+
void _checkMirrorsUsage(MethodInvocation node) {
|
|
337
|
+
final source = node.toSource();
|
|
338
|
+
|
|
339
|
+
if (S023NoJsonInjectionAnalyzer.containsUserInput(source)) {
|
|
340
|
+
violations.add(analyzer.createViolation(
|
|
341
|
+
filePath: filePath,
|
|
342
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
343
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
344
|
+
message: 'Reflection injection risk - validate user input before using with dart:mirrors',
|
|
345
|
+
));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/// Check HTML injection methods
|
|
350
|
+
void _checkHtmlInjection(MethodInvocation node) {
|
|
351
|
+
for (final arg in node.argumentList.arguments) {
|
|
352
|
+
final source = arg.toSource();
|
|
353
|
+
|
|
354
|
+
// Check for unescaped user data in HTML
|
|
355
|
+
if ((arg is StringInterpolation || source.contains('+')) &&
|
|
356
|
+
!S023NoJsonInjectionAnalyzer.hasSafeEncoding(source)) {
|
|
357
|
+
violations.add(analyzer.createViolation(
|
|
358
|
+
filePath: filePath,
|
|
359
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
360
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
361
|
+
message: 'HTML injection risk - use HtmlEscape or sanitize user data before inserting into HTML',
|
|
362
|
+
));
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
@override
|
|
369
|
+
void visitStringInterpolation(StringInterpolation node) {
|
|
370
|
+
// Skip if inside logging method or toString
|
|
371
|
+
if (_insideLoggingMethod || _insideToString) {
|
|
372
|
+
super.visitStringInterpolation(node);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
final source = node.toSource();
|
|
377
|
+
|
|
378
|
+
// Check if there are interpolation elements
|
|
379
|
+
bool hasInterpolation = node.elements.any((e) => e is InterpolationExpression);
|
|
380
|
+
if (!hasInterpolation) {
|
|
381
|
+
super.visitStringInterpolation(node);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Skip if source contains safe encoding or safe variable names
|
|
386
|
+
if (S023NoJsonInjectionAnalyzer.hasSafeEncoding(source) ||
|
|
387
|
+
S023NoJsonInjectionAnalyzer.hasSafeVariableName(source)) {
|
|
388
|
+
super.visitStringInterpolation(node);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Skip if interpolation contains only constants (not user input)
|
|
393
|
+
if (S023NoJsonInjectionAnalyzer.containsOnlyConstants(source)) {
|
|
394
|
+
super.visitStringInterpolation(node);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// 1. Check for JavaScript code being built
|
|
399
|
+
if (_looksLikeJavaScriptCode(source)) {
|
|
400
|
+
// Only flag if it looks like user input is being used
|
|
401
|
+
// Skip if the interpolated values don't appear to be user-controlled
|
|
402
|
+
if (S023NoJsonInjectionAnalyzer.looksLikeUserInput(source) ||
|
|
403
|
+
S023NoJsonInjectionAnalyzer.containsUserInput(source)) {
|
|
404
|
+
violations.add(analyzer.createViolation(
|
|
405
|
+
filePath: filePath,
|
|
406
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
407
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
408
|
+
message: 'JavaScript injection risk - encode data before building JavaScript code strings',
|
|
409
|
+
));
|
|
410
|
+
super.visitStringInterpolation(node);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
// For JavaScript code without clear user input, skip to reduce false positives
|
|
414
|
+
super.visitStringInterpolation(node);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// 2. Check for HTML with event handlers
|
|
419
|
+
if (_looksLikeHtmlWithEventHandler(source)) {
|
|
420
|
+
violations.add(analyzer.createViolation(
|
|
421
|
+
filePath: filePath,
|
|
422
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
423
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
424
|
+
message: 'Inline event handler injection risk - avoid building HTML with inline event handlers containing user data',
|
|
425
|
+
));
|
|
426
|
+
super.visitStringInterpolation(node);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// 3. Check for structured JSON being constructed
|
|
431
|
+
if (_looksLikeJsonStructure(source)) {
|
|
432
|
+
violations.add(analyzer.createViolation(
|
|
433
|
+
filePath: filePath,
|
|
434
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
435
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
436
|
+
message: 'JSON injection risk - use jsonEncode() instead of string interpolation for JSON',
|
|
437
|
+
));
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
super.visitStringInterpolation(node);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/// Check if string looks like JavaScript code
|
|
444
|
+
bool _looksLikeJavaScriptCode(String source) {
|
|
445
|
+
// Patterns indicating JavaScript code construction
|
|
446
|
+
final jsPatterns = [
|
|
447
|
+
RegExp(r'\bvar\s+\w+\s*='),
|
|
448
|
+
RegExp(r'\blet\s+\w+\s*='),
|
|
449
|
+
RegExp(r'\bconst\s+\w+\s*='),
|
|
450
|
+
RegExp(r'\bfunction\s*\('),
|
|
451
|
+
RegExp(r'=>\s*\{'),
|
|
452
|
+
RegExp(r'return\s+'),
|
|
453
|
+
RegExp(r'\beval\s*\('),
|
|
454
|
+
RegExp(r'\bnew\s+Function\s*\('),
|
|
455
|
+
RegExp(r'document\.'),
|
|
456
|
+
RegExp(r'window\.'),
|
|
457
|
+
RegExp(r'javascript:', caseSensitive: false),
|
|
458
|
+
];
|
|
459
|
+
|
|
460
|
+
return jsPatterns.any((pattern) => pattern.hasMatch(source));
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/// Check if string looks like HTML with inline event handlers
|
|
464
|
+
bool _looksLikeHtmlWithEventHandler(String source) {
|
|
465
|
+
// Pattern for inline event handlers: onclick, onload, onerror, etc.
|
|
466
|
+
final eventHandlerPattern = RegExp(r'\bon\w+\s*=\s*["\x27]', caseSensitive: false);
|
|
467
|
+
return eventHandlerPattern.hasMatch(source);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/// Check if string looks like actual JSON structure (not just debug log)
|
|
471
|
+
bool _looksLikeJsonStructure(String source) {
|
|
472
|
+
// Must have JSON-like structure: {"key": value} or [...]
|
|
473
|
+
// Not just "message: $var" which is common in logging
|
|
474
|
+
|
|
475
|
+
// Check for actual JSON object pattern: {"key": or {'key':
|
|
476
|
+
// Pattern: { followed by optional whitespace, optional quote, word, optional quote, colon
|
|
477
|
+
final jsonObjectPattern = RegExp(r'\{\s*["\x27]?\w+["\x27]?\s*:');
|
|
478
|
+
if (jsonObjectPattern.hasMatch(source)) {
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Check for JSON array with objects: [{"
|
|
483
|
+
if (source.contains('[{') || source.contains('[ {')) {
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
@override
|
|
491
|
+
void visitBinaryExpression(BinaryExpression node) {
|
|
492
|
+
// Skip if inside logging method or toString
|
|
493
|
+
if (_insideLoggingMethod || _insideToString) {
|
|
494
|
+
super.visitBinaryExpression(node);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Check for string concatenation
|
|
499
|
+
if (node.operator.type.lexeme == '+') {
|
|
500
|
+
final combined = node.toSource();
|
|
501
|
+
|
|
502
|
+
// Check for JavaScript code building
|
|
503
|
+
if (_looksLikeJavaScriptCode(combined) && !S023NoJsonInjectionAnalyzer.hasSafeEncoding(combined)) {
|
|
504
|
+
violations.add(analyzer.createViolation(
|
|
505
|
+
filePath: filePath,
|
|
506
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
507
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
508
|
+
message: 'JavaScript injection risk - avoid string concatenation for JavaScript code',
|
|
509
|
+
));
|
|
510
|
+
super.visitBinaryExpression(node);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Check for JSON structure
|
|
515
|
+
if (_looksLikeJsonStructure(combined)) {
|
|
516
|
+
violations.add(analyzer.createViolation(
|
|
517
|
+
filePath: filePath,
|
|
518
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
519
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
520
|
+
message: 'JSON injection risk - avoid string concatenation for JSON, use jsonEncode()',
|
|
521
|
+
));
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
super.visitBinaryExpression(node);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
@override
|
|
529
|
+
void visitAssignmentExpression(AssignmentExpression node) {
|
|
530
|
+
// Check for innerHTML-like assignments in Flutter web
|
|
531
|
+
final leftSide = node.leftHandSide.toSource().toLowerCase();
|
|
532
|
+
|
|
533
|
+
if (leftSide.contains('innerhtml') || leftSide.contains('outerhtml')) {
|
|
534
|
+
final rightSide = node.rightHandSide;
|
|
535
|
+
final source = rightSide.toSource();
|
|
536
|
+
|
|
537
|
+
if ((rightSide is StringInterpolation || source.contains('+')) &&
|
|
538
|
+
!S023NoJsonInjectionAnalyzer.hasSafeEncoding(source)) {
|
|
539
|
+
violations.add(analyzer.createViolation(
|
|
540
|
+
filePath: filePath,
|
|
541
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
542
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
543
|
+
message: 'HTML injection risk - sanitize user data before assigning to innerHTML/outerHTML',
|
|
544
|
+
));
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
super.visitAssignmentExpression(node);
|
|
549
|
+
}
|
|
550
|
+
}
|