@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,118 @@
|
|
|
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
|
+
/// S031: Secure Session Cookies
|
|
10
|
+
/// Ensure session cookies have Secure flag enabled
|
|
11
|
+
class S031SecureSessionCookiesAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'S031';
|
|
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 = _S031Visitor(
|
|
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 _S031Visitor extends RecursiveAstVisitor<void> {
|
|
35
|
+
final String filePath;
|
|
36
|
+
final LineInfo lineInfo;
|
|
37
|
+
final List<Violation> violations;
|
|
38
|
+
final S031SecureSessionCookiesAnalyzer analyzer;
|
|
39
|
+
|
|
40
|
+
_S031Visitor({
|
|
41
|
+
required this.filePath,
|
|
42
|
+
required this.lineInfo,
|
|
43
|
+
required this.violations,
|
|
44
|
+
required this.analyzer,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
@override
|
|
48
|
+
void visitInstanceCreationExpression(InstanceCreationExpression node) {
|
|
49
|
+
final typeName = node.constructorName.type.name2.lexeme.toLowerCase();
|
|
50
|
+
|
|
51
|
+
// Check for Cookie creation
|
|
52
|
+
if (typeName == 'cookie') {
|
|
53
|
+
final source = node.toSource().toLowerCase();
|
|
54
|
+
|
|
55
|
+
// Check for session-related cookies
|
|
56
|
+
bool isSessionCookie = source.contains('session') ||
|
|
57
|
+
source.contains('token') ||
|
|
58
|
+
source.contains('auth');
|
|
59
|
+
|
|
60
|
+
if (isSessionCookie) {
|
|
61
|
+
// Check for Secure flag
|
|
62
|
+
bool hasSecureFlag = source.contains('secure: true') ||
|
|
63
|
+
source.contains('secure:true');
|
|
64
|
+
|
|
65
|
+
if (!hasSecureFlag) {
|
|
66
|
+
violations.add(analyzer.createViolation(
|
|
67
|
+
filePath: filePath,
|
|
68
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
69
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
70
|
+
message: 'Session cookie should have Secure flag enabled (secure: true)',
|
|
71
|
+
));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
super.visitInstanceCreationExpression(node);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@override
|
|
80
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
81
|
+
final methodName = node.methodName.name.toLowerCase();
|
|
82
|
+
final source = node.toSource().toLowerCase();
|
|
83
|
+
|
|
84
|
+
// Check for cookie setting methods - must be cookie-related context
|
|
85
|
+
bool isCookieContext = source.contains('cookie') ||
|
|
86
|
+
source.contains('setcookie') ||
|
|
87
|
+
source.contains('set-cookie') ||
|
|
88
|
+
(node.target?.toSource().toLowerCase().contains('cookie') ?? false);
|
|
89
|
+
|
|
90
|
+
// Skip if not cookie context (e.g., interceptors.add(), list.add(), etc.)
|
|
91
|
+
if (!isCookieContext) {
|
|
92
|
+
super.visitMethodInvocation(node);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (methodName == 'setcookie' || methodName == 'set' || methodName == 'add') {
|
|
97
|
+
// Check if it's a session cookie
|
|
98
|
+
bool isSessionCookie = source.contains('session') ||
|
|
99
|
+
source.contains('token') ||
|
|
100
|
+
source.contains('auth');
|
|
101
|
+
|
|
102
|
+
if (isSessionCookie) {
|
|
103
|
+
bool hasSecureFlag = source.contains('secure');
|
|
104
|
+
|
|
105
|
+
if (!hasSecureFlag) {
|
|
106
|
+
violations.add(analyzer.createViolation(
|
|
107
|
+
filePath: filePath,
|
|
108
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
109
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
110
|
+
message: 'Session cookie without Secure flag',
|
|
111
|
+
));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
super.visitMethodInvocation(node);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
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
|
+
/// S032: HttpOnly Session Cookies
|
|
10
|
+
/// Ensure session cookies have HttpOnly flag enabled
|
|
11
|
+
class S032HttpOnlySessionCookiesAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'S032';
|
|
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 = _S032Visitor(
|
|
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 _S032Visitor extends RecursiveAstVisitor<void> {
|
|
35
|
+
final String filePath;
|
|
36
|
+
final LineInfo lineInfo;
|
|
37
|
+
final List<Violation> violations;
|
|
38
|
+
final S032HttpOnlySessionCookiesAnalyzer analyzer;
|
|
39
|
+
|
|
40
|
+
_S032Visitor({
|
|
41
|
+
required this.filePath,
|
|
42
|
+
required this.lineInfo,
|
|
43
|
+
required this.violations,
|
|
44
|
+
required this.analyzer,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
@override
|
|
48
|
+
void visitInstanceCreationExpression(InstanceCreationExpression node) {
|
|
49
|
+
final typeName = node.constructorName.type.name2.lexeme.toLowerCase();
|
|
50
|
+
|
|
51
|
+
if (typeName == 'cookie') {
|
|
52
|
+
final source = node.toSource().toLowerCase();
|
|
53
|
+
|
|
54
|
+
bool isSessionCookie = source.contains('session') ||
|
|
55
|
+
source.contains('token') ||
|
|
56
|
+
source.contains('auth');
|
|
57
|
+
|
|
58
|
+
if (isSessionCookie) {
|
|
59
|
+
bool hasHttpOnly = source.contains('httponly: true') ||
|
|
60
|
+
source.contains('httponly:true');
|
|
61
|
+
|
|
62
|
+
if (!hasHttpOnly) {
|
|
63
|
+
violations.add(analyzer.createViolation(
|
|
64
|
+
filePath: filePath,
|
|
65
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
66
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
67
|
+
message: 'Session cookie should have HttpOnly flag (httpOnly: true) to prevent XSS attacks',
|
|
68
|
+
));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
super.visitInstanceCreationExpression(node);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@override
|
|
77
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
78
|
+
final methodName = node.methodName.name.toLowerCase();
|
|
79
|
+
final source = node.toSource().toLowerCase();
|
|
80
|
+
|
|
81
|
+
// Check for cookie setting methods - must be cookie-related context
|
|
82
|
+
bool isCookieContext = source.contains('cookie') ||
|
|
83
|
+
source.contains('setcookie') ||
|
|
84
|
+
source.contains('set-cookie') ||
|
|
85
|
+
(node.target?.toSource().toLowerCase().contains('cookie') ?? false);
|
|
86
|
+
|
|
87
|
+
// Skip if not cookie context
|
|
88
|
+
if (!isCookieContext) {
|
|
89
|
+
super.visitMethodInvocation(node);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (methodName == 'setcookie' || methodName == 'set' || methodName == 'add') {
|
|
94
|
+
bool isSessionCookie = source.contains('session') ||
|
|
95
|
+
source.contains('token') ||
|
|
96
|
+
source.contains('auth');
|
|
97
|
+
|
|
98
|
+
if (isSessionCookie) {
|
|
99
|
+
bool hasHttpOnly = source.contains('httponly');
|
|
100
|
+
|
|
101
|
+
if (!hasHttpOnly) {
|
|
102
|
+
violations.add(analyzer.createViolation(
|
|
103
|
+
filePath: filePath,
|
|
104
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
105
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
106
|
+
message: 'Session cookie without HttpOnly flag - vulnerable to XSS',
|
|
107
|
+
));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
super.visitMethodInvocation(node);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
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
|
+
/// S033: SameSite Session Cookies
|
|
10
|
+
/// Ensure session cookies have SameSite attribute set
|
|
11
|
+
class S033SameSiteSessionCookiesAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'S033';
|
|
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 = _S033Visitor(
|
|
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 _S033Visitor extends RecursiveAstVisitor<void> {
|
|
35
|
+
final String filePath;
|
|
36
|
+
final LineInfo lineInfo;
|
|
37
|
+
final List<Violation> violations;
|
|
38
|
+
final S033SameSiteSessionCookiesAnalyzer analyzer;
|
|
39
|
+
|
|
40
|
+
_S033Visitor({
|
|
41
|
+
required this.filePath,
|
|
42
|
+
required this.lineInfo,
|
|
43
|
+
required this.violations,
|
|
44
|
+
required this.analyzer,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
@override
|
|
48
|
+
void visitInstanceCreationExpression(InstanceCreationExpression node) {
|
|
49
|
+
final typeName = node.constructorName.type.name2.lexeme.toLowerCase();
|
|
50
|
+
|
|
51
|
+
if (typeName == 'cookie') {
|
|
52
|
+
final source = node.toSource().toLowerCase();
|
|
53
|
+
|
|
54
|
+
bool isSessionCookie = source.contains('session') ||
|
|
55
|
+
source.contains('token') ||
|
|
56
|
+
source.contains('auth');
|
|
57
|
+
|
|
58
|
+
if (isSessionCookie) {
|
|
59
|
+
bool hasSameSite = source.contains('samesite');
|
|
60
|
+
|
|
61
|
+
if (!hasSameSite) {
|
|
62
|
+
violations.add(analyzer.createViolation(
|
|
63
|
+
filePath: filePath,
|
|
64
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
65
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
66
|
+
message: 'Session cookie should have SameSite attribute (Strict or Lax) to prevent CSRF',
|
|
67
|
+
));
|
|
68
|
+
} else if (source.contains('samesite: none') || source.contains("samesite:'none'")) {
|
|
69
|
+
violations.add(analyzer.createViolation(
|
|
70
|
+
filePath: filePath,
|
|
71
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
72
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
73
|
+
message: 'SameSite=None makes cookies vulnerable to CSRF - use Strict or Lax',
|
|
74
|
+
));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
super.visitInstanceCreationExpression(node);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@override
|
|
83
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
84
|
+
final methodName = node.methodName.name.toLowerCase();
|
|
85
|
+
final source = node.toSource().toLowerCase();
|
|
86
|
+
|
|
87
|
+
// Check for cookie setting methods - must be cookie-related context
|
|
88
|
+
bool isCookieContext = source.contains('cookie') ||
|
|
89
|
+
source.contains('setcookie') ||
|
|
90
|
+
source.contains('set-cookie') ||
|
|
91
|
+
(node.target?.toSource().toLowerCase().contains('cookie') ?? false);
|
|
92
|
+
|
|
93
|
+
// Skip if not cookie context
|
|
94
|
+
if (!isCookieContext) {
|
|
95
|
+
super.visitMethodInvocation(node);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (methodName == 'setcookie' || methodName == 'set' || methodName == 'add') {
|
|
100
|
+
bool isSessionCookie = source.contains('session') ||
|
|
101
|
+
source.contains('token') ||
|
|
102
|
+
source.contains('auth');
|
|
103
|
+
|
|
104
|
+
if (isSessionCookie) {
|
|
105
|
+
bool hasSameSite = source.contains('samesite');
|
|
106
|
+
|
|
107
|
+
if (!hasSameSite) {
|
|
108
|
+
violations.add(analyzer.createViolation(
|
|
109
|
+
filePath: filePath,
|
|
110
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
111
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
112
|
+
message: 'Session cookie without SameSite attribute',
|
|
113
|
+
));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
super.visitMethodInvocation(node);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
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
|
+
/// S034: __Host- Prefix Session Cookies
|
|
10
|
+
/// Use __Host- prefix for secure session cookies
|
|
11
|
+
class S034HostPrefixSessionCookiesAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'S034';
|
|
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 = _S034Visitor(
|
|
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 _S034Visitor extends RecursiveAstVisitor<void> {
|
|
35
|
+
final String filePath;
|
|
36
|
+
final LineInfo lineInfo;
|
|
37
|
+
final List<Violation> violations;
|
|
38
|
+
final S034HostPrefixSessionCookiesAnalyzer analyzer;
|
|
39
|
+
|
|
40
|
+
_S034Visitor({
|
|
41
|
+
required this.filePath,
|
|
42
|
+
required this.lineInfo,
|
|
43
|
+
required this.violations,
|
|
44
|
+
required this.analyzer,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
@override
|
|
48
|
+
void visitInstanceCreationExpression(InstanceCreationExpression node) {
|
|
49
|
+
final typeName = node.constructorName.type.name2.lexeme.toLowerCase();
|
|
50
|
+
|
|
51
|
+
if (typeName == 'cookie') {
|
|
52
|
+
final source = node.toSource().toLowerCase();
|
|
53
|
+
|
|
54
|
+
// Check for session-related cookies - more flexible matching
|
|
55
|
+
// Match patterns like: session_id, sessionid, session_token, sessionToken, auth_token, etc.
|
|
56
|
+
bool isSessionCookie = source.contains('session_id') ||
|
|
57
|
+
source.contains('sessionid') ||
|
|
58
|
+
source.contains('session_token') ||
|
|
59
|
+
source.contains('sessiontoken') ||
|
|
60
|
+
source.contains('auth_token') ||
|
|
61
|
+
source.contains('authtoken') ||
|
|
62
|
+
(source.contains('session') && source.contains('token'));
|
|
63
|
+
|
|
64
|
+
if (isSessionCookie) {
|
|
65
|
+
// Check for __Host- or __Secure- prefix
|
|
66
|
+
bool hasSecurePrefix = source.contains('__host-') || source.contains('__secure-');
|
|
67
|
+
|
|
68
|
+
if (!hasSecurePrefix) {
|
|
69
|
+
violations.add(analyzer.createViolation(
|
|
70
|
+
filePath: filePath,
|
|
71
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
72
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
73
|
+
message: 'Consider using __Host- prefix for session cookies for enhanced security',
|
|
74
|
+
));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
super.visitInstanceCreationExpression(node);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@override
|
|
83
|
+
void visitSimpleStringLiteral(SimpleStringLiteral node) {
|
|
84
|
+
final value = node.value.toLowerCase();
|
|
85
|
+
|
|
86
|
+
// Check for cookie name that should have prefix
|
|
87
|
+
if ((value.contains('session') || value.contains('auth')) &&
|
|
88
|
+
(value.contains('id') || value.contains('token'))) {
|
|
89
|
+
if (!value.startsWith('__host-') && !value.startsWith('__secure-')) {
|
|
90
|
+
// Check if this is a cookie name (heuristic)
|
|
91
|
+
final parent = node.parent;
|
|
92
|
+
if (parent != null) {
|
|
93
|
+
final parentSource = parent.toSource().toLowerCase();
|
|
94
|
+
|
|
95
|
+
// Skip non-cookie contexts (JSON keys, API params, annotations, etc.)
|
|
96
|
+
bool isNonCookieContext =
|
|
97
|
+
parentSource.contains('jsonkey') ||
|
|
98
|
+
parentSource.contains('json_key') ||
|
|
99
|
+
parentSource.contains('@jsonkey') ||
|
|
100
|
+
parentSource.contains('request') ||
|
|
101
|
+
parentSource.contains('postinstall') ||
|
|
102
|
+
parentSource.contains('apiresponse') ||
|
|
103
|
+
parentSource.contains('api_response') ||
|
|
104
|
+
parentSource.contains('factory');
|
|
105
|
+
|
|
106
|
+
if (isNonCookieContext) {
|
|
107
|
+
super.visitSimpleStringLiteral(node);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Only report if it's actually cookie-related
|
|
112
|
+
if (parentSource.contains('cookie') ||
|
|
113
|
+
(parentSource.contains('set-cookie') || parentSource.contains('setcookie'))) {
|
|
114
|
+
violations.add(analyzer.createViolation(
|
|
115
|
+
filePath: filePath,
|
|
116
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
117
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
118
|
+
message: 'Session cookie should use __Host- prefix for security',
|
|
119
|
+
));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
super.visitSimpleStringLiteral(node);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@override
|
|
129
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
130
|
+
final methodName = node.methodName.name.toLowerCase();
|
|
131
|
+
|
|
132
|
+
// Check for cookie add/set methods
|
|
133
|
+
if (methodName == 'setcookie' || methodName == 'set' || methodName == 'add') {
|
|
134
|
+
final source = node.toSource().toLowerCase();
|
|
135
|
+
|
|
136
|
+
// Check for session-related cookies
|
|
137
|
+
bool isSessionCookie = source.contains('session_id') ||
|
|
138
|
+
source.contains('sessionid') ||
|
|
139
|
+
source.contains('session_token') ||
|
|
140
|
+
source.contains('sessiontoken') ||
|
|
141
|
+
source.contains('auth_token') ||
|
|
142
|
+
source.contains('authtoken');
|
|
143
|
+
|
|
144
|
+
if (isSessionCookie) {
|
|
145
|
+
bool hasSecurePrefix = source.contains('__host-') || source.contains('__secure-');
|
|
146
|
+
|
|
147
|
+
if (!hasSecurePrefix) {
|
|
148
|
+
violations.add(analyzer.createViolation(
|
|
149
|
+
filePath: filePath,
|
|
150
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
151
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
152
|
+
message: 'Session cookie should use __Host- prefix',
|
|
153
|
+
));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
super.visitMethodInvocation(node);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
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
|
+
/// S035: Host separate applications on different hostnames
|
|
10
|
+
/// Detect patterns suggesting multiple apps sharing same origin (path-based routing)
|
|
11
|
+
class S035SeparateAppHostnamesAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'S035';
|
|
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 = _S035Visitor(
|
|
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 _S035Visitor extends RecursiveAstVisitor<void> {
|
|
35
|
+
final String filePath;
|
|
36
|
+
final LineInfo lineInfo;
|
|
37
|
+
final List<Violation> violations;
|
|
38
|
+
final S035SeparateAppHostnamesAnalyzer analyzer;
|
|
39
|
+
|
|
40
|
+
// Patterns indicating path-based multi-app on same origin
|
|
41
|
+
static final _pathBasedAppPatterns = [
|
|
42
|
+
RegExp(r'/app[12]/'),
|
|
43
|
+
RegExp(r'/admin/'),
|
|
44
|
+
RegExp(r'/portal/'),
|
|
45
|
+
RegExp(r'/dashboard/'),
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
_S035Visitor({
|
|
49
|
+
required this.filePath,
|
|
50
|
+
required this.lineInfo,
|
|
51
|
+
required this.violations,
|
|
52
|
+
required this.analyzer,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
@override
|
|
56
|
+
void visitSimpleStringLiteral(SimpleStringLiteral node) {
|
|
57
|
+
final value = node.value;
|
|
58
|
+
|
|
59
|
+
// Check for patterns suggesting path-based multi-app routing
|
|
60
|
+
// e.g., "https://example.com/app1/api" or "https://example.com/admin/"
|
|
61
|
+
if (_isPathBasedMultiAppUrl(value)) {
|
|
62
|
+
violations.add(analyzer.createViolation(
|
|
63
|
+
filePath: filePath,
|
|
64
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
65
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
66
|
+
message: 'Path-based app routing detected. Consider using separate hostnames for security isolation: "$value"',
|
|
67
|
+
));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
super.visitSimpleStringLiteral(node);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@override
|
|
74
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
75
|
+
final methodName = node.methodName.name.toLowerCase();
|
|
76
|
+
final source = node.toSource().toLowerCase();
|
|
77
|
+
|
|
78
|
+
// Check for route registration with path-based app separation
|
|
79
|
+
if (methodName == 'route' || methodName == 'addroute' || methodName == 'use') {
|
|
80
|
+
for (final pattern in _pathBasedAppPatterns) {
|
|
81
|
+
if (pattern.hasMatch(source)) {
|
|
82
|
+
violations.add(analyzer.createViolation(
|
|
83
|
+
filePath: filePath,
|
|
84
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
85
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
86
|
+
message: 'Path-based application routing. Consider separate hostnames for same-origin policy benefits',
|
|
87
|
+
));
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
super.visitMethodInvocation(node);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
bool _isPathBasedMultiAppUrl(String value) {
|
|
97
|
+
// Skip if not a URL
|
|
98
|
+
if (!value.startsWith('http://') && !value.startsWith('https://')) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Skip localhost/development URLs
|
|
103
|
+
if (value.contains('localhost') || value.contains('127.0.0.1')) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Check for path-based app patterns after the domain
|
|
108
|
+
// e.g., https://example.com/app1/... or https://example.com/admin/...
|
|
109
|
+
for (final pattern in _pathBasedAppPatterns) {
|
|
110
|
+
if (pattern.hasMatch(value)) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|