@elytrasec/engine 0.3.2 → 0.4.1
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/dist/index.js +417 -44
- package/package.json +13 -9
- package/dist/index.d.ts +0 -1358
package/dist/index.js
CHANGED
|
@@ -2957,12 +2957,43 @@ var securityRules = [
|
|
|
2957
2957
|
title: "Hardcoded secret or credential",
|
|
2958
2958
|
description: "A string that looks like a password, API key, or token is hardcoded in source code.",
|
|
2959
2959
|
suggestion: "Move the secret to an environment variable or a secrets manager. Rotate the exposed credential immediately.",
|
|
2960
|
-
|
|
2960
|
+
// `token` removed from the keyword list — too ambiguous in crypto context (tokenAddress,
|
|
2961
|
+
// token0, token1, tokenContract, tokenFactory all match and are public addresses, not secrets).
|
|
2962
|
+
// `auth_token`, `accessToken`, `bearerToken`, `jwt_token` etc. remain via specific patterns below.
|
|
2963
|
+
pattern: /(?:password|secret|apikey|api_key|api_secret|auth[_-]?token|access[_-]?token|bearer[_-]?token|jwt[_-]?secret|private[_-]?key|client_secret)\s*[:=]\s*["'](?!changeme|password|your-|<|TODO|xxx|test|example|placeholder|REPLACE|0x[0-9a-fA-F]{40}["'])[^"']{8,}["']/i,
|
|
2961
2964
|
severity: "critical",
|
|
2962
2965
|
category: "security",
|
|
2963
2966
|
confidence: "medium",
|
|
2964
2967
|
languages: ALL
|
|
2965
2968
|
},
|
|
2969
|
+
{
|
|
2970
|
+
// Crypto-native: a raw 32-byte (64-hex) value assigned to a *private*-key-shaped name.
|
|
2971
|
+
// Wallet/contract addresses (40 hex) are public — never flag those.
|
|
2972
|
+
// Hashes / UIDs / merkle roots (64 hex) on a `*Hash`, `*Uid`, `*Root` LHS are public — don't flag.
|
|
2973
|
+
// Only flag when the LHS specifically suggests a SIGNING KEY.
|
|
2974
|
+
id: "cp-sec-private-key-hex",
|
|
2975
|
+
title: "Hardcoded EVM private key",
|
|
2976
|
+
description: "A 64-character hex value is assigned to a variable whose name implies it's a signing key. If real, this is a wallet private key \u2014 anyone who reads the source can drain the wallet.",
|
|
2977
|
+
suggestion: "Move to an environment variable, KMS, or hardware wallet immediately. Rotate the key (transfer all assets to a new wallet and never use this key again).",
|
|
2978
|
+
pattern: /\b(?:private[_-]?key|signing[_-]?key|wallet[_-]?key|signer[_-]?key|deployer[_-]?key|mnemonic[_-]?seed|raw[_-]?key|eas_private_key|x402_wallet_key)\s*[:=]\s*["']?0x[0-9a-fA-F]{64}["']?/i,
|
|
2979
|
+
severity: "critical",
|
|
2980
|
+
category: "security",
|
|
2981
|
+
confidence: "high",
|
|
2982
|
+
languages: ALL
|
|
2983
|
+
},
|
|
2984
|
+
{
|
|
2985
|
+
// BIP-39 seed phrase — 12 or 24 lowercase English words in a row.
|
|
2986
|
+
// Any leak of these = full wallet compromise. Targets the common assignment pattern.
|
|
2987
|
+
id: "cp-sec-mnemonic-phrase",
|
|
2988
|
+
title: "Hardcoded BIP-39 mnemonic / seed phrase",
|
|
2989
|
+
description: "A string with 12 or 24 lowercase words looks like a BIP-39 mnemonic. A leaked seed phrase compromises the entire HD wallet and every account derived from it.",
|
|
2990
|
+
suggestion: "Never store mnemonics in source. Use a secure secret manager. If exposed, immediately transfer all assets out of every derived account, never reuse the seed.",
|
|
2991
|
+
pattern: /(?:mnemonic|seed[_-]?phrase|secret[_-]?phrase|backup[_-]?phrase)\s*[:=]\s*["'](?:[a-z]{3,8}\s+){11,23}[a-z]{3,8}["']/i,
|
|
2992
|
+
severity: "critical",
|
|
2993
|
+
category: "security",
|
|
2994
|
+
confidence: "high",
|
|
2995
|
+
languages: ALL
|
|
2996
|
+
},
|
|
2966
2997
|
{
|
|
2967
2998
|
id: "cp-sec-hardcoded-ip",
|
|
2968
2999
|
title: "Hardcoded IP address",
|
|
@@ -3074,7 +3105,7 @@ var securityRules = [
|
|
|
3074
3105
|
title: "Form without CSRF protection",
|
|
3075
3106
|
description: "HTML form with method=POST but no visible CSRF token field. This may be vulnerable to cross-site request forgery.",
|
|
3076
3107
|
suggestion: "Add a hidden CSRF token field to the form or use a framework-provided CSRF middleware.",
|
|
3077
|
-
|
|
3108
|
+
multilinePattern: /method\s*=\s*["']post["'][^>]*>(?![\s\S]{0,500}csrf)/i,
|
|
3078
3109
|
severity: "medium",
|
|
3079
3110
|
category: "security",
|
|
3080
3111
|
confidence: "low",
|
|
@@ -3130,7 +3161,7 @@ var securityRules = [
|
|
|
3130
3161
|
title: "XML parsing without entity restriction (XXE)",
|
|
3131
3162
|
description: "XML parsers that allow external entities can be exploited to read local files or perform SSRF.",
|
|
3132
3163
|
suggestion: "Disable external entity processing: set noent: false, or use defusedxml in Python.",
|
|
3133
|
-
|
|
3164
|
+
multilinePattern: /(?:parseXml|parseString|DOMParser|xml2js|libxmljs|XMLParser)\s*\((?![\s\S]*(?:noent:\s*false|resolve_entities:\s*false))/,
|
|
3134
3165
|
severity: "high",
|
|
3135
3166
|
category: "security",
|
|
3136
3167
|
confidence: "medium",
|
|
@@ -3180,7 +3211,7 @@ var securityRules = [
|
|
|
3180
3211
|
title: "Mass assignment via spread of user input",
|
|
3181
3212
|
description: "Spreading req.body directly into a database create/update allows attackers to set arbitrary fields (e.g. isAdmin).",
|
|
3182
3213
|
suggestion: "Explicitly pick allowed fields instead of spreading the entire request body.",
|
|
3183
|
-
|
|
3214
|
+
multilinePattern: /(?:create|update|insert|save|upsert)\s*\(\s*\{[\s\S]{0,50}\.\.\.(?:req\.body|req\.query|body|input)\b/,
|
|
3184
3215
|
severity: "high",
|
|
3185
3216
|
category: "security",
|
|
3186
3217
|
confidence: "medium",
|
|
@@ -3247,7 +3278,7 @@ var securityRules = [
|
|
|
3247
3278
|
title: "Cookie set without security flags",
|
|
3248
3279
|
description: "Cookies set without Secure, HttpOnly, or SameSite flags are vulnerable to interception and XSS theft.",
|
|
3249
3280
|
suggestion: "Set cookies with { secure: true, httpOnly: true, sameSite: 'strict' } flags.",
|
|
3250
|
-
|
|
3281
|
+
multilinePattern: /(?:res\.cookie|setCookie|set-cookie|document\.cookie)\s*(?:\(|=)(?![\s\S]{0,100}(?:secure|httpOnly|SameSite))/i,
|
|
3251
3282
|
severity: "medium",
|
|
3252
3283
|
category: "security",
|
|
3253
3284
|
confidence: "medium",
|
|
@@ -3275,7 +3306,7 @@ var securityRules = [
|
|
|
3275
3306
|
title: "CORS credentials with wildcard origin",
|
|
3276
3307
|
description: "Enabling credentials with a wildcard or reflected origin allows any site to make authenticated cross-origin requests.",
|
|
3277
3308
|
suggestion: "When using credentials: true, specify explicit trusted origins instead of '*' or reflecting the Origin header.",
|
|
3278
|
-
|
|
3309
|
+
multilinePattern: /credentials\s*:\s*true[\s\S]{0,200}origin\s*:\s*(?:["']\*["']|req\.headers\.origin|true)/,
|
|
3279
3310
|
severity: "high",
|
|
3280
3311
|
category: "security",
|
|
3281
3312
|
confidence: "high",
|
|
@@ -3309,13 +3340,13 @@ var securityRules = [
|
|
|
3309
3340
|
var solidityRules2 = [
|
|
3310
3341
|
{
|
|
3311
3342
|
id: "cp-sol-reentrancy",
|
|
3312
|
-
title: "Potential reentrancy \u2014 external call
|
|
3313
|
-
description: "An external call
|
|
3314
|
-
suggestion: "Follow
|
|
3315
|
-
pattern: /\.
|
|
3316
|
-
severity: "
|
|
3343
|
+
title: "Potential reentrancy \u2014 review external call sequencing",
|
|
3344
|
+
description: "An external call via `.call{value:...}` is present. The classic reentrancy pattern requires the call to occur BEFORE a state update \u2014 a same-line regex can't verify the order across multiple lines, so this is flagged for manual or AI review. `.send` and `.transfer` are intentionally not flagged because their 2300-gas stipend prevents reentrancy except in pathological cases (post-EIP-1884 gas changes).",
|
|
3345
|
+
suggestion: "Follow checks-effects-interactions: update state before external calls. Or use OpenZeppelin's ReentrancyGuard. Confirm by reading the surrounding function or running AI deep-review.",
|
|
3346
|
+
pattern: /\.call\s*\{(?:[^}]*value[^}]*)?\}/,
|
|
3347
|
+
severity: "high",
|
|
3317
3348
|
category: "security",
|
|
3318
|
-
confidence: "
|
|
3349
|
+
confidence: "low",
|
|
3319
3350
|
languages: SOL
|
|
3320
3351
|
},
|
|
3321
3352
|
{
|
|
@@ -3399,12 +3430,12 @@ var solidityRules2 = [
|
|
|
3399
3430
|
{
|
|
3400
3431
|
id: "cp-sol-assembly",
|
|
3401
3432
|
title: "Inline assembly usage",
|
|
3402
|
-
description: "Inline assembly bypasses Solidity's safety checks (
|
|
3403
|
-
suggestion: "
|
|
3433
|
+
description: "Inline assembly bypasses Solidity's safety checks. Routine in production DeFi for gas optimization (transient storage, packed structs, math) \u2014 flagged informationally so reviewers can verify the assembly block is justified.",
|
|
3434
|
+
suggestion: "Confirm the assembly block is necessary and audited. Document the storage slots / opcodes used.",
|
|
3404
3435
|
pattern: /\bassembly\s*\{/,
|
|
3405
|
-
severity: "
|
|
3406
|
-
category: "
|
|
3407
|
-
confidence: "
|
|
3436
|
+
severity: "info",
|
|
3437
|
+
category: "solidity",
|
|
3438
|
+
confidence: "low",
|
|
3408
3439
|
languages: SOL
|
|
3409
3440
|
},
|
|
3410
3441
|
{
|
|
@@ -3421,11 +3452,11 @@ var solidityRules2 = [
|
|
|
3421
3452
|
{
|
|
3422
3453
|
id: "cp-sol-unchecked-math",
|
|
3423
3454
|
title: "Unchecked arithmetic block",
|
|
3424
|
-
description: "
|
|
3425
|
-
suggestion: "
|
|
3455
|
+
description: "`unchecked` disables Solidity 0.8+ overflow checks. Usually intentional for gas savings on loop counters and proven-safe math, but worth confirming each block has a documented invariant.",
|
|
3456
|
+
suggestion: "Add a comment explaining why overflow is impossible in this block. Consider whether the gas savings justify the risk.",
|
|
3426
3457
|
pattern: /unchecked\s*\{/,
|
|
3427
|
-
severity: "
|
|
3428
|
-
category: "
|
|
3458
|
+
severity: "low",
|
|
3459
|
+
category: "solidity",
|
|
3429
3460
|
confidence: "low",
|
|
3430
3461
|
languages: SOL
|
|
3431
3462
|
},
|
|
@@ -3446,7 +3477,7 @@ var solidityRules2 = [
|
|
|
3446
3477
|
title: "balanceOf used for pricing without flash loan guards",
|
|
3447
3478
|
description: "Using balanceOf() for pricing calculations is vulnerable to flash loan manipulation.",
|
|
3448
3479
|
suggestion: "Use time-weighted average prices (TWAP) or Chainlink oracles instead of spot balanceOf for pricing.",
|
|
3449
|
-
|
|
3480
|
+
multilinePattern: /balanceOf\s*\([^)]*\)[\s\S]{0,20}(?:\*|\/)/,
|
|
3450
3481
|
severity: "high",
|
|
3451
3482
|
category: "security",
|
|
3452
3483
|
confidence: "medium",
|
|
@@ -3455,25 +3486,25 @@ var solidityRules2 = [
|
|
|
3455
3486
|
{
|
|
3456
3487
|
id: "cp-sol-storage-collision",
|
|
3457
3488
|
title: "delegatecall with state variables \u2014 storage collision risk",
|
|
3458
|
-
description: "Using delegatecall in a contract with state variables can cause storage layout collisions with the implementation contract.",
|
|
3489
|
+
description: "Using delegatecall in a contract with state variables can cause storage layout collisions with the implementation contract. Common in proxy patterns where it's intentional \u2014 verify EIP-1967 storage slots are used. High false-positive rate on standard proxy implementations.",
|
|
3459
3490
|
suggestion: "Use EIP-1967 proxy pattern with standardized storage slots, or ensure identical storage layouts.",
|
|
3460
3491
|
pattern: /delegatecall\s*\(/,
|
|
3461
3492
|
multilinePattern: /(?:uint|int|address|mapping|bytes|string|bool)\s+(?:public|private|internal)?\s*\w+[\s\S]{0,2000}delegatecall\s*\(/g,
|
|
3462
|
-
severity: "
|
|
3493
|
+
severity: "medium",
|
|
3463
3494
|
category: "security",
|
|
3464
|
-
confidence: "
|
|
3495
|
+
confidence: "low",
|
|
3465
3496
|
languages: SOL
|
|
3466
3497
|
},
|
|
3467
3498
|
{
|
|
3468
3499
|
id: "cp-sol-missing-event",
|
|
3469
3500
|
title: "State change without event emission",
|
|
3470
|
-
description: "State-changing functions should emit events for off-chain indexing and
|
|
3471
|
-
suggestion: "Add an event emission after state changes
|
|
3501
|
+
description: "State-changing functions should emit events for off-chain indexing. Best practice for indexers and UI, not a security vulnerability \u2014 flagged informationally.",
|
|
3502
|
+
suggestion: "Add an event emission after state changes if downstream indexers / UI need to track them.",
|
|
3472
3503
|
pattern: /(?!)/,
|
|
3473
3504
|
// never matches per-line; use multiline only
|
|
3474
3505
|
multilinePattern: /function\s+\w+\s*\([^)]*\)\s*(?:external|public)[^{]*\{(?:(?!emit\s)[\s\S]){1,500}\w+\s*=[^=]/g,
|
|
3475
|
-
severity: "
|
|
3476
|
-
category: "
|
|
3506
|
+
severity: "info",
|
|
3507
|
+
category: "quality",
|
|
3477
3508
|
confidence: "low",
|
|
3478
3509
|
languages: SOL
|
|
3479
3510
|
}
|
|
@@ -3506,7 +3537,7 @@ var javaRules = [
|
|
|
3506
3537
|
title: "XPath injection via string concatenation",
|
|
3507
3538
|
description: "Building XPath queries with string concatenation allows injection attacks.",
|
|
3508
3539
|
suggestion: "Use XPath parameterization via XPathExpression or sanitize input.",
|
|
3509
|
-
|
|
3540
|
+
multilinePattern: /(?:XPath|xpath)[\s\S]{0,50}\.(?:evaluate|compile)\s*\(\s*(?:["'][^"']*["']\s*\+|.*\+\s*["'])/,
|
|
3510
3541
|
severity: "high",
|
|
3511
3542
|
category: "security",
|
|
3512
3543
|
confidence: "medium",
|
|
@@ -3900,7 +3931,7 @@ var performanceRules = [
|
|
|
3900
3931
|
title: "Potential N+1 query \u2014 database call inside a loop",
|
|
3901
3932
|
description: "A database query inside a loop makes N separate round-trips instead of 1 batch query. This causes severe performance degradation at scale.",
|
|
3902
3933
|
suggestion: "Batch the queries: collect all IDs first, then execute a single WHERE IN query.",
|
|
3903
|
-
|
|
3934
|
+
multilinePattern: /(?:for|while|\.forEach|\.map)\s*\([\s\S]*?(?:\.find\(|\.findOne\(|\.findUnique\(|\.query\(|\.execute\(|SELECT\b)/,
|
|
3904
3935
|
severity: "high",
|
|
3905
3936
|
category: "performance",
|
|
3906
3937
|
confidence: "medium",
|
|
@@ -3911,7 +3942,7 @@ var performanceRules = [
|
|
|
3911
3942
|
title: "Nested iteration \u2014 Array search inside a loop",
|
|
3912
3943
|
description: "Using Array.find/filter/some/indexOf inside a loop is O(n*m). Use a Map or Set for O(n) lookups.",
|
|
3913
3944
|
suggestion: "Build a Map or Set from the inner array before the loop, then use .get()/.has() for O(1) lookup.",
|
|
3914
|
-
|
|
3945
|
+
multilinePattern: /(?:for|while|\.forEach|\.map)\s*\([\s\S]*?\.(?:find|filter|some|indexOf|includes)\s*\(/,
|
|
3915
3946
|
severity: "low",
|
|
3916
3947
|
category: "performance",
|
|
3917
3948
|
confidence: "medium",
|
|
@@ -3934,7 +3965,7 @@ var performanceRules = [
|
|
|
3934
3965
|
title: "RegExp construction inside a loop",
|
|
3935
3966
|
description: "Creating a new RegExp object on every iteration is wasteful. Regex compilation is expensive.",
|
|
3936
3967
|
suggestion: "Move the RegExp construction outside the loop and reuse the compiled pattern.",
|
|
3937
|
-
|
|
3968
|
+
multilinePattern: /(?:for|while|\.forEach|\.map)\s*\([\s\S]*?new\s+RegExp\s*\(/,
|
|
3938
3969
|
severity: "low",
|
|
3939
3970
|
category: "performance",
|
|
3940
3971
|
confidence: "medium",
|
|
@@ -3945,7 +3976,7 @@ var performanceRules = [
|
|
|
3945
3976
|
title: "JSON.parse inside a loop",
|
|
3946
3977
|
description: "Parsing JSON inside a loop is CPU-intensive. If the same data is re-parsed, cache the result.",
|
|
3947
3978
|
suggestion: "Parse the JSON once before the loop and reuse the result, or use streaming JSON parsing for large datasets.",
|
|
3948
|
-
|
|
3979
|
+
multilinePattern: /(?:for|while|\.forEach|\.map)\s*\([\s\S]*?JSON\.parse\s*\(/,
|
|
3949
3980
|
severity: "low",
|
|
3950
3981
|
category: "performance",
|
|
3951
3982
|
confidence: "medium",
|
|
@@ -3956,7 +3987,7 @@ var performanceRules = [
|
|
|
3956
3987
|
title: "Sequential await inside a loop",
|
|
3957
3988
|
description: "Using await inside a loop executes async operations sequentially. Independent operations can run in parallel with Promise.all.",
|
|
3958
3989
|
suggestion: "Collect promises in an array and use Promise.all() or Promise.allSettled() for parallel execution.",
|
|
3959
|
-
|
|
3990
|
+
multilinePattern: /(?:for|while)\s*\([\s\S]*?await\s+/,
|
|
3960
3991
|
severity: "low",
|
|
3961
3992
|
category: "performance",
|
|
3962
3993
|
confidence: "low",
|
|
@@ -4615,12 +4646,12 @@ var eip7702Rules = [
|
|
|
4615
4646
|
{
|
|
4616
4647
|
id: "cp-sol-7702-unguarded-init",
|
|
4617
4648
|
title: "EIP-7702 delegation: unguarded initializer",
|
|
4618
|
-
description: "Contracts
|
|
4619
|
-
suggestion: "
|
|
4649
|
+
description: "Contracts intended as EIP-7702 delegation targets must guard initializers \u2014 constructors don't run on delegated EOAs. An unguarded `initialize()` can be front-run, letting an attacker take ownership. The pattern also matches generic proxy initializers, which usually ARE guarded (just not by an `initializer` keyword on the same line), so confirm whether this contract is actually a 7702 target before treating as critical.",
|
|
4650
|
+
suggestion: "If this is a 7702 delegation target: add `initializer` modifier (OpenZeppelin) or a boolean guard. For regular proxies, confirm the initializer is gated by the proxy factory.",
|
|
4620
4651
|
pattern: /function\s+initialize\s*\([^)]*\)\s*(?:external|public)(?!\s+\w*[Ii]nitializer)/,
|
|
4621
|
-
severity: "
|
|
4652
|
+
severity: "high",
|
|
4622
4653
|
category: "solidity",
|
|
4623
|
-
confidence: "
|
|
4654
|
+
confidence: "low",
|
|
4624
4655
|
languages: SOL
|
|
4625
4656
|
},
|
|
4626
4657
|
{
|
|
@@ -4688,7 +4719,7 @@ var uniswapV4Rules = [
|
|
|
4688
4719
|
title: "Uniswap v4 hook: BalanceDelta returned without settle/take",
|
|
4689
4720
|
description: "Hooks that return a modified `BalanceDelta` must ensure the delta is fully consumed (settled or taken) within the same unlock cycle. An unconsumed delta causes `CurrencyNotSettled` revert; partial consumption can silently strand tokens.",
|
|
4690
4721
|
suggestion: "Ensure `poolManager.settle()` or `poolManager.take()` is called for both currency0 and currency1 before returning from the unlock callback.",
|
|
4691
|
-
|
|
4722
|
+
multilinePattern: /returns\s*\([^)]*BalanceDelta[^)]*\)(?![\s\S]{0,800}?(?:settle|\.take)\s*\()/,
|
|
4692
4723
|
severity: "high",
|
|
4693
4724
|
category: "solidity",
|
|
4694
4725
|
confidence: "low",
|
|
@@ -4699,7 +4730,7 @@ var uniswapV4Rules = [
|
|
|
4699
4730
|
title: "Uniswap v4 hook: overly broad hook permissions",
|
|
4700
4731
|
description: "Hook permissions are set at deployment via the address bit-flags and cannot be changed. Requesting permissions you don't need (e.g. `BEFORE_SWAP_RETURNS_DELTA` when you never return a delta) increases attack surface and gas costs for every pool interaction.",
|
|
4701
4732
|
suggestion: "Return only the minimum required `Hooks.Permissions` from `getHookPermissions()`. Audit each permission flag against your actual callback implementations.",
|
|
4702
|
-
|
|
4733
|
+
multilinePattern: /getHookPermissions\s*\(\s*\)\s*(?:public|external|override)[^{]*\{[\s\S]{0,300}?true/,
|
|
4703
4734
|
severity: "medium",
|
|
4704
4735
|
category: "solidity",
|
|
4705
4736
|
confidence: "low",
|
|
@@ -4717,9 +4748,337 @@ var uniswapV4Rules = [
|
|
|
4717
4748
|
languages: SOL
|
|
4718
4749
|
}
|
|
4719
4750
|
];
|
|
4751
|
+
var hackReplayRules = [
|
|
4752
|
+
/* ── Curve Finance ($73M, July 2023) ────────────────────────── */
|
|
4753
|
+
/* Root cause: Vyper compiler versions 0.2.15, 0.2.16, 0.3.0 */
|
|
4754
|
+
/* generated broken reentrancy locks — the malloc-style storage */
|
|
4755
|
+
/* slot for the lock was not reused correctly across functions. */
|
|
4756
|
+
{
|
|
4757
|
+
id: "cp-hack-curve-vyper-version",
|
|
4758
|
+
title: "Curve hack pattern \u2014 Vyper compiler version with broken reentrancy lock",
|
|
4759
|
+
description: "This file declares Vyper 0.2.15, 0.2.16, or 0.3.0 \u2014 the exact compiler versions that mis-generated reentrancy locks and enabled the July 2023 Curve Finance exploit ($73M across multiple pools). The bug is in the compiler, not your source code: any @nonreentrant guard in these versions is silently bypassable.",
|
|
4760
|
+
suggestion: "Upgrade to Vyper >= 0.3.1 (lock fix landed in 0.3.1) and redeploy any affected pools. If you cannot redeploy, pause all reentrancy-sensitive functions and migrate liquidity.",
|
|
4761
|
+
pattern: /#\s*@version\s+0\.(?:2\.1[56]|3\.0)(?:\s|$)/,
|
|
4762
|
+
severity: "critical",
|
|
4763
|
+
category: "hack-replay",
|
|
4764
|
+
confidence: "high",
|
|
4765
|
+
languages: [".vy"]
|
|
4766
|
+
},
|
|
4767
|
+
/* ── Euler Finance ($197M, March 2023) ──────────────────────── */
|
|
4768
|
+
/* Root cause: donateToReserves() allowed an attacker to push */
|
|
4769
|
+
/* their own account into an artificially unhealthy state, */
|
|
4770
|
+
/* then self-liquidate at a discount via the liquidation flow. */
|
|
4771
|
+
{
|
|
4772
|
+
id: "cp-hack-euler-donate-self-liquidation",
|
|
4773
|
+
title: "Euler hack pattern \u2014 donate function reachable from liquidation path",
|
|
4774
|
+
description: "A donate/donateTo* function in a lending or vault context can be weaponized to artificially worsen the caller's health factor, then trigger self-liquidation at a profit. This is the exact vector that drained $197M from Euler Finance in March 2023.",
|
|
4775
|
+
suggestion: "Either remove the donate function, or (a) require donations to come from accounts with no active borrows, (b) recompute health AFTER the donation as if the donated assets remained the donor's, and (c) prevent same-block liquidation of the donor.",
|
|
4776
|
+
pattern: /function\s+donate[A-Za-z]*\s*\(/,
|
|
4777
|
+
severity: "high",
|
|
4778
|
+
category: "hack-replay",
|
|
4779
|
+
confidence: "medium",
|
|
4780
|
+
languages: SOL
|
|
4781
|
+
},
|
|
4782
|
+
/* ── Radiant Capital ($53M, October 2024) ───────────────────── */
|
|
4783
|
+
/* Root cause: 3-of-11 multisig members signed a malicious */
|
|
4784
|
+
/* transferOwnership tx (their UI was tampered by malware). */
|
|
4785
|
+
/* A two-step transferOwnership + timelock would have given the */
|
|
4786
|
+
/* team a chance to notice and abort. */
|
|
4787
|
+
{
|
|
4788
|
+
id: "cp-hack-radiant-immediate-ownership-transfer",
|
|
4789
|
+
title: "Radiant hack pattern \u2014 single-step ownership transfer (no Ownable2Step)",
|
|
4790
|
+
description: "transferOwnership is implemented as a single-call function \u2014 the new owner takes effect immediately. This is the same pattern the Radiant Capital ($53M, Oct 2024) attackers exploited after compromising a multisig signer's UI: ownership flipped before anyone could react. A two-step or timelocked pattern would have created a defensive window.",
|
|
4791
|
+
suggestion: "Inherit from OpenZeppelin Ownable2Step (requires acceptOwnership from the new owner) AND gate it behind a timelock for production deployments. Never let a single signature transfer ownership atomically.",
|
|
4792
|
+
multilinePattern: /function\s+transferOwnership\s*\(\s*address\s+\w+\s*\)\s*(?:public|external)(?![\s\S]{0,400}?(?:pendingOwner|_pendingOwner|acceptOwnership|Ownable2Step|timelock|Timelock|TimelockController))/,
|
|
4793
|
+
severity: "high",
|
|
4794
|
+
category: "hack-replay",
|
|
4795
|
+
confidence: "medium",
|
|
4796
|
+
languages: SOL
|
|
4797
|
+
},
|
|
4798
|
+
/* ── zkSync Airdrop ($5M, April 2025) ───────────────────────── */
|
|
4799
|
+
/* Root cause: sweepUnclaimed() admin-only function with no */
|
|
4800
|
+
/* timelock or multisig gate — when the admin key was */
|
|
4801
|
+
/* compromised, attacker minted 111M ZK in a single tx. */
|
|
4802
|
+
/* Pattern targets the unambiguous red-flag names. We do NOT */
|
|
4803
|
+
/* flag plain `mint` since regular token contracts use it with */
|
|
4804
|
+
/* proper guards — single-line regex can't verify a timelock */
|
|
4805
|
+
/* check on the next line, so we stay conservative. */
|
|
4806
|
+
{
|
|
4807
|
+
id: "cp-hack-zksync-admin-sweep-no-timelock",
|
|
4808
|
+
title: "zkSync hack pattern \u2014 admin-only sweep/drain/rescue function",
|
|
4809
|
+
description: "An admin-gated function with a name like sweep*, drain*, rescue*, emergencyWithdraw*, forceMint*, adminMint* is the exact category of function the zkSync airdrop attacker exploited (April 2025, $5M loss). Even with a timelock check inside, these functions are high-value targets \u2014 a compromised admin key bypasses everything. The pattern is reviewed manually because a same-line regex cannot verify whether a timelock guard is enforced.",
|
|
4810
|
+
suggestion: "Audit this function carefully: (a) is it gated by a TimelockController contract address check, not just an onlyOwner modifier? (b) is the action bounded (max amount per call, per epoch)? (c) is there an N-of-M multisig requirement at the modifier level? If any answer is no, add it before mainnet.",
|
|
4811
|
+
pattern: /function\s+(?:sweep[A-Za-z]*|drain[A-Za-z]*|rescue[A-Za-z]*|emergencyWithdraw[A-Za-z]*|forceMint[A-Za-z]*|adminMint[A-Za-z]*)\s*\([^)]*\)\s*(?:external|public)/,
|
|
4812
|
+
severity: "high",
|
|
4813
|
+
category: "hack-replay",
|
|
4814
|
+
confidence: "medium",
|
|
4815
|
+
languages: SOL
|
|
4816
|
+
},
|
|
4817
|
+
/* ── Bybit ($1.46B, February 2025) ──────────────────────────── */
|
|
4818
|
+
/* Root cause: Safe{Wallet}'s frontend bundle on S3 was modified */
|
|
4819
|
+
/* to display a legit tx while signing a malicious one to */
|
|
4820
|
+
/* hardware wallets. This is a CI/CD + frontend integrity issue. */
|
|
4821
|
+
{
|
|
4822
|
+
id: "cp-hack-bybit-unpinned-action-checkout",
|
|
4823
|
+
title: "Bybit hack pattern \u2014 unpinned GitHub Action handling deploy/release",
|
|
4824
|
+
description: "A workflow that deploys frontend bundles or release artifacts uses a third-party action pinned to a mutable ref (@main, @master, or a version tag instead of a commit SHA). This is the supply-chain vector behind the $1.46B Bybit hack (Feb 2025): the deploy pipeline trusts a moving target. An attacker that compromises the action's repo can swap your build output.",
|
|
4825
|
+
suggestion: "Pin every third-party action to a full 40-char commit SHA (e.g. `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`). Use Dependabot or Renovate to keep SHAs up to date with review.",
|
|
4826
|
+
pattern: /uses:\s+(?!actions\/)[^\s]+@(?:main|master|v\d+(?:\.\d+)*)\s*$/m,
|
|
4827
|
+
severity: "high",
|
|
4828
|
+
category: "hack-replay",
|
|
4829
|
+
confidence: "medium",
|
|
4830
|
+
languages: [],
|
|
4831
|
+
pathPattern: /\.github\/workflows\/.*\.ya?ml$/
|
|
4832
|
+
},
|
|
4833
|
+
/* ── Beanstalk ($182M, April 2022) ──────────────────────────── */
|
|
4834
|
+
/* Root cause: governance proposal could call arbitrary code in */
|
|
4835
|
+
/* the same transaction it was submitted/voted-on, with voting */
|
|
4836
|
+
/* power based on token balance at proposal time. Attacker flash- */
|
|
4837
|
+
/* loaned BEAN, voted YES on a self-draining proposal, executed */
|
|
4838
|
+
/* immediately. No timelock between vote and execution. */
|
|
4839
|
+
{
|
|
4840
|
+
id: "cp-hack-beanstalk-instant-governance",
|
|
4841
|
+
title: "Beanstalk hack pattern \u2014 governance execute() with no timelock between vote and call",
|
|
4842
|
+
description: "A governance contract exposes an execute() / executeProposal() / propose() function callable in the same block as voting. This is the exact $182M Beanstalk vector (April 2022): an attacker flash-loaned the governance token, voted yes on a self-draining proposal, and called execute() in the same transaction. A timelock between successful vote and execution would have made the flash-loan economically pointless.",
|
|
4843
|
+
suggestion: "Add a queue/execute split: successful proposals enter a TimelockController with a minimum delay (24-48h is standard). Require execute() to verify block.timestamp >= queuedAt + delay. Never let voting power, proposal acceptance, and code execution happen atomically.",
|
|
4844
|
+
multilinePattern: /function\s+(?:execute(?:Proposal)?|propose(?:AndExecute)?)\s*\([^)]*\)\s*(?:external|public)(?:\s+payable)?(?:\s+returns)?[^{]*\{(?![\s\S]{0,600}?(?:timelock|Timelock|TimelockController|queued|block\.timestamp\s*[><]=?\s*\w+\s*\+))/,
|
|
4845
|
+
severity: "critical",
|
|
4846
|
+
category: "hack-replay",
|
|
4847
|
+
confidence: "low",
|
|
4848
|
+
languages: SOL
|
|
4849
|
+
},
|
|
4850
|
+
/* ── Multichain ($126M, July 2023) ──────────────────────────── */
|
|
4851
|
+
/* Root cause: bridge admin private keys controlled by a single */
|
|
4852
|
+
/* person (CEO). When that person was detained, funds were */
|
|
4853
|
+
/* withdrawn from MPC-controlled addresses. Single key controls */
|
|
4854
|
+
/* bridge withdraw / unlock = single point of failure. */
|
|
4855
|
+
{
|
|
4856
|
+
id: "cp-hack-multichain-single-admin-bridge",
|
|
4857
|
+
title: "Multichain hack pattern \u2014 bridge withdraw/unlock gated by single owner role",
|
|
4858
|
+
description: "A bridge function (withdraw, unlock, releaseTo, claim, mintBridged) is gated only by onlyOwner or a single-address admin check. This was the structural failure behind the $126M Multichain collapse (July 2023): the CEO's MPC key controlled bridge outflows, and when he was detained, attackers (or insiders) drained the bridge. Bridge withdraw functions are the highest-value attack surface in crypto and must be multi-party.",
|
|
4859
|
+
suggestion: "Require a multi-sig (Safe / Gnosis), a multi-party MPC (TSS), or an N-of-M validator set before any bridge outflow. Never let a single private key sign a withdraw of bridged assets. Add per-asset and per-block outflow caps as a defense-in-depth limit.",
|
|
4860
|
+
multilinePattern: /function\s+(?:withdraw|unlock|releaseTo|releaseFor|claim|mintBridged|bridgeOut|relayOut)\s*\([^)]*\)\s*(?:external|public)\s+(?:onlyOwner|onlyAdmin|onlyMPC)(?![\s\S]{0,200}?(?:multisig|Multisig|threshold|N_OF_M|validators|attestors))/,
|
|
4861
|
+
severity: "critical",
|
|
4862
|
+
category: "hack-replay",
|
|
4863
|
+
confidence: "medium",
|
|
4864
|
+
languages: SOL
|
|
4865
|
+
},
|
|
4866
|
+
/* ── Ronin Bridge ($625M, March 2022) ───────────────────────── */
|
|
4867
|
+
/* Root cause: 5-of-9 validator set with 4 validators run by */
|
|
4868
|
+
/* Sky Mavis + 1 by Axie DAO. Attacker compromised the 4 Sky */
|
|
4869
|
+
/* Mavis keys and the Axie DAO had previously granted Sky Mavis */
|
|
4870
|
+
/* signing rights "temporarily" but never revoked → 5/9 reached. */
|
|
4871
|
+
/* Detection: low signature threshold relative to validator set. */
|
|
4872
|
+
{
|
|
4873
|
+
id: "cp-hack-ronin-low-validator-threshold",
|
|
4874
|
+
title: "Ronin hack pattern \u2014 bridge validator threshold \u2264 5/9 or hardcoded low quorum",
|
|
4875
|
+
description: "A bridge contract uses a hardcoded signature threshold of 4, 5, or 6 \u2014 the same range that allowed the $625M Ronin bridge hack (March 2022). Sky Mavis ran 4 of 9 validators; combined with one improperly-revoked delegated key, the attacker hit the 5/9 threshold from a single team's compromise. A higher threshold (7-of-9, 8-of-11) makes single-team compromise insufficient.",
|
|
4876
|
+
suggestion: "Audit your validator threshold against the social/operational concentration of validator operators. Aim for \u2265 70% threshold (e.g. 7-of-9, 11-of-15). Periodically rotate delegated signing rights and verify revocations actually happen. Set up monitoring for validator-set changes.",
|
|
4877
|
+
pattern: /(?:signatureThreshold|requiredSignatures|threshold|quorum|VALIDATOR_THRESHOLD)\s*[=:]\s*(?:uint(?:8|16|32|256)\s*\(\s*)?[4-6]\b/i,
|
|
4878
|
+
severity: "high",
|
|
4879
|
+
category: "hack-replay",
|
|
4880
|
+
confidence: "low",
|
|
4881
|
+
languages: SOL
|
|
4882
|
+
},
|
|
4883
|
+
/* ── Cream Finance ($130M, October 2021) ────────────────────── */
|
|
4884
|
+
/* Root cause: oracle manipulated via flash-loaned yUSD vault */
|
|
4885
|
+
/* shares. Collateral price computed from spot share supply, */
|
|
4886
|
+
/* inflatable via flash loan → borrow against inflated value → */
|
|
4887
|
+
/* drain. Detection: spot-balance arithmetic in pricing without */
|
|
4888
|
+
/* TWAP / Chainlink fallback. */
|
|
4889
|
+
{
|
|
4890
|
+
id: "cp-hack-cream-spot-share-pricing",
|
|
4891
|
+
title: "Cream hack pattern \u2014 collateral price computed from spot vault share supply",
|
|
4892
|
+
description: "A function computes a token/share price by dividing total assets by spot totalSupply (or balanceOf this). The $130M Cream hack (Oct 2021) exploited exactly this on yUSDVault: flash-loan inflated the share supply during a single tx, the price oracle read the manipulated value, and the attacker borrowed massively against fictitious collateral. Spot supply/balance ratios are flash-loan-attackable.",
|
|
4893
|
+
suggestion: "Use Chainlink price feeds with staleness checks, a TWAP from a deep Uniswap V3 pool, or external collateral-pricing oracles. Never compute price from same-block totalSupply or balanceOf.",
|
|
4894
|
+
pattern: /(?:totalSupply|balanceOf)\s*\([^)]*\)\s*[*/]\s*\w+|\w+\s*[*/]\s*(?:totalSupply|balanceOf)\s*\(/,
|
|
4895
|
+
severity: "high",
|
|
4896
|
+
category: "hack-replay",
|
|
4897
|
+
confidence: "low",
|
|
4898
|
+
languages: SOL
|
|
4899
|
+
},
|
|
4900
|
+
/* ── Wormhole ($325M, February 2022) ────────────────────────── */
|
|
4901
|
+
/* Root cause: signature verification path used a stub guardian */
|
|
4902
|
+
/* set lookup that didn't validate the set hash. Attacker forged */
|
|
4903
|
+
/* a guardian signature against an empty set, which the contract */
|
|
4904
|
+
/* accepted because the set comparison was wrong. Detection: */
|
|
4905
|
+
/* ecrecover or signature-set membership without strict hash */
|
|
4906
|
+
/* equality of the validator set. */
|
|
4907
|
+
{
|
|
4908
|
+
id: "cp-hack-wormhole-unchecked-signature-set",
|
|
4909
|
+
title: "Wormhole hack pattern \u2014 guardian/validator signature accepted without strict set verification",
|
|
4910
|
+
description: "A function verifies a signature against a validator/guardian set but does not strictly check that the set hash matches the expected current set (or compares against a default/empty value). The $325M Wormhole hack (Feb 2022) exploited a stub `verify_signatures` path that accepted forged signatures because the validator set comparison was missing/insecure. ecrecover-based signature schemes need every validator set rotation tracked by an explicit hash check.",
|
|
4911
|
+
suggestion: "Verify the signed message includes a strict hash of the current validator set. Reject signatures where the recovered signer is not in the active set. Never use `default!()` or zero-initialized guardian arrays in signature paths.",
|
|
4912
|
+
multilinePattern: /(?:ecrecover|recover)\s*\([\s\S]{0,400}?\)(?![\s\S]{0,300}?(?:guardianSet|validatorSet|currentSetHash|setHash|require\s*\([^)]*==\s*expected))/,
|
|
4913
|
+
severity: "critical",
|
|
4914
|
+
category: "hack-replay",
|
|
4915
|
+
confidence: "low",
|
|
4916
|
+
languages: SOL
|
|
4917
|
+
},
|
|
4918
|
+
/* ── Nomad Bridge ($190M, August 2022) ──────────────────────── */
|
|
4919
|
+
/* Root cause: an upgrade initialized the trusted-root mapping */
|
|
4920
|
+
/* with 0x00. Any message with a 0-bytes proof became 'proven'. */
|
|
4921
|
+
/* Anyone could replay arbitrary messages against the bridge. */
|
|
4922
|
+
/* Detection: trusted-root / merkle-root mapping reads or writes */
|
|
4923
|
+
/* that don't reject zero values. */
|
|
4924
|
+
{
|
|
4925
|
+
id: "cp-hack-nomad-zero-root-acceptance",
|
|
4926
|
+
title: "Nomad hack pattern \u2014 trusted/merkle root lookup accepts zero hash as valid",
|
|
4927
|
+
description: "A function reads a value from a merkle-root or trusted-root mapping and treats any non-revert response as valid, including the zero default. The $190M Nomad bridge hack (Aug 2022) was caused by an upgrade that initialized the root mapping to zero \u2014 every message proof of zero became 'valid', and the entire bridge was drained by 300+ copy-paste attackers within hours.",
|
|
4928
|
+
suggestion: "Always `require(root != bytes32(0))` before treating a stored root/hash as a valid proof anchor. Apply the same check to any zero-value default in upgrade-initialized storage.",
|
|
4929
|
+
pattern: /(?:roots|messages|acceptableRoot|trustedRoot|merkleRoot)\s*\[[^\]]+\]\s*(?:==\s*(?:true|0x01)|!=\s*0x00\b)(?!\s*&&\s*\w+\s*!=\s*(?:bytes32\(0\)|0x0+))/,
|
|
4930
|
+
severity: "critical",
|
|
4931
|
+
category: "hack-replay",
|
|
4932
|
+
confidence: "low",
|
|
4933
|
+
languages: SOL
|
|
4934
|
+
},
|
|
4935
|
+
/* ── Mango Markets ($114M, October 2022) ────────────────────── */
|
|
4936
|
+
/* Root cause: oracle was a thinly-traded spot AMM price. */
|
|
4937
|
+
/* Attacker pumped MNGO token via CEX wash trades, then borrowed */
|
|
4938
|
+
/* $114M against inflated collateral. Detection: pricing pulled */
|
|
4939
|
+
/* from a single low-liquidity source with no sanity bounds. */
|
|
4940
|
+
{
|
|
4941
|
+
id: "cp-hack-mango-single-source-oracle",
|
|
4942
|
+
title: "Mango hack pattern \u2014 oracle uses single price source without bounds",
|
|
4943
|
+
description: "A pricing function reads from a single oracle source (one Uniswap pool, one CEX feed, one Chainlink aggregator) without sanity bounds or a secondary source. The $114M Mango Markets hack (Oct 2022) was a textbook oracle-manipulation attack: the attacker pumped MNGO price ~10\xD7 via CEX trades, the oracle dutifully reported it, and Mango let them borrow $114M against the now-inflated collateral.",
|
|
4944
|
+
suggestion: "Use at least two independent oracle sources for any collateral pricing. Apply a circuit breaker (max % change per block, deviation from TWAP). For long-tail tokens, cap borrowable collateral or require multi-source confirmation.",
|
|
4945
|
+
// Tightened: require the call to appear in a function whose name implies
|
|
4946
|
+
// lending / collateral / liquidation context (the actual hack surface).
|
|
4947
|
+
// Plain DEX pool internals like Uniswap getReserves are not at risk.
|
|
4948
|
+
multilinePattern: /function\s+(?:borrow\w*|liquidat\w*|isHealthy|collateralValue|getAccountHealth|maxBorrow|withdrawCollateral|getBorrowable)[\s\S]{0,600}?\b(?:getPrice|latestAnswer|peek|read)\s*\((?![\s\S]{0,400}?(?:twap|TWAP|deviation|maxDeviation|stalenessThreshold|secondaryPrice|sanity))/,
|
|
4949
|
+
severity: "medium",
|
|
4950
|
+
category: "hack-replay",
|
|
4951
|
+
confidence: "low",
|
|
4952
|
+
languages: SOL
|
|
4953
|
+
}
|
|
4954
|
+
];
|
|
4955
|
+
var rugSurfaceRules = [
|
|
4956
|
+
{
|
|
4957
|
+
id: "cp-rug-owner-can-mint",
|
|
4958
|
+
title: "Owner can mint tokens",
|
|
4959
|
+
description: "A mint function is gated by onlyOwner (or similar single-address modifier). Owner can dilute holders at will. Verify whether this is intentional (e.g. emissions schedule) or unrestricted.",
|
|
4960
|
+
suggestion: "Hard-cap max supply, route mints through a timelock + multisig, or renounce ownership after initial mint.",
|
|
4961
|
+
pattern: /function\s+(?:_?mint|mintTo|airdrop)\s*\([^)]*\)\s*(?:external|public)\s+(?:onlyOwner|onlyAdmin|onlyMinter)/,
|
|
4962
|
+
severity: "high",
|
|
4963
|
+
category: "rug-surface",
|
|
4964
|
+
confidence: "medium",
|
|
4965
|
+
languages: SOL
|
|
4966
|
+
},
|
|
4967
|
+
{
|
|
4968
|
+
id: "cp-rug-owner-can-pause",
|
|
4969
|
+
title: "Owner can pause transfers",
|
|
4970
|
+
description: "Owner can call _pause() or set a paused flag \u2014 halting all transfers. Useful for emergencies, dangerous if the owner is an EOA without timelock.",
|
|
4971
|
+
suggestion: "Move pause behind multisig + timelock. Document expected pause scenarios. Consider auto-unpause after N blocks.",
|
|
4972
|
+
pattern: /function\s+(?:pause|setPaused|_pause|emergencyPause)\s*\([^)]*\)\s*(?:external|public)\s+(?:onlyOwner|onlyAdmin)/,
|
|
4973
|
+
severity: "medium",
|
|
4974
|
+
category: "rug-surface",
|
|
4975
|
+
confidence: "medium",
|
|
4976
|
+
languages: SOL
|
|
4977
|
+
},
|
|
4978
|
+
{
|
|
4979
|
+
id: "cp-rug-owner-can-blacklist",
|
|
4980
|
+
title: "Owner can blacklist addresses",
|
|
4981
|
+
description: "Owner can prevent specific addresses from transferring. Honeypot pattern when combined with no transfer-tax limit or a hidden blacklist mapping.",
|
|
4982
|
+
suggestion: "Either remove the blacklist entirely (rely on protocol-level sanctions), or gate it behind a multisig + timelock with public allowlist process.",
|
|
4983
|
+
pattern: /(?:blacklist|denylist|isBlocked|blocked)\s*\[\s*\w+\s*\]\s*=\s*(?:true|1)|function\s+(?:blacklist|setBlacklist|addToBlacklist|block(?:Address|User|Account))\s*\(/i,
|
|
4984
|
+
severity: "high",
|
|
4985
|
+
category: "rug-surface",
|
|
4986
|
+
confidence: "medium",
|
|
4987
|
+
languages: SOL
|
|
4988
|
+
},
|
|
4989
|
+
{
|
|
4990
|
+
id: "cp-rug-owner-can-change-fees",
|
|
4991
|
+
title: "Owner can change transfer fees",
|
|
4992
|
+
description: "A setFee / updateFee / setTax function gated by onlyOwner. Owner can spike fees to 99% effectively stopping trading.",
|
|
4993
|
+
suggestion: "Cap maximum fee in the contract (e.g. require new fee <= 10%). Combine with a timelock so changes are observable in advance.",
|
|
4994
|
+
pattern: /function\s+(?:setFee|setTax|setBuyFee|setSellFee|setTransferFee|updateFee|updateTax|setFees)\s*\([^)]*\)\s*(?:external|public)\s+(?:onlyOwner|onlyAdmin)/i,
|
|
4995
|
+
severity: "high",
|
|
4996
|
+
category: "rug-surface",
|
|
4997
|
+
confidence: "medium",
|
|
4998
|
+
languages: SOL
|
|
4999
|
+
},
|
|
5000
|
+
{
|
|
5001
|
+
id: "cp-rug-owner-can-change-router",
|
|
5002
|
+
title: "Owner can swap router or pair",
|
|
5003
|
+
description: "A setRouter / setPair / setSwapRouter function gated by onlyOwner. Owner can redirect liquidity to an attacker-controlled router and drain trades.",
|
|
5004
|
+
suggestion: "Make router / pair immutable. If upgrades are needed, gate behind multisig + 7-day timelock with clear public announcement.",
|
|
5005
|
+
pattern: /function\s+(?:setRouter|setPair|setSwapRouter|updateRouter|setUniswapRouter|setSwapPair)\s*\([^)]*\)\s*(?:external|public)\s+(?:onlyOwner|onlyAdmin)/i,
|
|
5006
|
+
severity: "high",
|
|
5007
|
+
category: "rug-surface",
|
|
5008
|
+
confidence: "medium",
|
|
5009
|
+
languages: SOL
|
|
5010
|
+
},
|
|
5011
|
+
{
|
|
5012
|
+
id: "cp-rug-owner-can-sweep",
|
|
5013
|
+
title: "Owner can sweep ETH or ERC20 from contract",
|
|
5014
|
+
description: 'Owner can call a sweep / rescue / withdraw / recover function that drains arbitrary tokens or ETH from the contract. Often labelled "rescue stuck funds" \u2014 but the same path drains user-owned balances.',
|
|
5015
|
+
suggestion: "Restrict to specific tokens that should never be in the contract. Exclude the protocol's own LP token / treasury. Behind multisig + timelock.",
|
|
5016
|
+
pattern: /function\s+(?:rescueTokens|rescueERC20|sweepTokens|sweep|sweepERC20|recoverTokens|recoverERC20|withdrawStuck|withdrawTokens|emergencyWithdraw|emergencyRescue)\s*\([^)]*\)\s*(?:external|public)\s+(?:onlyOwner|onlyAdmin)/i,
|
|
5017
|
+
severity: "critical",
|
|
5018
|
+
category: "rug-surface",
|
|
5019
|
+
confidence: "medium",
|
|
5020
|
+
languages: SOL
|
|
5021
|
+
},
|
|
5022
|
+
{
|
|
5023
|
+
id: "cp-rug-owner-can-upgrade",
|
|
5024
|
+
title: "Proxy upgrade gated by single owner",
|
|
5025
|
+
description: "An _upgradeTo / upgradeTo function is callable by onlyOwner (UUPS pattern). The implementation contract can be swapped to attacker code unilaterally.",
|
|
5026
|
+
suggestion: "Route upgrades through a TimelockController owned by a multisig. Publish the new implementation address before the timelock expires.",
|
|
5027
|
+
pattern: /function\s+(?:_upgradeTo|upgradeTo|upgradeToAndCall|_authorizeUpgrade)\s*\([^)]*\)\s*(?:external|public|internal|virtual\s+override)?(?:[^{]*?)(?:onlyOwner|onlyAdmin)/,
|
|
5028
|
+
severity: "high",
|
|
5029
|
+
category: "rug-surface",
|
|
5030
|
+
confidence: "medium",
|
|
5031
|
+
languages: SOL
|
|
5032
|
+
},
|
|
5033
|
+
{
|
|
5034
|
+
id: "cp-rug-role-self-grant",
|
|
5035
|
+
title: "Role admin can grant itself any role",
|
|
5036
|
+
description: "DEFAULT_ADMIN_ROLE or a self-managed role can call grantRole() on itself or on other privileged roles. Single admin compromise = full takeover.",
|
|
5037
|
+
suggestion: "Use AccessControlEnumerable + revoke admin's ability to manage its own role. Move sensitive roles to a separate multisig.",
|
|
5038
|
+
pattern: /_setupRole\s*\(\s*DEFAULT_ADMIN_ROLE|_grantRole\s*\(\s*DEFAULT_ADMIN_ROLE|grantRole\s*\([^,]+,\s*msg\.sender\s*\)/,
|
|
5039
|
+
severity: "high",
|
|
5040
|
+
category: "rug-surface",
|
|
5041
|
+
confidence: "medium",
|
|
5042
|
+
languages: SOL
|
|
5043
|
+
},
|
|
5044
|
+
{
|
|
5045
|
+
id: "cp-rug-no-renounce-evidence",
|
|
5046
|
+
title: "Renounceable ownership without evidence of renunciation",
|
|
5047
|
+
description: "Contract inherits Ownable / OwnableUpgradeable but the constructor / initializer doesn't transfer ownership to a known-safe address (zero, multisig, timelock). Owner power may still be active.",
|
|
5048
|
+
suggestion: "Either renounce ownership in the constructor (acceptably for fully-immutable contracts), or transfer to a multisig. Document this explicitly.",
|
|
5049
|
+
multilinePattern: /(?:contract|abstract\s+contract)\s+\w+[\s\S]{0,500}?\bis\s+[\w,\s]*?Ownable(?:Upgradeable)?\b(?![\s\S]{0,2000}?(?:renounceOwnership\s*\(\)|transferOwnership\s*\(\s*address\(0x|TimelockController|Safe\s*\())/,
|
|
5050
|
+
severity: "medium",
|
|
5051
|
+
category: "rug-surface",
|
|
5052
|
+
confidence: "low",
|
|
5053
|
+
languages: SOL
|
|
5054
|
+
},
|
|
5055
|
+
{
|
|
5056
|
+
id: "cp-rug-max-wallet-or-tx",
|
|
5057
|
+
title: "Trading restrictions (max wallet / max tx)",
|
|
5058
|
+
description: "Contract enforces maxWallet, maxTransactionAmount, or similar caps. Common in memecoin templates \u2014 verify the cap can't be set to 0 or 1 wei (effectively halting trading).",
|
|
5059
|
+
suggestion: "Ensure max-cap setters have a minimum floor (e.g. 0.1% of supply). Combine with timelock for changes after launch.",
|
|
5060
|
+
pattern: /(?:maxWallet|maxTransactionAmount|maxTx|maxWalletAmount|_maxWallet|_maxTx)\s*[:=]/i,
|
|
5061
|
+
severity: "low",
|
|
5062
|
+
category: "rug-surface",
|
|
5063
|
+
confidence: "medium",
|
|
5064
|
+
languages: SOL
|
|
5065
|
+
},
|
|
5066
|
+
{
|
|
5067
|
+
id: "cp-rug-hidden-modifier",
|
|
5068
|
+
title: "Privileged modifier without visible role check in body",
|
|
5069
|
+
description: "A modifier name doesn't suggest privilege (e.g. `lock`, `safe`, `nonReentrant`-shaped names) but its body restricts access. Auditors miss these; users assume the function is open.",
|
|
5070
|
+
suggestion: "Rename modifier to make privilege explicit (e.g. `onlyOperator`). Or move check inline so reviewers see it in the function body.",
|
|
5071
|
+
multilinePattern: /modifier\s+(?!onlyOwner|onlyAdmin|onlyRole|nonReentrant|whenNotPaused|whenPaused|initializer|reinitializer)(\w+)\s*\(\s*\)\s*\{[\s\S]{0,200}?require\s*\(\s*msg\.sender\s*==\s*\w+/,
|
|
5072
|
+
severity: "medium",
|
|
5073
|
+
category: "rug-surface",
|
|
5074
|
+
confidence: "low",
|
|
5075
|
+
languages: SOL
|
|
5076
|
+
}
|
|
5077
|
+
];
|
|
4720
5078
|
var ALL_RULES2 = [
|
|
4721
5079
|
...securityRules,
|
|
4722
5080
|
...solidityRules2,
|
|
5081
|
+
...rugSurfaceRules,
|
|
4723
5082
|
...javaRules,
|
|
4724
5083
|
...rubyRules,
|
|
4725
5084
|
...phpRules,
|
|
@@ -4730,7 +5089,8 @@ var ALL_RULES2 = [
|
|
|
4730
5089
|
...iacRules,
|
|
4731
5090
|
...eip7702Rules,
|
|
4732
5091
|
...tstoreRules,
|
|
4733
|
-
...uniswapV4Rules
|
|
5092
|
+
...uniswapV4Rules,
|
|
5093
|
+
...hackReplayRules
|
|
4734
5094
|
];
|
|
4735
5095
|
|
|
4736
5096
|
// src/static/pattern-scanner.ts
|
|
@@ -4782,9 +5142,13 @@ function isJsxFile(filePath) {
|
|
|
4782
5142
|
function isScriptDir(filePath) {
|
|
4783
5143
|
return /(?:^|\/)(?:scripts|build|tools|infra)\//i.test(filePath);
|
|
4784
5144
|
}
|
|
5145
|
+
function isVendorFile(filePath) {
|
|
5146
|
+
return /(?:^|\/)(?:node_modules|@openzeppelin|@uniswap|@aave|@chainlink|solmate|solady|lib|vendor|dependencies|forge-std|ds-test)(?:\/|@)/i.test(filePath);
|
|
5147
|
+
}
|
|
4785
5148
|
function isFalsePositive(ctx) {
|
|
4786
5149
|
if (isDocsFile(ctx.filePath)) return true;
|
|
4787
|
-
if (
|
|
5150
|
+
if (isVendorFile(ctx.filePath)) return true;
|
|
5151
|
+
if (ctx.rule.id !== "cp-qual-todo-fixme" && ctx.rule.category !== "hack-replay" && isCommentLine(ctx.line)) return true;
|
|
4788
5152
|
if (isImportLine(ctx.line) && /(?:http-no-tls|hardcoded-ip|hardcoded-secret)/.test(ctx.rule.id)) return true;
|
|
4789
5153
|
if (isConfigFile(ctx.filePath) && ctx.rule.category === "security") return true;
|
|
4790
5154
|
const trimmed = ctx.line.trim();
|
|
@@ -4802,6 +5166,11 @@ function isFalsePositive(ctx) {
|
|
|
4802
5166
|
if (/process\.env\b/.test(ctx.line)) return true;
|
|
4803
5167
|
}
|
|
4804
5168
|
}
|
|
5169
|
+
if (ctx.rule.id === "cp-sec-hardcoded-secret") {
|
|
5170
|
+
if (/=\s*["']?0x[0-9a-fA-F]{40}["']?/.test(ctx.line)) return true;
|
|
5171
|
+
if (/\b(?:address|token0?|token1|tokenAddress|contractAddress|pool|router|factory|vault|recipient|payTo|treasury|owner|admin|operator|signer|delegate|merkleRoot|root|domain|salt|chainId|deadline|attestation|uid|schema)\b\s*[:=]/i.test(ctx.line)) return true;
|
|
5172
|
+
if (/\b(?:hash|sha|keccak|merkle|root|uid|tx|proof|signature|sig|domain|salt|nonce|attestation|commitment)\w*\s*[:=]\s*["']?0x[0-9a-fA-F]{64}/i.test(ctx.line)) return true;
|
|
5173
|
+
}
|
|
4805
5174
|
return false;
|
|
4806
5175
|
}
|
|
4807
5176
|
function adjustSeverityForContext(severity, filePath) {
|
|
@@ -4939,7 +5308,7 @@ ${lines[i]}`,
|
|
|
4939
5308
|
}
|
|
4940
5309
|
}
|
|
4941
5310
|
}
|
|
4942
|
-
if (lines.length > 8) {
|
|
5311
|
+
if (lines.length > 8 && !SOL2.includes(ext)) {
|
|
4943
5312
|
const WINDOW = 4;
|
|
4944
5313
|
const blockMap = /* @__PURE__ */ new Map();
|
|
4945
5314
|
for (let i = 0; i <= lines.length - WINDOW; i++) {
|
|
@@ -5026,6 +5395,7 @@ function scanFile(relPath, content, rules, changedRanges) {
|
|
|
5026
5395
|
const applicableRules = rules.filter((r) => ruleAppliesToFile(r, relPath));
|
|
5027
5396
|
if (applicableRules.length === 0) return findings;
|
|
5028
5397
|
for (const rule of applicableRules) {
|
|
5398
|
+
if (!rule.pattern) continue;
|
|
5029
5399
|
for (let i = 0; i < lines.length; i++) {
|
|
5030
5400
|
const lineNumber = i + 1;
|
|
5031
5401
|
const line = lines[i];
|
|
@@ -5057,8 +5427,10 @@ function scanFile(relPath, content, rules, changedRanges) {
|
|
|
5057
5427
|
if (rule.id === "cp-clean-callback-hell" && isTestFile(relPath)) continue;
|
|
5058
5428
|
if (rule.id === "cp-sec-command-injection" && isScriptDir(relPath)) continue;
|
|
5059
5429
|
rule.multilinePattern.lastIndex = 0;
|
|
5430
|
+
const isGlobal = rule.multilinePattern.flags.includes("g");
|
|
5060
5431
|
let match;
|
|
5061
5432
|
while ((match = rule.multilinePattern.exec(content)) !== null) {
|
|
5433
|
+
const breakAfter = !isGlobal;
|
|
5062
5434
|
const textBefore = content.slice(0, match.index);
|
|
5063
5435
|
const startLine = textBefore.split("\n").length;
|
|
5064
5436
|
const matchLines = match[0].split("\n").length;
|
|
@@ -5084,6 +5456,7 @@ function scanFile(relPath, content, rules, changedRanges) {
|
|
|
5084
5456
|
if (match[0].length === 0) {
|
|
5085
5457
|
rule.multilinePattern.lastIndex++;
|
|
5086
5458
|
}
|
|
5459
|
+
if (breakAfter) break;
|
|
5087
5460
|
}
|
|
5088
5461
|
}
|
|
5089
5462
|
if (rules.some((r) => r.category === "code-cleaning")) {
|