Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions lib/src/analyzers/code/rules/auth/avoid_empty_catch.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import 'package:analyzer/analysis_rule/analysis_rule.dart';
import 'package:analyzer/analysis_rule/rule_context.dart';
import 'package:analyzer/analysis_rule/rule_visitor_registry.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/error/error.dart';

/// Detects empty catch blocks that silently swallow exceptions.
///
/// Empty catch blocks can hide security issues and make debugging difficult.
class AvoidEmptyCatch extends AnalysisRule {
AvoidEmptyCatch()
: super(
name: 'avoid_empty_catch',
description:
'Avoid empty catch blocks that silently swallow exceptions.',
);

static const LintCode code = LintCode(
'avoid_empty_catch',
'Empty catch block silently swallows exceptions.',
correctionMessage:
'Handle the exception, log it, or rethrow it. '
'Never silently ignore exceptions.',
);

@override
DiagnosticCode get diagnosticCode => code;

@override
void registerNodeProcessors(
RuleVisitorRegistry registry,
RuleContext context,
) {
final visitor = _Visitor(rule: this);
registry.addCatchClause(this, visitor);
}
}

class _Visitor extends SimpleAstVisitor<void> {
_Visitor({required this.rule});
final AnalysisRule rule;

@override
void visitCatchClause(CatchClause node) {
final body = node.body;
final statements = body.statements;

// Check if the catch block is empty or contains only comments
if (statements.isEmpty) {
rule.reportAtNode(node);
return;
}

// Check if all statements are empty statements or just have no effect
final hasEffectiveStatement = statements.any((stmt) {
// Empty statement
if (stmt is EmptyStatement) return false;

// Expression statement with just a variable reference (no effect)
if (stmt is ExpressionStatement) {
final expr = stmt.expression;
// Check for rethrow, throw, return, method calls, etc.
if (expr is RethrowExpression) return true;
if (expr is ThrowExpression) return true;
if (expr is MethodInvocation) return true;
if (expr is FunctionExpressionInvocation) return true;
if (expr is AssignmentExpression) return true;
// Simple identifier alone has no effect
if (expr is SimpleIdentifier) return false;
}

if (stmt is ReturnStatement) return true;
if (stmt is IfStatement) return true;
if (stmt is Block) return true;
if (stmt is VariableDeclarationStatement) return true;

return false;
});

if (!hasEffectiveStatement) {
rule.reportAtNode(node);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import 'package:analyzer/analysis_rule/analysis_rule.dart';
import 'package:analyzer/analysis_rule/rule_context.dart';
import 'package:analyzer/analysis_rule/rule_visitor_registry.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/error/error.dart';

/// Detects dynamic SQL queries with string interpolation.
///
/// CWE-89: SQL Injection
class AvoidDynamicSqlQueries extends AnalysisRule {
AvoidDynamicSqlQueries()
: super(
name: 'avoid_dynamic_sql_queries',
description:
'Avoid dynamic SQL queries with string interpolation to '
'prevent SQL injection.',
);

static const LintCode code = LintCode(
'avoid_dynamic_sql_queries',
'Avoid dynamic SQL queries with string interpolation.',
correctionMessage:
'Use parameterized queries or prepared statements '
'instead of string interpolation.',
);

@override
DiagnosticCode get diagnosticCode => code;

@override
void registerNodeProcessors(
RuleVisitorRegistry registry,
RuleContext context,
) {
final visitor = _Visitor(rule: this);
registry.addStringInterpolation(this, visitor);
}
}

class _Visitor extends SimpleAstVisitor<void> {
_Visitor({required this.rule});
final AnalysisRule rule;

static const _sqlKeywords = [
'SELECT',
'INSERT',
'UPDATE',
'DELETE',
'DROP',
'CREATE',
'ALTER',
'TRUNCATE',
];

@override
void visitStringInterpolation(StringInterpolation node) {
// Get the full string content
final buffer = StringBuffer();
for (final element in node.elements) {
if (element is InterpolationString) {
buffer.write(element.value);
} else if (element is InterpolationExpression) {
buffer.write('\${}'); // Placeholder for interpolation
}
}
final stringValue = buffer.toString().toUpperCase();

// Check if the string looks like a SQL query
final hasSqlKeyword = _sqlKeywords.any(
(keyword) => stringValue.contains(keyword),
);

// Check if it has interpolation (not just a static SQL string)
final hasInterpolation =
node.elements.any((e) => e is InterpolationExpression);

if (hasSqlKeyword && hasInterpolation) {
rule.reportAtNode(node);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import 'package:analyzer/analysis_rule/analysis_rule.dart';
import 'package:analyzer/analysis_rule/rule_context.dart';
import 'package:analyzer/analysis_rule/rule_visitor_registry.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/error/error.dart';

/// Detects logging/printing of sensitive data.
///
/// CWE-532: Insertion of Sensitive Information into Log File
class AvoidLoggingSensitiveData extends AnalysisRule {
AvoidLoggingSensitiveData()
: super(
name: 'avoid_logging_sensitive_data',
description: 'Avoid logging sensitive data like passwords or tokens.',
);

static const LintCode code = LintCode(
'avoid_logging_sensitive_data',
'Avoid logging sensitive data like passwords or tokens.',
correctionMessage:
'Remove sensitive data from log statements or use '
'redaction before logging.',
);

@override
DiagnosticCode get diagnosticCode => code;

@override
void registerNodeProcessors(
RuleVisitorRegistry registry,
RuleContext context,
) {
final visitor = _Visitor(rule: this);
registry.addMethodInvocation(this, visitor);
}
}

class _Visitor extends SimpleAstVisitor<void> {
_Visitor({required this.rule});
final AnalysisRule rule;

static const _sensitivePatterns = [
'password',
'passwd',
'token',
'secret',
'apikey',
'api_key',
'credential',
'privatekey',
'private_key',
'accesstoken',
'access_token',
'authtoken',
'auth_token',
'auth',
];

static const _loggingFunctions = [
'print',
'debugPrint',
'log',
'logger',
'info',
'debug',
'warning',
'error',
'severe',
];

@override
void visitMethodInvocation(MethodInvocation node) {
final methodName = node.methodName.name.toLowerCase();

// Check if this is a logging function
if (!_loggingFunctions.any((f) => methodName.contains(f))) {
return;
}

// Check arguments for sensitive variable names
for (final arg in node.argumentList.arguments) {
if (arg is SimpleIdentifier) {
final argName = arg.name.toLowerCase();
if (_sensitivePatterns.any((p) => argName.contains(p))) {
rule.reportAtNode(node);
return;
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import 'package:analyzer/analysis_rule/analysis_rule.dart';
import 'package:analyzer/analysis_rule/rule_context.dart';
import 'package:analyzer/analysis_rule/rule_visitor_registry.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/error/error.dart';

/// Detects bypassing of certificate pinning/validation.
///
/// CWE-295: Improper Certificate Validation
class AvoidCertificatePinningBypass extends AnalysisRule {
AvoidCertificatePinningBypass()
: super(
name: 'avoid_certificate_pinning_bypass',
description: 'Avoid bypassing SSL certificate validation.',
);

static const LintCode code = LintCode(
'avoid_certificate_pinning_bypass',
'Avoid bypassing SSL certificate validation.',
correctionMessage:
'Implement proper certificate validation instead of '
'returning true unconditionally.',
);

@override
DiagnosticCode get diagnosticCode => code;

@override
void registerNodeProcessors(
RuleVisitorRegistry registry,
RuleContext context,
) {
final visitor = _Visitor(rule: this);
registry.addAssignmentExpression(this, visitor);
}
}

class _Visitor extends SimpleAstVisitor<void> {
_Visitor({required this.rule});
final AnalysisRule rule;

@override
void visitAssignmentExpression(AssignmentExpression node) {
// Check for badCertificateCallback assignment
final leftSide = node.leftHandSide;
String? propertyName;

if (leftSide is PropertyAccess) {
propertyName = leftSide.propertyName.name;
} else if (leftSide is PrefixedIdentifier) {
propertyName = leftSide.identifier.name;
}

if (propertyName != 'badCertificateCallback') {
return;
}

// Check if the right side is a function that returns true
final rightSide = node.rightHandSide;
if (_returnsTrueUnconditionally(rightSide)) {
rule.reportAtNode(node);
}
}

bool _returnsTrueUnconditionally(Expression expr) {
if (expr is FunctionExpression) {
final body = expr.body;
if (body is ExpressionFunctionBody) {
final expression = body.expression;
if (expression is BooleanLiteral && expression.value) {
return true;
}
}
}
return false;
}
}
25 changes: 22 additions & 3 deletions lib/src/analyzers/code/rules/rules.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
import 'package:analyzer/analysis_rule/analysis_rule.dart';
import 'package:dart_shield/src/analyzers/code/rules/auth/avoid_empty_catch.dart';
import 'package:dart_shield/src/analyzers/code/rules/cryptography/avoid_weak_hashing.dart';
import 'package:dart_shield/src/analyzers/code/rules/cryptography/prefer_secure_random.dart';
import 'package:dart_shield/src/analyzers/code/rules/injection/avoid_dynamic_sql_queries.dart';
import 'package:dart_shield/src/analyzers/code/rules/logging/avoid_logging_sensitive_data.dart';
import 'package:dart_shield/src/analyzers/code/rules/network/avoid_certificate_pinning_bypass.dart';
import 'package:dart_shield/src/analyzers/code/rules/network/avoid_harcoded_urls.dart';
import 'package:dart_shield/src/analyzers/code/rules/network/prefer_https_over_http.dart';
import 'package:dart_shield/src/analyzers/code/rules/secrets/avoid_hardcoded_secrets.dart';
import 'package:dart_shield/src/analyzers/code/rules/storage/avoid_insecure_file_storage.dart';
import 'package:dart_shield/src/analyzers/code/rules/storage/avoid_shared_preferences_for_secrets.dart';

final List<AnalysisRule> rules = [
AvoidHardcodedSecrets(),
AvoidHardcodedUrls(),
// Auth
AvoidEmptyCatch(),
// Cryptography
AvoidWeakHashing(),
PreferHttpsOverHttp(),
PreferSecureRandom(),
// Injection
AvoidDynamicSqlQueries(),
// Logging
AvoidLoggingSensitiveData(),
// Network
AvoidCertificatePinningBypass(),
AvoidHardcodedUrls(),
PreferHttpsOverHttp(),
// Secrets
AvoidHardcodedSecrets(),
// Storage
AvoidInsecureFileStorage(),
AvoidSharedPreferencesForSecrets(),
];
Loading
Loading