@elytrasec/engine 0.4.6 → 0.4.8
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 +100 -3
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -3985,7 +3985,9 @@ var performanceRules = [
|
|
|
3985
3985
|
title: "Potential N+1 query \u2014 database call inside a loop",
|
|
3986
3986
|
description: "A database query inside a loop makes N separate round-trips instead of 1 batch query. This causes severe performance degradation at scale.",
|
|
3987
3987
|
suggestion: "Batch the queries: collect all IDs first, then execute a single WHERE IN query.",
|
|
3988
|
-
|
|
3988
|
+
// Tightened: only fire on PROMISE-shaped DB calls. Array.prototype.find/map on a local var
|
|
3989
|
+
// is NOT an N+1 — must be `await ...` OR namespaced like `db.find`/`prisma.user.findUnique`.
|
|
3990
|
+
multilinePattern: /(?:for|while)\s*\([^)]*\)\s*\{[\s\S]{0,500}?(?:await\s+\w+(?:\.\w+)*\.(?:find|findOne|findUnique|query|execute)\s*\(|\b(?:db|prisma|sequelize|knex|mongoose|repo|repository)\.\w+\.\s*(?:find|findOne|findUnique|query|execute)\s*\(|SELECT\b.*FROM)/,
|
|
3989
3991
|
severity: "high",
|
|
3990
3992
|
category: "performance",
|
|
3991
3993
|
confidence: "medium",
|
|
@@ -5308,6 +5310,99 @@ var solanaRules = [
|
|
|
5308
5310
|
category: "solidity",
|
|
5309
5311
|
confidence: "medium",
|
|
5310
5312
|
languages: RUST
|
|
5313
|
+
},
|
|
5314
|
+
{
|
|
5315
|
+
id: "cp-sl-close-account-no-lamport-check",
|
|
5316
|
+
title: "Solana: account closed without verifying lamport destination",
|
|
5317
|
+
description: "Using `#[account(close = destination)]` or manually closing an account transfers its rent-exempt SOL to the destination. If `destination` isn't validated (constrained to the rightful owner), an attacker can drain rent into their own wallet on every close.",
|
|
5318
|
+
suggestion: "Constrain the close destination: `#[account(close = expected_owner, has_one = expected_owner)]`. Or verify with `require_keys_eq!(destination.key(), authority.key())` before transfer.",
|
|
5319
|
+
// Match only when close=X appears in a #[account(...)] block that has NO has_one / constraint
|
|
5320
|
+
// in the same block (tempered greedy excludes those keywords inside the brackets).
|
|
5321
|
+
multilinePattern: /#\[account\s*\((?:(?!has_one|constraint)[^)\n]){0,400}close\s*=\s*\w+(?:(?!has_one|constraint)[^)\n]){0,400}\)\]/,
|
|
5322
|
+
severity: "high",
|
|
5323
|
+
category: "solidity",
|
|
5324
|
+
confidence: "low",
|
|
5325
|
+
languages: RUST
|
|
5326
|
+
},
|
|
5327
|
+
{
|
|
5328
|
+
id: "cp-sl-lamport-overflow",
|
|
5329
|
+
title: "Solana: unchecked lamport arithmetic (overflow / underflow)",
|
|
5330
|
+
description: "Direct mutation of `account.lamports.borrow_mut() += X` or `-= X` without `checked_add` / `checked_sub` can overflow on large mints or underflow on overdraft. Solana's runtime does NOT panic on integer overflow in release mode.",
|
|
5331
|
+
suggestion: "Use `lamports.borrow_mut().checked_add(amount).ok_or(MyError::Overflow)?` and matching `checked_sub`. Or use `**lamports = lamports.checked_add(X)?`.",
|
|
5332
|
+
pattern: /\*\*[\w.]+?\.lamports\s*\.\s*borrow_mut\s*\(\s*\)\s*[+\-]=/,
|
|
5333
|
+
severity: "high",
|
|
5334
|
+
category: "solidity",
|
|
5335
|
+
confidence: "medium",
|
|
5336
|
+
languages: RUST
|
|
5337
|
+
},
|
|
5338
|
+
{
|
|
5339
|
+
id: "cp-sl-sysvar-substitution",
|
|
5340
|
+
title: "Solana: sysvar accepted as AccountInfo (substitution risk)",
|
|
5341
|
+
description: "Accepting `Clock`, `Rent`, `EpochSchedule`, or `SlotHashes` as raw `AccountInfo` allows an attacker to substitute a forged account holding crafted bytes. Type confusion \u2192 wrong clock values \u2192 expiry bypass.",
|
|
5342
|
+
suggestion: "Use typed Anchor sysvar wrappers: `pub clock: Sysvar<'info, Clock>`, `pub rent: Sysvar<'info, Rent>`. The runtime enforces the correct pubkey.",
|
|
5343
|
+
multilinePattern: /pub\s+(?:clock|rent|epoch_schedule|slot_hashes|recent_blockhashes|instructions)\s*:\s*AccountInfo\s*<\s*'info\s*>/,
|
|
5344
|
+
severity: "high",
|
|
5345
|
+
category: "solidity",
|
|
5346
|
+
confidence: "high",
|
|
5347
|
+
languages: RUST
|
|
5348
|
+
},
|
|
5349
|
+
{
|
|
5350
|
+
id: "cp-sl-pda-bump-not-stored",
|
|
5351
|
+
title: "Solana: PDA bump derived but not stored on account",
|
|
5352
|
+
description: "Calling `Pubkey::find_program_address(seeds, program_id)` recomputes the canonical bump every time \u2014 that's 32+ ed25519 ops per call. More critically, if you don't STORE the bump on the account, attackers can derive alternative PDAs with non-canonical bumps and break invariants.",
|
|
5353
|
+
suggestion: "Anchor: declare the seed + bump in the account constraint: `#[account(seeds = [...], bump = state.bump)]`. Store the bump on first init.",
|
|
5354
|
+
pattern: /Pubkey::find_program_address\s*\(/,
|
|
5355
|
+
severity: "low",
|
|
5356
|
+
category: "solidity",
|
|
5357
|
+
confidence: "low",
|
|
5358
|
+
languages: RUST
|
|
5359
|
+
},
|
|
5360
|
+
{
|
|
5361
|
+
id: "cp-sl-multisig-threshold-check",
|
|
5362
|
+
title: "Solana: multisig execution without threshold validation",
|
|
5363
|
+
description: "A function checks individual signer authority but never validates that the count of signers meets the multisig threshold. Squads, Realms, and similar multisig programs must enforce `signers.len() >= threshold` before executing.",
|
|
5364
|
+
suggestion: "Add `require!(approvals.len() as u8 >= multisig.threshold, MyError::QuorumNotReached)` before the dispatch.",
|
|
5365
|
+
// Allow qualified field access like multisig.threshold, state.quorum, etc.
|
|
5366
|
+
multilinePattern: /(?:signers|approvals|approvers|signatures|members)\s*\.\s*len\s*\(\s*\)(?![\s\S]{0,500}?(?:>=|>)\s*(?:\w+\.)?(?:threshold|quorum|min_signers|required_signers|min_approvals|m_of_n))/,
|
|
5367
|
+
severity: "high",
|
|
5368
|
+
category: "solidity",
|
|
5369
|
+
confidence: "low",
|
|
5370
|
+
languages: RUST
|
|
5371
|
+
}
|
|
5372
|
+
];
|
|
5373
|
+
var modernSolidityV2Rules = [
|
|
5374
|
+
{
|
|
5375
|
+
id: "cp-sol-eip1271-replay",
|
|
5376
|
+
title: "EIP-1271 isValidSignature without nonce / expiry",
|
|
5377
|
+
description: "Smart-contract signatures via `isValidSignature` (ERC-1271) that don't bind to a nonce + expiry can be replayed indefinitely. Permit2 leak-class attacks of 2024 exploited exactly this. Once a signature is leaked, it's valid forever and on every chain.",
|
|
5378
|
+
suggestion: "Bind every accepted signature to: (a) a per-account nonce, (b) a deadline / expiry timestamp, AND (c) `block.chainid` in the typed-data domain. Increment nonce on successful verify.",
|
|
5379
|
+
multilinePattern: /function\s+isValidSignature\s*\([^)]*\)\s*(?:external|public)[^{;]*\{(?![\s\S]{0,1500}?(?:nonce|deadline|expiry|chainid|usedSig|_consumeNonce|_useNonce))/,
|
|
5380
|
+
severity: "high",
|
|
5381
|
+
category: "solidity",
|
|
5382
|
+
confidence: "medium",
|
|
5383
|
+
languages: SOL
|
|
5384
|
+
},
|
|
5385
|
+
{
|
|
5386
|
+
id: "cp-sol-safemint-reentrancy",
|
|
5387
|
+
title: "ERC-721 _safeMint reentrancy via onERC721Received callback",
|
|
5388
|
+
description: "`_safeMint` calls the recipient's `onERC721Received` hook. If state (totalSupply, balances, mint cap) is updated AFTER the safeMint, the recipient contract can re-enter and bypass the cap. Several memecoin / NFT mints have lost their entire whitelist allocation this way.",
|
|
5389
|
+
suggestion: "Apply checks-effects-interactions: update mintCount, totalSupply, whitelist-spent BEFORE calling `_safeMint`. Or use `_mint` (no callback) when you don't need ERC-721-receiver compatibility, or add `nonReentrant`.",
|
|
5390
|
+
multilinePattern: /_safeMint\s*\([^)]*\)\s*;[\s\S]{0,300}?(?:totalSupply|_mintedCount|mintedPerUser|whitelist\s*\[[^\]]*\]\s*=|_balanceOf\s*\[[^\]]*\]\s*=|\.\s*\w+\s*\+=\s*1)/,
|
|
5391
|
+
severity: "high",
|
|
5392
|
+
category: "solidity",
|
|
5393
|
+
confidence: "medium",
|
|
5394
|
+
languages: SOL
|
|
5395
|
+
},
|
|
5396
|
+
{
|
|
5397
|
+
id: "cp-sol-voting-snapshot-bypass",
|
|
5398
|
+
title: "Voting power read from spot balanceOf (snapshot manipulation)",
|
|
5399
|
+
description: "A governance proposal reads voting weight from `balanceOf(voter)` at vote time. Attacker flash-borrows the governance token, votes, returns the loan in the same tx. This is distinct from the Beanstalk same-block execute bug \u2014 here even a 24h timelock won't help because the bad vote is already counted.",
|
|
5400
|
+
suggestion: "Use `getPriorVotes(voter, proposalSnapshotBlock)` (Compound), `ERC20Votes.getPastVotes`, or maintain a delegated-balance snapshot via Comp/OZ Votes. Never use `balanceOf` for voting weight.",
|
|
5401
|
+
multilinePattern: /function\s+(?:castVote|vote|castVoteWithReason|_castVote)\s*\([^)]*\)[^{;]*\{[\s\S]{0,800}?balanceOf\s*\(\s*(?:msg\.sender|voter|\w+)\s*\)(?![\s\S]{0,400}?(?:getPriorVotes|getPastVotes|getVotes|snapshot|_snapshot|checkpoints))/,
|
|
5402
|
+
severity: "critical",
|
|
5403
|
+
category: "solidity",
|
|
5404
|
+
confidence: "medium",
|
|
5405
|
+
languages: SOL
|
|
5311
5406
|
}
|
|
5312
5407
|
];
|
|
5313
5408
|
var ALL_RULES2 = [
|
|
@@ -5327,7 +5422,8 @@ var ALL_RULES2 = [
|
|
|
5327
5422
|
...uniswapV4Rules,
|
|
5328
5423
|
...hackReplayRules,
|
|
5329
5424
|
...modernDeFiRules,
|
|
5330
|
-
...solanaRules
|
|
5425
|
+
...solanaRules,
|
|
5426
|
+
...modernSolidityV2Rules
|
|
5331
5427
|
];
|
|
5332
5428
|
|
|
5333
5429
|
// src/static/pattern-scanner.ts
|
|
@@ -5364,7 +5460,7 @@ function isDocsFile(filePath) {
|
|
|
5364
5460
|
}
|
|
5365
5461
|
function isCommentLine(line) {
|
|
5366
5462
|
const trimmed = line.trim();
|
|
5367
|
-
return /^(?:\/\/|#|\/?\*|<!--)/.test(trimmed);
|
|
5463
|
+
return /^(?!\*\*\w)(?:\/\/|#|\/?\*|<!--)/.test(trimmed);
|
|
5368
5464
|
}
|
|
5369
5465
|
function isImportLine(line) {
|
|
5370
5466
|
const trimmed = line.trim();
|
|
@@ -5672,6 +5768,7 @@ function scanFile(relPath, content, rules, changedRanges) {
|
|
|
5672
5768
|
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
5769
|
if (rule.id === "cp-sl-arbitrary-cpi" && /Program\s*<\s*'info\s*,/.test(content)) continue;
|
|
5674
5770
|
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;
|
|
5771
|
+
if (rule.id === "cp-sol-voting-snapshot-bypass" && /(?:getPriorVotes|getPastVotes|getVotes|_snapshot|checkpoints|ERC20Votes|VotesUpgradeable|delegateBySig)/.test(content)) continue;
|
|
5675
5772
|
rule.multilinePattern.lastIndex = 0;
|
|
5676
5773
|
const isGlobal = rule.multilinePattern.flags.includes("g");
|
|
5677
5774
|
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.8",
|
|
4
|
+
"description": "Core analysis engine for Elytra \u2014 196 detection rules across Solidity, Solana/Anchor (Rust), JS/TS, Python, Go, IaC. 12 famous-hack patterns, 11 rug-surface, 5 modern-DeFi + 3 modern-Solidity-v2, 12 Solana detectors.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ElytraSec <hello@elytrasec.io>",
|
|
7
7
|
"homepage": "https://elytrasec.io",
|