Skip to content

Commit cedfa21

Browse files
committed
feat(security): add secure fallback function analysis (closes #128)
1 parent 99fd966 commit cedfa21

5 files changed

Lines changed: 4545 additions & 151 deletions

File tree

apps/api-service/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@
5656
"@nestjs/swagger": "^8.1.0",
5757
"@types/bcrypt": "^5.0.2",
5858
"@types/nodemailer": "^6.4.14",
59-
"@types/fast-csv": "^4.0.0",
6059
"@types/passport-jwt": "^4.0.1",
6160
"@types/uuid": "^10.0.0",
6261
"@nomicfoundation/hardhat-toolbox": "^6.1.0",

apps/api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
},
3333
"devDependencies": {
3434
"@nestjs/cli": "^10.3.0",
35-
"@nestjs/schematics": "^10.3.0",
35+
"@nestjs/schematics": "^10.2.3",
3636
"@nestjs/testing": "^10.4.22",
3737
"@types/express": "^4.17.21",
3838
"@types/jest": "^30.0.0",

libs/engine/analyzers/solidity-analyzer.ts

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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*(fallback|receive)\s*\(\s*\)\s*[^;{]*\{/;
453+
454+
const hasSensitiveOperation = (body: string): boolean => {
455+
const sensitivePatterns = [
456+
/\bdelegatecall\s*\(/,
457+
/\bcallcode\s*\(/,
458+
/\bselfdestruct\s*\(/,
459+
/\.call\s*\{/,
460+
/\.transfer\s*\(/,
461+
/\.send\s*\(/,
462+
];
463+
464+
return sensitivePatterns.some(pattern => pattern.test(body));
465+
};
466+
467+
const hasStateMutation = (bodyLines: string[]): boolean => {
468+
const localDeclarationPattern = /^\s*(?:u?int(?:8|16|32|64|128|256)?|address|bool|string|bytes(?:\d+)?|bytes|mapping\s*\(|var|memory|storage)\b/;
469+
const stateMutationPattern = /\b[A-Za-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 /\brevert\s*\(/.test(body) || /\brequire\s*\(\s*false\b/.test(body) || /\bassert\s*\(\s*false\b/.test(body);
495+
};
496+
497+
const hasInputValidation = (body: string): boolean => {
498+
const validationPatterns = [
499+
/\brequire\s*\([^)]*msg\.(sender|value|data)[^)]*\)/,
500+
/\bif\s*\([^)]*msg\.(sender|value|data)[^)]*\)\s*\{?\s*revert\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

Comments
 (0)