@elytrasec/engine 0.4.4 → 0.4.6
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 +157 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2912,6 +2912,7 @@ var PY = [".py"];
|
|
|
2912
2912
|
var GO = [".go"];
|
|
2913
2913
|
var SOL = [".sol"];
|
|
2914
2914
|
var JAVA = [".java"];
|
|
2915
|
+
var RUST = [".rs"];
|
|
2915
2916
|
var RUBY = [".rb", ".erb"];
|
|
2916
2917
|
var PHP = [".php"];
|
|
2917
2918
|
var ALL = ["*"];
|
|
@@ -5164,6 +5165,151 @@ var rugSurfaceRules = [
|
|
|
5164
5165
|
languages: SOL
|
|
5165
5166
|
}
|
|
5166
5167
|
];
|
|
5168
|
+
var modernDeFiRules = [
|
|
5169
|
+
{
|
|
5170
|
+
id: "cp-sol-erc4626-inflation",
|
|
5171
|
+
title: "ERC-4626 vault: missing inflation-attack protection",
|
|
5172
|
+
description: "ERC-4626 deposit/mint computes shares as `assets * totalSupply / totalAssets`. On an empty vault, an attacker who is the first depositor can deposit 1 wei, then DIRECTLY transfer a large amount of underlying to the vault \u2014 inflating the share/asset ratio. Subsequent depositors get rounded to ZERO shares for any deposit smaller than the inflated rate. Cream, Hundred, and many forks lost funds this way.",
|
|
5173
|
+
suggestion: "Use OpenZeppelin's ERC4626 v5+ which adds 1e6 virtual shares/assets (decimal offset). OR: in deposit(), require msg.sender to mint to a dead address first (initial burn of N shares). OR: enforce a minimum total-supply invariant.",
|
|
5174
|
+
multilinePattern: /function\s+(?:deposit|mint)\s*\([^)]*\)\s*(?:external|public)[^{;]*\{[\s\S]{0,1500}?(?:totalSupply|_totalSupply)\s*\([^)]*\)\s*[*/]\s*\w(?![\s\S]{0,800}?(?:_decimalsOffset|virtualShares|VIRTUAL_SHARES|MINIMUM_LIQUIDITY|10\s*\*\*\s*\d|DEAD_SHARES|_burnDead|dead\s*\(|initialBurn))/,
|
|
5175
|
+
severity: "high",
|
|
5176
|
+
category: "solidity",
|
|
5177
|
+
confidence: "medium",
|
|
5178
|
+
languages: SOL
|
|
5179
|
+
},
|
|
5180
|
+
{
|
|
5181
|
+
id: "cp-sol-eip712-missing-chainid",
|
|
5182
|
+
title: "EIP-712 domain separator computed without chainId",
|
|
5183
|
+
description: "An EIP-712 typed-data signature scheme that omits `block.chainid` from the DOMAIN_SEPARATOR allows the same signature to be replayed on a fork or testnet. Several bridge and permit exploits have come from this exact gap.",
|
|
5184
|
+
suggestion: "Always include `block.chainid` in the EIP-712 domain hash, OR inherit from OpenZeppelin's EIP712 which handles it. Recompute the separator when chainid changes (fork detection).",
|
|
5185
|
+
// Catches the domain-separator computation. File-level FP filter in pattern-scanner.ts
|
|
5186
|
+
// suppresses this rule entirely if the file has any chainid reference anywhere.
|
|
5187
|
+
multilinePattern: /(?:DOMAIN_SEPARATOR|_domainSeparator|domainSeparator)\s*=\s*keccak256\s*\(\s*abi\.encode\s*\([\s\S]{20,500}?\)\s*\)/,
|
|
5188
|
+
severity: "high",
|
|
5189
|
+
category: "solidity",
|
|
5190
|
+
confidence: "medium",
|
|
5191
|
+
languages: SOL
|
|
5192
|
+
},
|
|
5193
|
+
{
|
|
5194
|
+
id: "cp-sol-swap-zero-deadline",
|
|
5195
|
+
title: "Swap call with zero or absent deadline (MEV vulnerable)",
|
|
5196
|
+
description: "Calling a Uniswap V2/V3/V4 router swap with deadline = 0, deadline = block.timestamp (no protection), or deadline = type(uint256).max means the transaction can sit in the mempool indefinitely and be sandwiched whenever the price moves against you. Users lose money to MEV bots.",
|
|
5197
|
+
suggestion: "Enforce a real deadline: `deadline = block.timestamp + 15 minutes` at minimum. Pass it from the user, not computed inside the function. Reject calls with deadline > block.timestamp + 1 hour.",
|
|
5198
|
+
// Catches both named-arg (deadline: 0) and positional last-arg = 0 or block.timestamp or type(uint).max.
|
|
5199
|
+
// Multiline + tolerant of nested parens (address(this), etc.). Catches positional last-arg
|
|
5200
|
+
// = 0, block.timestamp, or type(uint).max as the deadline.
|
|
5201
|
+
multilinePattern: /(?:swapExactTokensForTokens|swapTokensForExactTokens|exactInput|exactOutput|swap[A-Z]\w*)\s*\([\s\S]{0,400}?,\s*(?:0|block\.timestamp|type\s*\(\s*uint256\s*\)\s*\.\s*max)\s*\)\s*;/,
|
|
5202
|
+
severity: "medium",
|
|
5203
|
+
category: "solidity",
|
|
5204
|
+
confidence: "low",
|
|
5205
|
+
languages: SOL
|
|
5206
|
+
},
|
|
5207
|
+
{
|
|
5208
|
+
id: "cp-sol-bridge-missing-source-check",
|
|
5209
|
+
title: "Cross-chain receive function: missing source-chain / trusted-remote check",
|
|
5210
|
+
description: "LayerZero `lzReceive`, Axelar `_execute`, Hyperlane `handle`, Wormhole `receiveMessage` and similar cross-chain receivers MUST verify (a) the source chain ID and (b) the trusted-remote sender address. Without these checks ANY contract on any chain can forge a message and trigger the receive logic \u2014 leading to unauthorized mint/withdraw of bridged assets.",
|
|
5211
|
+
suggestion: "Add explicit checks: `require(_srcChainId == trustedSrcChain)` and `require(keccak256(_srcAddress) == keccak256(trustedRemote))`. For LayerZero use `lzApp.setTrustedRemote`. For Axelar verify `sourceChain` + `sourceAddress`.",
|
|
5212
|
+
// Bridge-specific function names; file-level filter in pattern-scanner.ts skips ERC-4337
|
|
5213
|
+
// and similar accounts. Negative lookahead for trusted-remote / source-check in body.
|
|
5214
|
+
multilinePattern: /function\s+(?:lzReceive|nonblockingLzReceive|_nonblockingLzReceive|_handleMessage|receiveMessage|handle)\s*\([^)]*\)\s*(?:external|internal|public)[^{;]*\{(?![\s\S]{0,1500}?(?:trustedRemote|_trustedRemoteLookup|sourceChain\s*==|_srcChainId\s*==|trustedSrcChain|trustedSender|_validateMessageSource|_authorizeSource|onlyEndpoint|onlyMailbox|onlyGateway|interchainSecurityModule))/,
|
|
5215
|
+
severity: "critical",
|
|
5216
|
+
category: "solidity",
|
|
5217
|
+
confidence: "medium",
|
|
5218
|
+
languages: SOL
|
|
5219
|
+
},
|
|
5220
|
+
{
|
|
5221
|
+
id: "cp-sol-permit2-no-deadline-check",
|
|
5222
|
+
title: "Permit signature accepted without deadline / expiry check",
|
|
5223
|
+
description: "EIP-2612 permit() (and similar typed-data signature endpoints) must check that `deadline >= block.timestamp`. Without this, a leaked signature is valid forever. Several memecoin-launchpad bugs of 2024 leaked permits and lost early-mover allocations.",
|
|
5224
|
+
suggestion: 'Add `require(deadline >= block.timestamp, "Permit expired")` at the top of permit-like functions. Reject if deadline is type(uint256).max (suggests caller forgot to set one).',
|
|
5225
|
+
// Also accept assembly-style timestamp/deadline check (Solady pattern: gt(timestamp(), deadline))
|
|
5226
|
+
multilinePattern: /function\s+(?:permit|permitWithSig|permitTransfer|permitTransferFrom)\s*\([^)]*deadline[^)]*\)[^{;]*\{(?![\s\S]{0,1500}?(?:deadline\s*[><]=?\s*block\.timestamp|block\.timestamp\s*[<>]=?\s*deadline|require\s*\([^)]*deadline|gt\s*\(\s*timestamp\s*\(\s*\)\s*,\s*deadline|PermitExpired|ExpiredSignature|EXPIRED))/,
|
|
5227
|
+
severity: "high",
|
|
5228
|
+
category: "solidity",
|
|
5229
|
+
confidence: "medium",
|
|
5230
|
+
languages: SOL
|
|
5231
|
+
}
|
|
5232
|
+
];
|
|
5233
|
+
var solanaRules = [
|
|
5234
|
+
{
|
|
5235
|
+
id: "cp-sl-missing-signer-check",
|
|
5236
|
+
title: "Solana: account used as authority without signer check",
|
|
5237
|
+
description: "An account is passed to an instruction and used as the authority for an action (transfer, mint, close) without verifying it has signed the transaction. The function deserializes the account but never checks `is_signer` or uses the Anchor `Signer<'info>` type.",
|
|
5238
|
+
suggestion: "Anchor: declare the authority as `pub authority: Signer<'info>` so the runtime enforces is_signer. Raw: `require!(ctx.accounts.authority.is_signer, ErrorCode::Unauthorized);`",
|
|
5239
|
+
multilinePattern: /pub\s+\w+\s*:\s*AccountInfo\s*<'info>[\s\S]{0,400}?(?:transfer|mint_to|close|set_authority|burn)\s*\((?![\s\S]{0,800}?(?:is_signer|Signer\s*<|require!\s*\([^)]*is_signer|require_keys_eq!\s*\([^)]*authority))/,
|
|
5240
|
+
severity: "critical",
|
|
5241
|
+
category: "solidity",
|
|
5242
|
+
confidence: "medium",
|
|
5243
|
+
languages: RUST
|
|
5244
|
+
},
|
|
5245
|
+
{
|
|
5246
|
+
id: "cp-sl-missing-owner-check",
|
|
5247
|
+
title: "Solana: account ownership not verified before deserialization",
|
|
5248
|
+
description: "Raw `AccountInfo` is deserialized with `try_from_slice` or accessed via `data.borrow()` without first checking `account.owner == &expected_program_id`. An attacker can pass any account holding crafted bytes, leading to type confusion.",
|
|
5249
|
+
suggestion: "Anchor: use typed account wrappers (`Account<'info, MyState>`) which check the discriminator automatically. Raw: `require_keys_eq!(*account.owner, program_id, ErrorCode::WrongOwner);` before any deserialization.",
|
|
5250
|
+
multilinePattern: /(?:\.|::)try_from_slice\s*\(\s*(?:&|&mut\s+)\s*\w+\.(?:data|try_borrow_data)(?![\s\S]{0,800}?(?:require_keys_eq!\s*\([^)]*\.owner|require!\s*\([^)]*\.owner\s*==|assert_eq!\s*\([^)]*\.owner|Account\s*<|AccountLoader\s*<))/,
|
|
5251
|
+
severity: "critical",
|
|
5252
|
+
category: "solidity",
|
|
5253
|
+
confidence: "medium",
|
|
5254
|
+
languages: RUST
|
|
5255
|
+
},
|
|
5256
|
+
{
|
|
5257
|
+
id: "cp-sl-ata-confusion",
|
|
5258
|
+
title: "Solana: token account mint not validated (ATA confusion)",
|
|
5259
|
+
description: "An SPL `TokenAccount` is used in a transfer without validating its `mint` field matches the expected mint. An attacker can substitute a token account they control with the same authority but different (worthless) mint, draining the real account.",
|
|
5260
|
+
suggestion: "Anchor: use `#[account(mint = expected_mint, token::authority = user)]` constraints. Raw: `require_keys_eq!(token_account.mint, expected_mint);`",
|
|
5261
|
+
multilinePattern: /token::transfer\s*\([\s\S]{0,400}?\)(?![\s\S]{0,1500}?(?:mint\s*=\s*\w+|mint\s*==\s*\w+|require_keys_eq!\s*\([^)]*\.mint|associated_token::))/,
|
|
5262
|
+
severity: "high",
|
|
5263
|
+
category: "solidity",
|
|
5264
|
+
confidence: "low",
|
|
5265
|
+
languages: RUST
|
|
5266
|
+
},
|
|
5267
|
+
{
|
|
5268
|
+
id: "cp-sl-account-substitution",
|
|
5269
|
+
title: "Solana: account passed without has_one or constraint binding",
|
|
5270
|
+
description: "An account is destructured from `ctx.accounts` and read/written, but the program never checks that this account is the one expected by the user's PDA-derived state. An attacker can substitute any same-typed account.",
|
|
5271
|
+
suggestion: "Use Anchor's `#[account(has_one = expected_field)]` or `#[account(constraint = a.key() == state.b)]` to bind related accounts. Raw: `require_keys_eq!(ctx.accounts.target.key(), state.target);`",
|
|
5272
|
+
multilinePattern: /#\[derive\s*\(\s*Accounts\s*\)\][\s\S]{0,2000}?pub\s+\w+\s*:\s*Account\s*<\s*'info\s*,\s*\w+\s*>\s*,(?![\s\S]{0,2000}?(?:has_one\s*=|constraint\s*=|seeds\s*=))/,
|
|
5273
|
+
// Downgraded to medium: it's a "review needed" heuristic, not a confirmed exploit.
|
|
5274
|
+
severity: "medium",
|
|
5275
|
+
category: "solidity",
|
|
5276
|
+
confidence: "low",
|
|
5277
|
+
languages: RUST
|
|
5278
|
+
},
|
|
5279
|
+
{
|
|
5280
|
+
id: "cp-sl-pyth-staleness",
|
|
5281
|
+
title: "Solana: Pyth/Switchboard price used without staleness check",
|
|
5282
|
+
description: "Pyth `get_price_unchecked` (or Switchboard `get_result`) returns a price that may be stale \u2014 the publisher could be offline. Using this for collateral pricing without checking `publish_time` is the Solana-side equivalent of the Mango oracle exploit ($114M).",
|
|
5283
|
+
suggestion: "Use Pyth's `get_price_no_older_than(clock, MAX_AGE)` (or `get_ema_price_no_older_than`). For Switchboard, check `result.last_update_timestamp` against `clock.unix_timestamp`.",
|
|
5284
|
+
multilinePattern: /(?:get_price_unchecked|get_price_no_older|get_ema_price_unchecked|get_result\s*\(\s*\))\s*\([\s\S]{0,200}?\)(?![\s\S]{0,1200}?(?:publish_time|MAX_AGE|max_age|staleness|STALENESS|unix_timestamp\s*-\s*\w+|clock\.\s*unix_timestamp))/,
|
|
5285
|
+
severity: "high",
|
|
5286
|
+
category: "solidity",
|
|
5287
|
+
confidence: "medium",
|
|
5288
|
+
languages: RUST
|
|
5289
|
+
},
|
|
5290
|
+
{
|
|
5291
|
+
id: "cp-sl-cpi-reentrancy",
|
|
5292
|
+
title: "Solana: state mutation after cross-program invocation",
|
|
5293
|
+
description: "Anchor checks-effects-interactions: state should be updated BEFORE invoking another program. A CPI followed by state mutation invites cross-program reentrancy, especially with callback programs.",
|
|
5294
|
+
suggestion: "Move state writes to BEFORE the `invoke` / `invoke_signed` / `CpiContext::new` call. Or use Anchor's account locking + a reentrancy guard flag.",
|
|
5295
|
+
multilinePattern: /(?:invoke|invoke_signed|CpiContext::new(?:_with_signer)?)\s*\([\s\S]{0,400}?\)\s*\?\s*;[\s\S]{0,400}?(?:\.\s*\w+\s*=\s*\w+|state\.\s*\w+\s*=|account\.\s*\w+\s*=)/,
|
|
5296
|
+
severity: "high",
|
|
5297
|
+
category: "solidity",
|
|
5298
|
+
confidence: "low",
|
|
5299
|
+
languages: RUST
|
|
5300
|
+
},
|
|
5301
|
+
{
|
|
5302
|
+
id: "cp-sl-arbitrary-cpi",
|
|
5303
|
+
title: "Solana: program passed as account, used for arbitrary CPI",
|
|
5304
|
+
description: "An `AccountInfo` (or untyped account) is used as the `program_id` for an `invoke()` call. An attacker who can choose the program account effectively gets arbitrary cross-program invocation \u2014 equivalent to delegatecall in EVM.",
|
|
5305
|
+
suggestion: "Type the program field as `Program<'info, MyProgram>` (Anchor) so the runtime enforces the program's pubkey. Or `require_keys_eq!(program.key(), expected_program_id);` before invoke.",
|
|
5306
|
+
multilinePattern: /invoke(?:_signed)?\s*\(\s*&Instruction\s*\{\s*program_id\s*:\s*[*\w.()]+\s*,/,
|
|
5307
|
+
severity: "critical",
|
|
5308
|
+
category: "solidity",
|
|
5309
|
+
confidence: "medium",
|
|
5310
|
+
languages: RUST
|
|
5311
|
+
}
|
|
5312
|
+
];
|
|
5167
5313
|
var ALL_RULES2 = [
|
|
5168
5314
|
...securityRules,
|
|
5169
5315
|
...solidityRules2,
|
|
@@ -5179,7 +5325,9 @@ var ALL_RULES2 = [
|
|
|
5179
5325
|
...eip7702Rules,
|
|
5180
5326
|
...tstoreRules,
|
|
5181
5327
|
...uniswapV4Rules,
|
|
5182
|
-
...hackReplayRules
|
|
5328
|
+
...hackReplayRules,
|
|
5329
|
+
...modernDeFiRules,
|
|
5330
|
+
...solanaRules
|
|
5183
5331
|
];
|
|
5184
5332
|
|
|
5185
5333
|
// src/static/pattern-scanner.ts
|
|
@@ -5516,6 +5664,14 @@ function scanFile(relPath, content, rules, changedRanges) {
|
|
|
5516
5664
|
if (rule.id === "cp-clean-callback-hell" && isTestFile(relPath)) continue;
|
|
5517
5665
|
if (rule.id === "cp-sec-command-injection" && isScriptDir(relPath)) continue;
|
|
5518
5666
|
if (rule.id === "cp-hack-wormhole-unchecked-signature-set" && /\b(?:EIP712|DOMAIN_SEPARATOR|_hashTypedDataV4|PERMIT_TYPEHASH|DELEGATION_TYPEHASH|ERC1271|EIP712Upgradeable)\b/.test(content)) continue;
|
|
5667
|
+
if (rule.id === "cp-sol-eip712-missing-chainid" && /\b(?:block\.chainid|chainId|chainid)\b/.test(content)) continue;
|
|
5668
|
+
if (rule.id === "cp-sol-bridge-missing-source-check" && /\b(?:EntryPoint|UserOperation|PackedUserOperation|IERC7579|IERC4337|executionCalldata|onlyEntryPoint)\b/.test(content)) continue;
|
|
5669
|
+
if (rule.id === "cp-sl-missing-signer-check" && /\bSigner\s*<\s*'info\s*>/.test(content)) continue;
|
|
5670
|
+
if (rule.id === "cp-sl-ata-confusion" && /(?:mint\s*=\s*\w+|require_keys_eq!\s*\([^)]*\.mint|token::mint_to)/.test(content)) continue;
|
|
5671
|
+
if (rule.id === "cp-sl-account-substitution" && /#\[account\s*\([^)]*(?:has_one|constraint|seeds)\s*=/.test(content)) continue;
|
|
5672
|
+
if (rule.id === "cp-sl-pyth-staleness" && /(?:get_price_no_older_than|publish_time|MAX_AGE|max_age|stale|clock\.unix_timestamp\s*-)/.test(content)) continue;
|
|
5673
|
+
if (rule.id === "cp-sl-arbitrary-cpi" && /Program\s*<\s*'info\s*,/.test(content)) continue;
|
|
5674
|
+
if (rule.id === "cp-sl-missing-owner-check" && /(?:require_keys_eq!\s*\([^)]*\.owner|assert_eq!\s*\([^)]*\.owner|Account\s*<\s*'info|AccountLoader\s*<\s*'info)/.test(content)) continue;
|
|
5519
5675
|
rule.multilinePattern.lastIndex = 0;
|
|
5520
5676
|
const isGlobal = rule.multilinePattern.flags.includes("g");
|
|
5521
5677
|
let match;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elytrasec/engine",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "Core analysis engine for Elytra \u2014
|
|
3
|
+
"version": "0.4.6",
|
|
4
|
+
"description": "Core analysis engine for Elytra \u2014 188 detection rules across Solidity, Solana/Anchor (Rust), JS/TS, Python, Go, IaC. Includes 12 famous-hack patterns, 11 rug-surface checks, 5 modern-DeFi detectors, 7 Solana detectors.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ElytraSec <hello@elytrasec.io>",
|
|
7
7
|
"homepage": "https://elytrasec.io",
|