@elytrasec/engine 0.4.6 → 0.4.7

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.
Files changed (2) hide show
  1. package/dist/index.js +97 -2
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -5308,6 +5308,99 @@ var solanaRules = [
5308
5308
  category: "solidity",
5309
5309
  confidence: "medium",
5310
5310
  languages: RUST
5311
+ },
5312
+ {
5313
+ id: "cp-sl-close-account-no-lamport-check",
5314
+ title: "Solana: account closed without verifying lamport destination",
5315
+ 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.",
5316
+ 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.",
5317
+ // Match only when close=X appears in a #[account(...)] block that has NO has_one / constraint
5318
+ // in the same block (tempered greedy excludes those keywords inside the brackets).
5319
+ multilinePattern: /#\[account\s*\((?:(?!has_one|constraint)[^)\n]){0,400}close\s*=\s*\w+(?:(?!has_one|constraint)[^)\n]){0,400}\)\]/,
5320
+ severity: "high",
5321
+ category: "solidity",
5322
+ confidence: "low",
5323
+ languages: RUST
5324
+ },
5325
+ {
5326
+ id: "cp-sl-lamport-overflow",
5327
+ title: "Solana: unchecked lamport arithmetic (overflow / underflow)",
5328
+ 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.",
5329
+ suggestion: "Use `lamports.borrow_mut().checked_add(amount).ok_or(MyError::Overflow)?` and matching `checked_sub`. Or use `**lamports = lamports.checked_add(X)?`.",
5330
+ pattern: /\*\*[\w.]+?\.lamports\s*\.\s*borrow_mut\s*\(\s*\)\s*[+\-]=/,
5331
+ severity: "high",
5332
+ category: "solidity",
5333
+ confidence: "medium",
5334
+ languages: RUST
5335
+ },
5336
+ {
5337
+ id: "cp-sl-sysvar-substitution",
5338
+ title: "Solana: sysvar accepted as AccountInfo (substitution risk)",
5339
+ 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.",
5340
+ suggestion: "Use typed Anchor sysvar wrappers: `pub clock: Sysvar<'info, Clock>`, `pub rent: Sysvar<'info, Rent>`. The runtime enforces the correct pubkey.",
5341
+ multilinePattern: /pub\s+(?:clock|rent|epoch_schedule|slot_hashes|recent_blockhashes|instructions)\s*:\s*AccountInfo\s*<\s*'info\s*>/,
5342
+ severity: "high",
5343
+ category: "solidity",
5344
+ confidence: "high",
5345
+ languages: RUST
5346
+ },
5347
+ {
5348
+ id: "cp-sl-pda-bump-not-stored",
5349
+ title: "Solana: PDA bump derived but not stored on account",
5350
+ 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.",
5351
+ suggestion: "Anchor: declare the seed + bump in the account constraint: `#[account(seeds = [...], bump = state.bump)]`. Store the bump on first init.",
5352
+ pattern: /Pubkey::find_program_address\s*\(/,
5353
+ severity: "low",
5354
+ category: "solidity",
5355
+ confidence: "low",
5356
+ languages: RUST
5357
+ },
5358
+ {
5359
+ id: "cp-sl-multisig-threshold-check",
5360
+ title: "Solana: multisig execution without threshold validation",
5361
+ 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.",
5362
+ suggestion: "Add `require!(approvals.len() as u8 >= multisig.threshold, MyError::QuorumNotReached)` before the dispatch.",
5363
+ // Allow qualified field access like multisig.threshold, state.quorum, etc.
5364
+ 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))/,
5365
+ severity: "high",
5366
+ category: "solidity",
5367
+ confidence: "low",
5368
+ languages: RUST
5369
+ }
5370
+ ];
5371
+ var modernSolidityV2Rules = [
5372
+ {
5373
+ id: "cp-sol-eip1271-replay",
5374
+ title: "EIP-1271 isValidSignature without nonce / expiry",
5375
+ 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.",
5376
+ 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.",
5377
+ multilinePattern: /function\s+isValidSignature\s*\([^)]*\)\s*(?:external|public)[^{;]*\{(?![\s\S]{0,1500}?(?:nonce|deadline|expiry|chainid|usedSig|_consumeNonce|_useNonce))/,
5378
+ severity: "high",
5379
+ category: "solidity",
5380
+ confidence: "medium",
5381
+ languages: SOL
5382
+ },
5383
+ {
5384
+ id: "cp-sol-safemint-reentrancy",
5385
+ title: "ERC-721 _safeMint reentrancy via onERC721Received callback",
5386
+ 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.",
5387
+ 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`.",
5388
+ multilinePattern: /_safeMint\s*\([^)]*\)\s*;[\s\S]{0,300}?(?:totalSupply|_mintedCount|mintedPerUser|whitelist\s*\[[^\]]*\]\s*=|_balanceOf\s*\[[^\]]*\]\s*=|\.\s*\w+\s*\+=\s*1)/,
5389
+ severity: "high",
5390
+ category: "solidity",
5391
+ confidence: "medium",
5392
+ languages: SOL
5393
+ },
5394
+ {
5395
+ id: "cp-sol-voting-snapshot-bypass",
5396
+ title: "Voting power read from spot balanceOf (snapshot manipulation)",
5397
+ 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.",
5398
+ suggestion: "Use `getPriorVotes(voter, proposalSnapshotBlock)` (Compound), `ERC20Votes.getPastVotes`, or maintain a delegated-balance snapshot via Comp/OZ Votes. Never use `balanceOf` for voting weight.",
5399
+ 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))/,
5400
+ severity: "critical",
5401
+ category: "solidity",
5402
+ confidence: "medium",
5403
+ languages: SOL
5311
5404
  }
5312
5405
  ];
5313
5406
  var ALL_RULES2 = [
@@ -5327,7 +5420,8 @@ var ALL_RULES2 = [
5327
5420
  ...uniswapV4Rules,
5328
5421
  ...hackReplayRules,
5329
5422
  ...modernDeFiRules,
5330
- ...solanaRules
5423
+ ...solanaRules,
5424
+ ...modernSolidityV2Rules
5331
5425
  ];
5332
5426
 
5333
5427
  // src/static/pattern-scanner.ts
@@ -5364,7 +5458,7 @@ function isDocsFile(filePath) {
5364
5458
  }
5365
5459
  function isCommentLine(line) {
5366
5460
  const trimmed = line.trim();
5367
- return /^(?:\/\/|#|\/?\*|<!--)/.test(trimmed);
5461
+ return /^(?!\*\*\w)(?:\/\/|#|\/?\*|<!--)/.test(trimmed);
5368
5462
  }
5369
5463
  function isImportLine(line) {
5370
5464
  const trimmed = line.trim();
@@ -5672,6 +5766,7 @@ function scanFile(relPath, content, rules, changedRanges) {
5672
5766
  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
5767
  if (rule.id === "cp-sl-arbitrary-cpi" && /Program\s*<\s*'info\s*,/.test(content)) continue;
5674
5768
  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;
5769
+ if (rule.id === "cp-sol-voting-snapshot-bypass" && /(?:getPriorVotes|getPastVotes|getVotes|_snapshot|checkpoints|ERC20Votes|VotesUpgradeable|delegateBySig)/.test(content)) continue;
5675
5770
  rule.multilinePattern.lastIndex = 0;
5676
5771
  const isGlobal = rule.multilinePattern.flags.includes("g");
5677
5772
  let match;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elytrasec/engine",
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.",
3
+ "version": "0.4.7",
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",