@@ -81,6 +81,16 @@ export class SolidityAnalyzer extends BaseAnalyzer implements Analyzer {
8181 tags : [ 'security' , 'reentrancy' , 'vulnerability' ] ,
8282 documentationUrl : 'https://docs.gasguard.dev/rules/sol-006' ,
8383 } ,
84+ {
85+ id : 'sol-007' ,
86+ name : 'Insecure Fallback Function' ,
87+ description : 'Fallback/default handlers should reject unknown calls or enforce strict validation' ,
88+ severity : Severity . HIGH ,
89+ category : 'security' ,
90+ enabled : true ,
91+ tags : [ 'security' , 'fallback' , 'receive' , 'validation' ] ,
92+ documentationUrl : 'https://docs.gasguard.dev/rules/sol-007' ,
93+ } ,
8494 ] ;
8595
8696 getName ( ) : string {
@@ -206,6 +216,25 @@ export class SolidityAnalyzer extends BaseAnalyzer implements Analyzer {
206216 } ,
207217 } ) ) ) ;
208218 }
219+
220+ // Rule: sol-007 - Insecure Fallback Function
221+ if ( this . isRuleEnabled ( 'sol-007' , config ) ) {
222+ const insecureFallbacks = this . detectInsecureFallbackFunctions ( code ) ;
223+ findings . push ( ...insecureFallbacks . map ( location => ( {
224+ ruleId : 'sol-007' ,
225+ message : 'Fallback/receive handler is permissive or executes sensitive logic without strict validation' ,
226+ severity : this . getRuleSeverity ( 'sol-007' , config ) ,
227+ location : {
228+ file : filePath ,
229+ ...location ,
230+ } ,
231+ suggestedFix : {
232+ description : 'Keep fallback minimal: reject unknown calls, avoid sensitive logic, and validate accepted ETH transfers' ,
233+ codeSnippet : 'fallback() external payable {\n revert("Unknown function call");\n}' ,
234+ documentationUrl : 'https://docs.gasguard.dev/rules/sol-007' ,
235+ } ,
236+ } ) ) ) ;
237+ }
209238 } catch ( error ) {
210239 errors . push ( {
211240 file : filePath ,
@@ -416,4 +445,133 @@ export class SolidityAnalyzer extends BaseAnalyzer implements Analyzer {
416445
417446 return findings ;
418447 }
448+
449+ private detectInsecureFallbackFunctions ( code : string ) : Array < { startLine : number ; endLine : number } > {
450+ const findings : Array < { startLine : number ; endLine : number } > = [ ] ;
451+ const lines = code . split ( '\n' ) ;
452+ const fallbackDeclarationPattern = / ^ \s * ( f a l l b a c k | r e c e i v e ) \s * \( \s * \) \s * [ ^ ; { ] * \{ / ;
453+
454+ const hasSensitiveOperation = ( body : string ) : boolean => {
455+ const sensitivePatterns = [
456+ / \b d e l e g a t e c a l l \s * \( / ,
457+ / \b c a l l c o d e \s * \( / ,
458+ / \b s e l f d e s t r u c t \s * \( / ,
459+ / \. c a l l \s * \{ / ,
460+ / \. t r a n s f e r \s * \( / ,
461+ / \. s e n d \s * \( / ,
462+ ] ;
463+
464+ return sensitivePatterns . some ( pattern => pattern . test ( body ) ) ;
465+ } ;
466+
467+ const hasStateMutation = ( bodyLines : string [ ] ) : boolean => {
468+ const localDeclarationPattern = / ^ \s * (?: u ? i n t (?: 8 | 1 6 | 3 2 | 6 4 | 1 2 8 | 2 5 6 ) ? | a d d r e s s | b o o l | s t r i n g | b y t e s (?: \d + ) ? | b y t e s | m a p p i n g \s * \( | v a r | m e m o r y | s t o r a g e ) \b / ;
469+ const stateMutationPattern = / \b [ A - Z a - z _ ] \w * (?: \[ [ ^ \] ] + \] ) ? \s * (?: \+ \+ | - - | \+ = | - = | \* = | \/ = | % = | = ) \s * [ ^ = ] / ;
470+
471+ for ( const line of bodyLines ) {
472+ const trimmed = line . trim ( ) ;
473+ if ( ! trimmed || trimmed . startsWith ( '//' ) || trimmed . startsWith ( '/*' ) || trimmed . startsWith ( '*' ) ) {
474+ continue ;
475+ }
476+
477+ if ( trimmed . startsWith ( 'emit ' ) ) {
478+ continue ;
479+ }
480+
481+ if ( localDeclarationPattern . test ( trimmed ) ) {
482+ continue ;
483+ }
484+
485+ if ( stateMutationPattern . test ( trimmed ) ) {
486+ return true ;
487+ }
488+ }
489+
490+ return false ;
491+ } ;
492+
493+ const hasExplicitReject = ( body : string ) : boolean => {
494+ return / \b r e v e r t \s * \( / . test ( body ) || / \b r e q u i r e \s * \( \s * f a l s e \b / . test ( body ) || / \b a s s e r t \s * \( \s * f a l s e \b / . test ( body ) ;
495+ } ;
496+
497+ const hasInputValidation = ( body : string ) : boolean => {
498+ const validationPatterns = [
499+ / \b r e q u i r e \s * \( [ ^ ) ] * m s g \. ( s e n d e r | v a l u e | d a t a ) [ ^ ) ] * \) / ,
500+ / \b i f \s * \( [ ^ ) ] * m s g \. ( s e n d e r | v a l u e | d a t a ) [ ^ ) ] * \) \s * \{ ? \s * r e v e r t \s * \( / ,
501+ ] ;
502+
503+ return validationPatterns . some ( pattern => pattern . test ( body ) ) ;
504+ } ;
505+
506+ const isOnlyEventsOrNoop = ( bodyLines : string [ ] ) : boolean => {
507+ const executable = bodyLines
508+ . map ( line => line . trim ( ) )
509+ . filter ( line => line && line !== '{' && line !== '}' && ! line . startsWith ( '//' ) && ! line . startsWith ( '/*' ) && ! line . startsWith ( '*' ) ) ;
510+
511+ if ( executable . length === 0 ) {
512+ return true ;
513+ }
514+
515+ return executable . every ( line => line . startsWith ( 'emit ' ) || line === ';' ) ;
516+ } ;
517+
518+ for ( let i = 0 ; i < lines . length ; i ++ ) {
519+ const declarationLine = lines [ i ] ;
520+ const declarationMatch = declarationLine . match ( fallbackDeclarationPattern ) ;
521+
522+ if ( ! declarationMatch ) {
523+ continue ;
524+ }
525+
526+ const handlerType = declarationMatch [ 1 ] ;
527+ const startLine = i + 1 ;
528+ let braceDepth = 0 ;
529+ const bodyLines : string [ ] = [ ] ;
530+ let started = false ;
531+
532+ for ( let j = i ; j < lines . length ; j ++ ) {
533+ const currentLine = lines [ j ] ;
534+ const openBraces = ( currentLine . match ( / \{ / g) || [ ] ) . length ;
535+ const closeBraces = ( currentLine . match ( / \} / g) || [ ] ) . length ;
536+
537+ if ( openBraces > 0 ) {
538+ started = true ;
539+ }
540+
541+ if ( started ) {
542+ bodyLines . push ( currentLine ) ;
543+ }
544+
545+ braceDepth += openBraces ;
546+ braceDepth -= closeBraces ;
547+
548+ if ( started && braceDepth === 0 ) {
549+ i = j ;
550+ break ;
551+ }
552+ }
553+
554+ const body = bodyLines . join ( '\n' ) ;
555+ const sensitive = hasSensitiveOperation ( body ) ;
556+ const mutatesState = hasStateMutation ( bodyLines ) ;
557+ const explicitReject = hasExplicitReject ( body ) ;
558+ const validatesInput = hasInputValidation ( body ) ;
559+ const eventsOnly = isOnlyEventsOrNoop ( bodyLines ) ;
560+
561+ const insecureFallback =
562+ handlerType === 'fallback' && ! explicitReject && ! validatesInput && ! eventsOnly ;
563+
564+ const insecureReceive =
565+ handlerType === 'receive' && ( sensitive || mutatesState ) && ! validatesInput ;
566+
567+ if ( sensitive || mutatesState || insecureFallback || insecureReceive ) {
568+ findings . push ( {
569+ startLine,
570+ endLine : startLine ,
571+ } ) ;
572+ }
573+ }
574+
575+ return findings ;
576+ }
419577}
0 commit comments