eslint-plugin-node-security 5.2.3 → 5.3.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/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  <p align="center">
2
- <a href="https://eslint.interlace.tools/?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security" target="blank"><img src="https://eslint.interlace.tools/logos/interlace.svg" alt="Interlace" height="90" /></a>
2
+ <a href="https://eslint.interlace.tools/?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security" target="blank"><picture><source media="(prefers-color-scheme: dark)" srcset="https://eslint.interlace.tools/logos/interlace-dark.svg"><img src="https://eslint.interlace.tools/logos/interlace-light.svg" alt="Interlace" height="90" /></picture></a>
3
3
  &nbsp;&nbsp;
4
4
  <a href="https://nodejs.org" target="_blank"><img src="https://eslint.interlace.tools/logos/node.svg" alt="Node.js" height="90" /></a>
5
5
  &nbsp;&nbsp;
6
- <a href="https://oxc.rs" target="_blank"><img src="https://eslint.interlace.tools/logos/oxlint.svg" alt="oxlint" height="90" /></a>
6
+ <a href="https://oxc.rs" target="_blank"><picture><source media="(prefers-color-scheme: dark)" srcset="https://eslint.interlace.tools/logos/oxlint-dark.svg"><img src="https://eslint.interlace.tools/logos/oxlint-light.svg" alt="oxlint" height="90" /></picture></a>
7
7
  &nbsp;&nbsp;
8
- <a href="https://eslint.org" target="_blank"><img src="https://eslint.interlace.tools/logos/eslint.svg" alt="ESLint" height="90" /></a>
8
+ <a href="https://eslint.org" target="_blank"><picture><source media="(prefers-color-scheme: dark)" srcset="https://eslint.interlace.tools/logos/eslint-dark.svg"><img src="https://eslint.interlace.tools/logos/eslint-light.svg" alt="ESLint" height="90" /></picture></a>
9
9
  </p>
10
10
 
11
11
  <p align="center">
@@ -15,6 +15,7 @@
15
15
  <p align="center">
16
16
  <a href="https://www.npmjs.com/package/eslint-plugin-node-security" target="_blank"><img src="https://img.shields.io/npm/v/eslint-plugin-node-security.svg" alt="NPM Version" /></a>
17
17
  <a href="https://www.npmjs.com/package/eslint-plugin-node-security" target="_blank"><img src="https://img.shields.io/npm/dm/eslint-plugin-node-security.svg" alt="NPM Downloads" /></a>
18
+ <a href="https://packagephobia.com/result?p=eslint-plugin-node-security" target="_blank"><img src="https://badgen.net/packagephobia/install/eslint-plugin-node-security" alt="Install Size" /></a>
18
19
  <a href="https://opensource.org/licenses/MIT" target="_blank"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="Package License" /></a>
19
20
  <a href="https://app.codecov.io/gh/ofri-peretz/eslint/components?components%5B0%5D=eslint-plugin-node-security" target="_blank"><img src="https://codecov.io/gh/ofri-peretz/eslint/graph/badge.svg?component=eslint-plugin-node-security" alt="Codecov" /></a>
20
21
  <a href="https://github.com/ofri-peretz/eslint" target="_blank"><img src="https://img.shields.io/badge/Since-Dec_2025-blue?logo=rocket&logoColor=white" alt="Since Dec 2025" /></a>
@@ -27,57 +28,25 @@
27
28
 
28
29
  This plugin provides Security rules for Node.js core modules (fs, child_process, crypto, etc).
29
30
 
30
- ## Philosophy
31
-
32
- **Interlace** fosters **strength through integration**. Instead of stacking isolated rules, we **interlace** security directly into your workflow to create a resilient fabric of code. We believe tools should **guide rather than gatekeep**, providing educational feedback that strengthens the developer with every interaction.
33
-
34
31
  <!-- AUTO-GENERATED:DOCTRINE:START - Do not edit manually -->
35
32
 
36
- ## Why these rules are quiet
37
-
38
- **Noise creates apathy, and apathy is not a security posture.** A linter that reports
39
- a thousand things a week gets switched off in a month, and the real finding goes with
40
- it. So every rule here is built to be worth reading: we would rather miss a finding
41
- than spend your attention on one that was never real.
42
-
43
- That is a trade, and it is made deliberately. It costs recall, and we measure what it
44
- costs rather than assuming it is free.
45
-
46
- ## How the rules decide
47
-
48
- **Evidence, not names.** A rule fires on what the code *does*, resolved through the
49
- AST and ESLint's own scope analysis — not on an identifier that happens to contain
50
- `query`, a method called `setItem`, or a file whose path contains `key`. Every one of
51
- those was a real false positive in this ecosystem, found by reading our own output on
52
- open-source projects and fixed with a test that fails on the unfixed rule.
53
-
54
- Where a rule has known false-positive shapes, its page carries a **Not a finding**
55
- section: what it deliberately stays quiet on, and what to check first when it fires
56
- and you disagree.
33
+ - **Why** a linter nobody reads protects nothing. We would rather miss a finding
34
+ than spend your attention on one that was never real.
35
+ - **How** evidence, not names. A rule fires on what the code *does*, resolved
36
+ through the AST and ESLint's own scope analysis.
37
+ - **What** every finding carries its fix, in prose for a human and as structured
38
+ JSON for an agent. Security rules add a CWE mapping and, where assigned, a CVSS score.
57
39
 
58
- ## What you get
59
-
60
- The rules below. Security rules carry a CWE mapping and, where one is assigned, a
61
- CVSS score; every rule carries a fix on its message — in prose for a human and as
62
- structured JSON for an agent. Install it, enable
63
- `recommended`, and read the findings. If one of them is wrong,
64
- [open an issue](https://github.com/ofri-peretz/eslint/issues) — a false positive is a
65
- bug here, not a tuning exercise for you.
66
-
67
- How that is measured, on which projects, and where it falls short:
68
- [benchmark methodology](https://github.com/ofri-peretz/eslint/blob/main/BENCHMARK-METHODOLOGY.md)
69
- and [results](https://github.com/ofri-peretz/eslint/blob/main/BENCHMARK-RESULTS.md).
40
+ That trade costs recall, and we measure it:
41
+ [methodology](https://github.com/ofri-peretz/eslint/blob/main/BENCHMARK-METHODOLOGY.md)
42
+ · [results](https://github.com/ofri-peretz/eslint/blob/main/BENCHMARK-RESULTS.md)
43
+ · [a false positive is a bug](https://github.com/ofri-peretz/eslint/issues).
70
44
 
71
45
  <!-- AUTO-GENERATED:DOCTRINE:END -->
72
46
 
73
47
  ## Getting Started
74
48
 
75
49
  - To check out the [guide](https://eslint.interlace.tools/docs/security/plugin-node-security?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security), visit [eslint.interlace.tools](https://eslint.interlace.tools/?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security). 📚
76
- - 要查看中文 [指南](https://eslint.interlace.tools/docs/security/plugin-node-security?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security), 请访问 [eslint.interlace.tools](https://eslint.interlace.tools/?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security). 📚
77
- - [가이드](https://eslint.interlace.tools/docs/security/plugin-node-security?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security) 문서는 [eslint.interlace.tools](https://eslint.interlace.tools/?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security)에서 확인하실 수 있습니다. 📚
78
- - [ガイド](https://eslint.interlace.tools/docs/security/plugin-node-security?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security)は [eslint.interlace.tools](https://eslint.interlace.tools/?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security)でご確認ください。 📚
79
- - Para ver la [guía](https://eslint.interlace.tools/docs/security/plugin-node-security?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security), visita [eslint.interlace.tools](https://eslint.interlace.tools/?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security). 📚
80
- - للاطلاع على [الدليل](https://eslint.interlace.tools/docs/security/plugin-node-security?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security)، قم بزيارة [eslint.interlace.tools](https://eslint.interlace.tools/?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security). 📚
81
50
 
82
51
  ```bash
83
52
  npm install eslint-plugin-node-security --save-dev
@@ -163,28 +132,57 @@ See the [ESLint Version Support Policy](../../docs/ESLINT_VERSION_SUPPORT.md)
163
132
  | [prefer-native-crypto](https://eslint.interlace.tools/docs/security/plugin-node-security/rules/prefer-native-crypto?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security) | CWE-1104 | A06:2021 | | Prefer native crypto over third-party libraries | 🟢 | | | | | |
164
133
  | [require-aead-tag-verification](https://eslint.interlace.tools/docs/security/plugin-node-security/rules/require-aead-tag-verification?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security) | CWE-327 | A02:2021 | | Require AEAD decryption to verify the authentication tag (setAuthTag + final) | 🟢 | 💼 | | | | |
165
134
  | [require-dependency-integrity](https://eslint.interlace.tools/docs/security/plugin-node-security/rules/require-dependency-integrity?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security) | CWE-494 | | | CWE: [CWE-494](https://cwe.mitre.org/data/definitions/494.html) | 🟢 | 💼 | | | | |
166
- | [require-secure-credential-storage](https://eslint.interlace.tools/docs/security/plugin-node-security/rules/require-secure-credential-storage?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security) | CWE-312 | | | This rule detects a credential written to localStorage, sessionStorage or AsyncStorage without encryption | 🟢 | | | | | |
135
+ | [require-secure-credential-storage](https://eslint.interlace.tools/docs/security/plugin-node-security/rules/require-secure-credential-storage?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security) | CWE-312 | | | This rule detects a credential written to localStorage, sessionStorage, AsyncStorage or process.env without | 🟢 | | | | | |
167
136
  | [require-secure-deletion](https://eslint.interlace.tools/docs/security/plugin-node-security/rules/require-secure-deletion?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security) | CWE-459 | | | CWE: [CWE-459](https://cwe.mitre.org/data/definitions/459.html) | 🟢 | | | | | |
168
137
  | [require-storage-encryption](https://eslint.interlace.tools/docs/security/plugin-node-security/rules/require-storage-encryption?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security) | CWE-312 | | | CWE: [CWE-312](https://cwe.mitre.org/data/definitions/312.html) | 🟢 | | | | | |
169
138
  | [require-stream-error-handler](https://eslint.interlace.tools/docs/security/plugin-node-security/rules/require-stream-error-handler?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security) | CWE-248 | A04:2021 | | Require an error listener on streams passed to pipe, which does not forward errors | 🟢 | 💼 | | | | |
170
139
  <!-- AUTO-GENERATED:RULES_TABLE:END -->
140
+ <!-- AUTO-GENERATED:ECOSYSTEM_TABLE:START - Do not edit manually -->
141
+
171
142
  ## 🔗 Related ESLint Plugins
172
143
 
173
- Part of the **Interlace ESLint Ecosystem** — AI-native security plugins with LLM-optimized error messages:
144
+ Part of the **Interlace ESLint ecosystem** — AI-native rules with LLM-optimized error messages:
145
+
146
+ **Security**
147
+
148
+ | Plugin | Downloads | Description |
149
+ | :--- | :---: | :--- |
150
+ | [`eslint-plugin-anthropic-security`](https://www.npmjs.com/package/eslint-plugin-anthropic-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-anthropic-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-anthropic-security) | Anthropic SDK security. |
151
+ | [`eslint-plugin-browser-security`](https://www.npmjs.com/package/eslint-plugin-browser-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-browser-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-browser-security) | XSS, DOM security. |
152
+ | [`eslint-plugin-drizzle-security`](https://www.npmjs.com/package/eslint-plugin-drizzle-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-drizzle-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-drizzle-security) | Drizzle security. |
153
+ | [`eslint-plugin-express-security`](https://www.npmjs.com/package/eslint-plugin-express-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-express-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-express-security) | Express middleware hardening. |
154
+ | [`eslint-plugin-gemini-security`](https://www.npmjs.com/package/eslint-plugin-gemini-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-gemini-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-gemini-security) | Google Gemini SDK security. |
155
+ | [`eslint-plugin-jwt-security`](https://www.npmjs.com/package/eslint-plugin-jwt-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-jwt-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-jwt-security) | Token security. |
156
+ | [`eslint-plugin-knex-security`](https://www.npmjs.com/package/eslint-plugin-knex-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-knex-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-knex-security) | Knex security. |
157
+ | [`eslint-plugin-lambda-security`](https://www.npmjs.com/package/eslint-plugin-lambda-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-lambda-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-lambda-security) | AWS Lambda hardening. |
158
+ | [`eslint-plugin-mcp-sdk-security`](https://www.npmjs.com/package/eslint-plugin-mcp-sdk-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-mcp-sdk-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-mcp-sdk-security) | MCP SDK security. |
159
+ | [`eslint-plugin-mongodb-security`](https://www.npmjs.com/package/eslint-plugin-mongodb-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-mongodb-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-mongodb-security) | MongoDB injection. |
160
+ | [`eslint-plugin-mysql-security`](https://www.npmjs.com/package/eslint-plugin-mysql-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-mysql-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-mysql-security) | MySQL security. |
161
+ | [`eslint-plugin-nestjs-security`](https://www.npmjs.com/package/eslint-plugin-nestjs-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-nestjs-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-nestjs-security) | NestJS framework hardening. |
162
+ | [`eslint-plugin-openai-security`](https://www.npmjs.com/package/eslint-plugin-openai-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-openai-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-openai-security) | OpenAI SDK security. |
163
+ | [`eslint-plugin-postgresql-security`](https://www.npmjs.com/package/eslint-plugin-postgresql-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-postgresql-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-postgresql-security) | PostgreSQL security. |
164
+ | [`eslint-plugin-prisma-security`](https://www.npmjs.com/package/eslint-plugin-prisma-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-prisma-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-prisma-security) | Prisma security. |
165
+ | [`eslint-plugin-secure-coding`](https://www.npmjs.com/package/eslint-plugin-secure-coding) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-secure-coding.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-secure-coding) | Injection prevention. |
166
+ | [`eslint-plugin-sequelize-security`](https://www.npmjs.com/package/eslint-plugin-sequelize-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-sequelize-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-sequelize-security) | Sequelize ORM security. |
167
+ | [`eslint-plugin-sqlite-security`](https://www.npmjs.com/package/eslint-plugin-sqlite-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-sqlite-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-sqlite-security) | SQLite security. |
168
+ | [`eslint-plugin-typeorm-security`](https://www.npmjs.com/package/eslint-plugin-typeorm-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-typeorm-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-typeorm-security) | TypeORM security. |
169
+ | [`eslint-plugin-vercel-ai-security`](https://www.npmjs.com/package/eslint-plugin-vercel-ai-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-vercel-ai-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-vercel-ai-security) | AI SDK security. |
170
+
171
+ **Code quality**
174
172
 
175
173
  | Plugin | Downloads | Description |
176
174
  | :--- | :---: | :--- |
177
- | [`eslint-plugin-secure-coding`](https://www.npmjs.com/package/eslint-plugin-secure-coding) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-secure-coding.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-secure-coding) | General security rules & OWASP guidelines. |
178
- | [`eslint-plugin-pg`](https://www.npmjs.com/package/eslint-plugin-pg) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-pg.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-pg) | PostgreSQL security & best practices. |
179
- | [`eslint-plugin-node-security`](https://www.npmjs.com/package/eslint-plugin-node-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-node-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-node-security) | Node.js core-module security (fs, child_process, vm, crypto, Buffer). |
180
- | [`eslint-plugin-jwt`](https://www.npmjs.com/package/eslint-plugin-jwt) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-jwt.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-jwt) | JWT security & best practices. |
181
- | [`eslint-plugin-browser-security`](https://www.npmjs.com/package/eslint-plugin-browser-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-browser-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-browser-security) | Browser-specific security & XSS prevention. |
182
- | [`eslint-plugin-express-security`](https://www.npmjs.com/package/eslint-plugin-express-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-express-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-express-security) | Express.js security hardening rules. |
183
- | [`eslint-plugin-lambda-security`](https://www.npmjs.com/package/eslint-plugin-lambda-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-lambda-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-lambda-security) | AWS Lambda security best practices. |
184
- | [`eslint-plugin-nestjs-security`](https://www.npmjs.com/package/eslint-plugin-nestjs-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-nestjs-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-nestjs-security) | NestJS security rules & patterns. |
185
- | [`eslint-plugin-mongodb-security`](https://www.npmjs.com/package/eslint-plugin-mongodb-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-mongodb-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-mongodb-security) | MongoDB security best practices. |
186
- | [`eslint-plugin-vercel-ai-security`](https://www.npmjs.com/package/eslint-plugin-vercel-ai-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-vercel-ai-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-vercel-ai-security) | Vercel AI SDK security hardening. |
187
- | [`eslint-plugin-import-next`](https://www.npmjs.com/package/eslint-plugin-import-next) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-import-next.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-import-next) | Next-gen import sorting & architecture. |
175
+ | [`eslint-plugin-conventions`](https://www.npmjs.com/package/eslint-plugin-conventions) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-conventions.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-conventions) | Team-specific habits and styles. |
176
+ | [`eslint-plugin-import-next`](https://www.npmjs.com/package/eslint-plugin-import-next) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-import-next.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-import-next) | Fast cycle + import-graph analysis. |
177
+ | [`eslint-plugin-maintainability`](https://www.npmjs.com/package/eslint-plugin-maintainability) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-maintainability.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-maintainability) | Cognitive load and clean-code patterns. |
178
+ | [`eslint-plugin-modernization`](https://www.npmjs.com/package/eslint-plugin-modernization) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-modernization.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-modernization) | ESNext migration + syntax evolution. |
179
+ | [`eslint-plugin-modularity`](https://www.npmjs.com/package/eslint-plugin-modularity) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-modularity.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-modularity) | Structural integrity and DDD patterns. |
180
+ | [`eslint-plugin-operability`](https://www.npmjs.com/package/eslint-plugin-operability) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-operability.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-operability) | Production readiness and resource health. |
181
+ | [`eslint-plugin-react-a11y`](https://www.npmjs.com/package/eslint-plugin-react-a11y) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-react-a11y.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-react-a11y) | React accessibility / WCAG. |
182
+ | [`eslint-plugin-react-features`](https://www.npmjs.com/package/eslint-plugin-react-features) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-react-features.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-react-features) | React best practices and optimization. |
183
+ | [`eslint-plugin-reliability`](https://www.npmjs.com/package/eslint-plugin-reliability) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-reliability.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-reliability) | Runtime stability and error safety. |
184
+
185
+ <!-- AUTO-GENERATED:ECOSYSTEM_TABLE:END -->
188
186
 
189
187
  <!-- INTERLACE:STAR_CTA:START -->
190
188
  ## ⭐ Support & follow
@@ -204,5 +202,5 @@ MIT © [Ofri Peretz](https://github.com/ofri-peretz)
204
202
  </p>
205
203
 
206
204
  <p align="center">
207
- <a href="https://eslint.interlace.tools/?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security" target="blank"><img src="https://eslint.interlace.tools/icon-light.svg" alt="Interlace" height="70" /></a>
205
+ <a href="https://eslint.interlace.tools/?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security" target="blank"><picture><source media="(prefers-color-scheme: dark)" srcset="https://eslint.interlace.tools/logos/interlace-dark.svg"><img src="https://eslint.interlace.tools/logos/interlace-light.svg" alt="Interlace" height="70" /></picture></a>
208
206
  </p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-node-security",
3
- "version": "5.2.3",
3
+ "version": "5.3.1",
4
4
  "description": "ESLint plugin for Node.js security — detects command injection, path traversal, SSRF, zip slip, and weak crypto (MD5/SHA-1, ECB, static IV) in fs, child_process, vm, and crypto.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -79,7 +79,7 @@
79
79
  "node": ">=18.0.0"
80
80
  },
81
81
  "dependencies": {
82
- "@interlace/eslint-devkit": "^1.17.3"
82
+ "@interlace/eslint-devkit": "^1.18.2"
83
83
  },
84
84
  "peerDependencies": {
85
85
  "eslint": "^8.40.0 || ^9.0.0 || ^10.0.0"
package/src/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.configs=exports.plugin=exports.rules=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.rules={get"detect-child-process"(){return require("./rules/detect-child-process").detectChildProcess},get"detect-eval-with-expression"(){return require("./rules/detect-eval-with-expression").detectEvalWithExpression},get"detect-non-literal-fs-filename"(){return require("./rules/detect-non-literal-fs-filename").detectNonLiteralFsFilename},get"no-unsafe-dynamic-require"(){return require("./rules/no-unsafe-dynamic-require").noUnsafeDynamicRequire},get"no-buffer-overread"(){return require("./rules/no-buffer-overread").noBufferOverread},get"no-deprecated-buffer"(){return require("./rules/no-deprecated-buffer").noDeprecatedBuffer},get"no-unsafe-buffer-alloc"(){return require("./rules/no-unsafe-buffer-alloc").noUnsafeBufferAlloc},get"no-toctou-vulnerability"(){return require("./rules/no-toctou-vulnerability").noToctouVulnerability},get"no-zip-slip"(){return require("./rules/no-zip-slip").noZipSlip},get"no-arbitrary-file-access"(){return require("./rules/no-arbitrary-file-access").noArbitraryFileAccess},get"no-data-in-temp-storage"(){return require("./rules/no-data-in-temp-storage").noDataInTempStorage},get"no-ssrf"(){return require("./rules/no-ssrf").noSsrf},get"no-shell-injection"(){return require("./rules/no-shell-injection").noShellInjection},get"no-dynamic-command-string"(){return require("./rules/no-dynamic-command-string").noDynamicCommandString},get"no-env-injection"(){return require("./rules/no-env-injection").noEnvInjection},get"no-dynamic-algorithm-selection"(){return require("./rules/no-dynamic-algorithm-selection").noDynamicAlgorithmSelection},get"detect-suspicious-dependencies"(){return require("./rules/detect-suspicious-dependencies").detectSuspiciousDependencies},get"lock-file"(){return require("./rules/lock-file").lockFile},get"no-dynamic-dependency-loading"(){return require("./rules/no-dynamic-dependency-loading").noDynamicDependencyLoading},get"require-dependency-integrity"(){return require("./rules/require-dependency-integrity").requireDependencyIntegrity},get"require-secure-credential-storage"(){return require("./rules/require-secure-credential-storage").requireSecureCredentialStorage},get"require-secure-deletion"(){return require("./rules/require-secure-deletion").requireSecureDeletion},get"require-storage-encryption"(){return require("./rules/require-storage-encryption").requireStorageEncryption},get"no-dynamic-require"(){return require("./rules/no-dynamic-require").noDynamicRequire},get"no-cryptojs"(){return require("./rules/no-cryptojs").noCryptojs},get"no-cryptojs-weak-random"(){return require("./rules/no-cryptojs-weak-random").noCryptojsWeakRandom},get"no-deprecated-cipher-method"(){return require("./rules/no-deprecated-cipher-method").noDeprecatedCipherMethod},get"no-ecb-mode"(){return require("./rules/no-ecb-mode").noEcbMode},get"no-insecure-key-derivation"(){return require("./rules/no-insecure-key-derivation").noInsecureKeyDerivation},get"no-insecure-rsa-padding"(){return require("./rules/no-insecure-rsa-padding").noInsecureRsaPadding},get"no-math-random-crypto"(){return require("./rules/no-math-random-crypto").noMathRandomCrypto},get"no-self-signed-certs"(){return require("./rules/no-self-signed-certs").noSelfSignedCerts},get"no-sha1-hash"(){return require("./rules/no-sha1-hash").noSha1Hash},get"no-static-iv"(){return require("./rules/no-static-iv").noStaticIv},get"no-timing-unsafe-compare"(){return require("./rules/no-timing-unsafe-compare").noTimingUnsafeCompare},get"no-weak-cipher-algorithm"(){return require("./rules/no-weak-cipher-algorithm").noWeakCipherAlgorithm},get"no-weak-hash-algorithm"(){return require("./rules/no-weak-hash-algorithm").noWeakHashAlgorithm},get"prefer-native-crypto"(){return require("./rules/prefer-native-crypto").preferNativeCrypto},get"require-aead-tag-verification"(){return require("./rules/require-aead-tag-verification").requireAeadTagVerification},get"no-unbounded-decompression"(){return require("./rules/no-unbounded-decompression").noUnboundedDecompression},get"no-insecure-http-parser"(){return require("./rules/no-insecure-http-parser").noInsecureHttpParser},get"require-stream-error-handler"(){return require("./rules/require-stream-error-handler").requireStreamErrorHandler}};(0,eslint_devkit_1.withCanonicalDocsUrls)("plugin-node-security",exports.rules);exports.plugin={meta:{name:"eslint-plugin-node-security",version:"5.2.2"},rules:exports.rules};const recommendedRules={"node-security/detect-child-process":"error","node-security/detect-eval-with-expression":"error","node-security/detect-non-literal-fs-filename":"warn","node-security/no-unsafe-dynamic-require":"error","node-security/no-buffer-overread":"warn","node-security/no-deprecated-buffer":"error","node-security/no-unsafe-buffer-alloc":"warn","node-security/no-toctou-vulnerability":"error","node-security/no-zip-slip":"error","node-security/no-arbitrary-file-access":"error","node-security/no-data-in-temp-storage":"error","node-security/no-ssrf":"warn","node-security/no-shell-injection":"error","node-security/no-dynamic-command-string":"error","node-security/no-env-injection":"error","node-security/no-dynamic-algorithm-selection":"error","node-security/no-timing-unsafe-compare":"warn","node-security/detect-suspicious-dependencies":"warn","node-security/require-dependency-integrity":"error","node-security/no-weak-hash-algorithm":"error","node-security/no-weak-cipher-algorithm":"error","node-security/no-static-iv":"error","node-security/no-ecb-mode":"error","node-security/no-math-random-crypto":"error","node-security/no-cryptojs":"error","node-security/no-self-signed-certs":"error","node-security/require-aead-tag-verification":"error","node-security/no-unbounded-decompression":"error","node-security/no-insecure-http-parser":"error","node-security/require-stream-error-handler":"error"};exports.configs={recommended:{plugins:{"node-security":exports.plugin},rules:recommendedRules},strict:{plugins:{"node-security":exports.plugin},rules:Object.fromEntries(Object.keys(exports.rules).map(ruleName=>[`node-security/${ruleName}`,"error"]))}};exports.default=exports.plugin;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.configs=exports.plugin=exports.rules=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.rules={get"detect-child-process"(){return require("./rules/detect-child-process").detectChildProcess},get"detect-eval-with-expression"(){return require("./rules/detect-eval-with-expression").detectEvalWithExpression},get"detect-non-literal-fs-filename"(){return require("./rules/detect-non-literal-fs-filename").detectNonLiteralFsFilename},get"no-unsafe-dynamic-require"(){return require("./rules/no-unsafe-dynamic-require").noUnsafeDynamicRequire},get"no-buffer-overread"(){return require("./rules/no-buffer-overread").noBufferOverread},get"no-deprecated-buffer"(){return require("./rules/no-deprecated-buffer").noDeprecatedBuffer},get"no-unsafe-buffer-alloc"(){return require("./rules/no-unsafe-buffer-alloc").noUnsafeBufferAlloc},get"no-toctou-vulnerability"(){return require("./rules/no-toctou-vulnerability").noToctouVulnerability},get"no-zip-slip"(){return require("./rules/no-zip-slip").noZipSlip},get"no-arbitrary-file-access"(){return require("./rules/no-arbitrary-file-access").noArbitraryFileAccess},get"no-data-in-temp-storage"(){return require("./rules/no-data-in-temp-storage").noDataInTempStorage},get"no-ssrf"(){return require("./rules/no-ssrf").noSsrf},get"no-shell-injection"(){return require("./rules/no-shell-injection").noShellInjection},get"no-dynamic-command-string"(){return require("./rules/no-dynamic-command-string").noDynamicCommandString},get"no-env-injection"(){return require("./rules/no-env-injection").noEnvInjection},get"no-dynamic-algorithm-selection"(){return require("./rules/no-dynamic-algorithm-selection").noDynamicAlgorithmSelection},get"detect-suspicious-dependencies"(){return require("./rules/detect-suspicious-dependencies").detectSuspiciousDependencies},get"lock-file"(){return require("./rules/lock-file").lockFile},get"no-dynamic-dependency-loading"(){return require("./rules/no-dynamic-dependency-loading").noDynamicDependencyLoading},get"require-dependency-integrity"(){return require("./rules/require-dependency-integrity").requireDependencyIntegrity},get"require-secure-credential-storage"(){return require("./rules/require-secure-credential-storage").requireSecureCredentialStorage},get"require-secure-deletion"(){return require("./rules/require-secure-deletion").requireSecureDeletion},get"require-storage-encryption"(){return require("./rules/require-storage-encryption").requireStorageEncryption},get"no-dynamic-require"(){return require("./rules/no-dynamic-require").noDynamicRequire},get"no-cryptojs"(){return require("./rules/no-cryptojs").noCryptojs},get"no-cryptojs-weak-random"(){return require("./rules/no-cryptojs-weak-random").noCryptojsWeakRandom},get"no-deprecated-cipher-method"(){return require("./rules/no-deprecated-cipher-method").noDeprecatedCipherMethod},get"no-ecb-mode"(){return require("./rules/no-ecb-mode").noEcbMode},get"no-insecure-key-derivation"(){return require("./rules/no-insecure-key-derivation").noInsecureKeyDerivation},get"no-insecure-rsa-padding"(){return require("./rules/no-insecure-rsa-padding").noInsecureRsaPadding},get"no-math-random-crypto"(){return require("./rules/no-math-random-crypto").noMathRandomCrypto},get"no-self-signed-certs"(){return require("./rules/no-self-signed-certs").noSelfSignedCerts},get"no-sha1-hash"(){return require("./rules/no-sha1-hash").noSha1Hash},get"no-static-iv"(){return require("./rules/no-static-iv").noStaticIv},get"no-timing-unsafe-compare"(){return require("./rules/no-timing-unsafe-compare").noTimingUnsafeCompare},get"no-weak-cipher-algorithm"(){return require("./rules/no-weak-cipher-algorithm").noWeakCipherAlgorithm},get"no-weak-hash-algorithm"(){return require("./rules/no-weak-hash-algorithm").noWeakHashAlgorithm},get"prefer-native-crypto"(){return require("./rules/prefer-native-crypto").preferNativeCrypto},get"require-aead-tag-verification"(){return require("./rules/require-aead-tag-verification").requireAeadTagVerification},get"no-unbounded-decompression"(){return require("./rules/no-unbounded-decompression").noUnboundedDecompression},get"no-insecure-http-parser"(){return require("./rules/no-insecure-http-parser").noInsecureHttpParser},get"require-stream-error-handler"(){return require("./rules/require-stream-error-handler").requireStreamErrorHandler}};(0,eslint_devkit_1.withCanonicalDocsUrls)("plugin-node-security",exports.rules);exports.plugin={meta:{name:"eslint-plugin-node-security",version:"5.3.1"},rules:exports.rules};const recommendedRules={"node-security/detect-child-process":"error","node-security/detect-eval-with-expression":"error","node-security/detect-non-literal-fs-filename":"warn","node-security/no-unsafe-dynamic-require":"error","node-security/no-buffer-overread":"warn","node-security/no-deprecated-buffer":"error","node-security/no-unsafe-buffer-alloc":"warn","node-security/no-toctou-vulnerability":"error","node-security/no-zip-slip":"error","node-security/no-arbitrary-file-access":"error","node-security/no-data-in-temp-storage":"error","node-security/no-ssrf":"warn","node-security/no-shell-injection":"error","node-security/no-dynamic-command-string":"error","node-security/no-env-injection":"error","node-security/no-dynamic-algorithm-selection":"error","node-security/no-timing-unsafe-compare":"warn","node-security/detect-suspicious-dependencies":"warn","node-security/require-dependency-integrity":"error","node-security/no-weak-hash-algorithm":"error","node-security/no-weak-cipher-algorithm":"error","node-security/no-static-iv":"error","node-security/no-ecb-mode":"error","node-security/no-math-random-crypto":"error","node-security/no-cryptojs":"error","node-security/no-self-signed-certs":"error","node-security/require-aead-tag-verification":"error","node-security/no-unbounded-decompression":"error","node-security/no-insecure-http-parser":"error","node-security/require-stream-error-handler":"error"};exports.configs={recommended:{plugins:{"node-security":exports.plugin},rules:recommendedRules},strict:{plugins:{"node-security":exports.plugin},rules:Object.fromEntries(Object.keys(exports.rules).map(ruleName=>[`node-security/${ruleName}`,"error"]))}};exports.default=exports.plugin;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.detectEvalWithExpression=exports.generateRefactoringSteps=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");const const_value_1=require("../../utils/const-value");const EVAL_PATTERNS=[{pattern:"JSON\\.parse|parse\\(.*\\)",category:"json",safeAlternative:"JSON.parse()",example:{bad:`eval('{"key": "' + value + '"}"')`,good:`JSON.parse('{"key": "' + value + '"}"')`},effort:"2 minutes"},{pattern:"Math\\.|parseInt|parseFloat",category:"math",safeAlternative:"Math functions or parseInt/parseFloat",example:{bad:"eval('Math.' + method + '(' + arg + ')')",good:"const mathMethods = {sin: Math.sin, cos: Math.cos}; mathMethods[method](arg)"},effort:"5 minutes"},{pattern:"\\$\\{|template|interpolat",category:"template",safeAlternative:"Template literals or template engine",example:{bad:"eval('Hello ' + userName + '!')",good:"const template = `Hello ${userName}!`;"},effort:"3 minutes"},{pattern:"\\[.*\\]|object\\[|obj\\.|\\.",category:"object",safeAlternative:"Direct property access or Map",example:{bad:"eval('obj.' + property)",good:"const allowedProps = {name: true, age: true}; if (allowedProps[property]) obj[property]"},effort:"8 minutes"}];const VM_CODE_SINK_METHODS=new Set(["runInNewContext","runInThisContext","runInContext","compileFunction"]);const VM_CODE_CONSTRUCTORS=new Set(["Script","SourceTextModule"]);const VM2_SANDBOX_CONSTRUCTORS=new Set(["VM","NodeVM"]);const VM2_CODE_CONSTRUCTORS=new Set(["VMScript"]);const VM_MODULES=new Set(["vm","node:vm"]);const VM2_MODULES=new Set(["vm2"]);const NAMESPACE="*";function requiredModule(node){if(!node||node.type!=="CallExpression")return null;if(node.callee.type!=="Identifier"||node.callee.name!=="require"){return null}const[source]=node.arguments;if(!source||source.type!=="Literal")return null;return typeof source.value==="string"?source.value:null}function resolveModuleMember(callee,bindings,modules,name){if(name===null)return null;if(callee.type==="Identifier"){const bound=bindings.get(name);return bound!==void 0&&bound!==NAMESPACE?bound:null}const{object}=callee;if(object.type==="Identifier"){return bindings.get(object.name)===NAMESPACE?name:null}const inline=requiredModule(object);return inline!==null&&modules.has(inline)?name:null}function calleeTrailingName(callee){if(callee.type==="Identifier")return callee.name;if(callee.type!=="MemberExpression"||callee.computed)return null;return callee.property.type==="Identifier"?callee.property.name:null}function isStaticStringNode(node){if(node.type==="Literal")return typeof node.value==="string";if(node.type==="TemplateLiteral")return node.expressions.length===0;return false}const generateRefactoringSteps=pattern=>{if(!pattern){return[" 1. Remove eval() usage entirely"," 2. Identify what the code is trying to achieve"," 3. Use appropriate safe alternative (JSON.parse, Map, etc.)"," 4. Add input validation if dynamic behavior needed"," 5. Test thoroughly for edge cases"].join("\n")}switch(pattern.category){case"json":return[" 1. Replace eval() with JSON.parse()"," 2. Ensure input is valid JSON string"," 3. Add try/catch for JSON parsing errors"," 4. Consider using a JSON schema validator"].join("\n");case"math":return[" 1. Create whitelist of allowed Math functions"," 2. Use direct function calls: Math.sin(x)"," 3. Validate inputs are numbers"," 4. Consider using a math expression parser library"].join("\n");case"template":return[" 1. Use template literals: `Hello ${name}`"," 2. Sanitize variables before interpolation"," 3. Use a template engine like Handlebars if complex"," 4. Validate template structure"].join("\n");case"object":return[" 1. Use Map or plain object for key-value access"," 2. Whitelist allowed property names"," 3. Use hasOwnProperty() check"," 4. Consider Object.create(null) for clean objects"].join("\n");default:return[" 1. Identify the specific use case"," 2. Find a safer alternative approach"," 3. Add comprehensive input validation"," 4. Use static analysis if possible"].join("\n")}};exports.generateRefactoringSteps=generateRefactoringSteps;exports.detectEvalWithExpression=(0,eslint_devkit_2.createRule)({name:"detect-eval-with-expression",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/detect-eval-with-expression.md",description:"Detects strings turned into running code \u2014 eval(variable), the Function constructor, and the vm / vm2 sinks that are mistaken for sandboxes",cwe:"CWE-95",cvss:9.8,confidence:"high"},hasSuggestions:false,messages:{vmCodeExecution:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Code Execution Through the vm Module (CWE-94)",cwe:"CWE-94",cvss:9.8,description:'vm.{{api}}() compiles and runs its first argument as JavaScript, and that argument is not written out in full here. The vm module is NOT a security boundary \u2014 Node documents it as such \u2014 because any object reachable from the context carries a constructor chain back out: `this.constructor.constructor("return process")()`. A string that is not a constant is a string an attacker may be able to steer.',severity:"CRITICAL",fix:"Do not evaluate the value as code. Parse it (JSON.parse, an expression parser) or dispatch through a fixed map of allowed operations; if untrusted code genuinely has to run, isolate it in a separate process with its own privileges \u2014 not in vm.",documentationLink:"https://nodejs.org/api/vm.html#vm-executing-javascript"}),vm2CodeExecution:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Code Execution Through vm2 (CWE-94)",cwe:"CWE-94",cvss:9.8,description:"vm2 is abandoned and was retired by its maintainer after sandbox escapes that it could not fix (CVE-2023-37903, CVE-2023-37466). Running source that is not a constant inside it is arbitrary code execution on the host, not sandboxed execution.",severity:"CRITICAL",fix:"Stop using vm2. Run untrusted code out-of-process under an OS-level boundary (a separate process with dropped privileges, a container, or isolated-vm), or remove the need to execute caller-supplied source.",documentationLink:"https://github.com/patriksimek/vm2/issues/533"}),useJsonParse:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe eval() for JSON parsing",cwe:"CWE-95",description:"Use JSON.parse() instead of eval() for JSON string parsing",severity:"HIGH",fix:"Replace eval() with JSON.parse()",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),useObjectAccess:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe eval() for property access",cwe:"CWE-95",description:"Use direct property access instead of eval() for dynamic property access",severity:"HIGH",fix:"Use obj[key] or Map.get(key) instead of eval()",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),useTemplateLiteral:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe eval() for string interpolation",cwe:"CWE-95",description:"Use template literals instead of eval() for string interpolation",severity:"HIGH",fix:"Replace eval() with template literals: `Hello ${name}`",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),strategyRemove:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Critical eval() security vulnerability",cwe:"CWE-95",description:"eval() usage poses severe security risk",severity:"CRITICAL",fix:"Remove eval() entirely - security risk too high",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),strategyRefactor:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"eval() refactoring required",cwe:"CWE-95",description:"eval() can be refactored to safer alternative",severity:"HIGH",fix:"{{safeAlternative}}",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),strategyValidate:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"eval() input validation needed",cwe:"CWE-95",description:"eval() requires input validation for security",severity:"MEDIUM",fix:"Add input validation before using eval()",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"})},schema:[{type:"object",properties:{allowLiteralStrings:{type:"boolean",default:false,description:"Allow eval with literal strings (false = stricter)"},additionalEvalFunctions:{type:"array",items:{type:"string"},default:[],description:"Additional functions to treat as eval-like"},strategy:{type:"string",enum:["remove","refactor","validate","auto"],default:"auto",description:"Strategy for fixing eval usage (auto = smart detection)"}},additionalProperties:false}]},defaultOptions:[{allowLiteralStrings:false,additionalEvalFunctions:[],strategy:"auto"}],create(context){const options=context.options[0]||{};const{allowLiteralStrings=false,additionalEvalFunctions=[],strategy="auto"}=options;const evalFunctions=new Set(["eval","Function",...additionalEvalFunctions]);const isLiteralString=node=>{return node.type==="Literal"&&typeof node.value==="string"};const selectStrategyMessage=pattern=>{switch(strategy){case"remove":return"strategyRemove";case"refactor":return"strategyRefactor";case"validate":return"strategyValidate";case"auto":default:if(pattern&&pattern.category==="json"){return"useJsonParse"}if(pattern&&pattern.category==="object"){return"useObjectAccess"}if(pattern&&pattern.category==="template"){return"useTemplateLiteral"}return"strategyRefactor"}};const detectPattern=expression=>{for(const pattern of EVAL_PATTERNS){if(new RegExp(pattern.pattern,"i").test(expression)){return pattern}}return null};const extractExpression=node=>{const sourceCode=context.sourceCode;if(node.arguments.length>0){return sourceCode.getText(node.arguments[0])}return"dynamic expression"};const GLOBAL_OBJECTS=new Set(["globalThis","global","window","self"]);const isGlobalBinding=identifier=>{const variable=findVariable(identifier);return!variable||variable.defs.length===0};const evalCalleeName=(callee,depth=0)=>{if(depth>3)return null;if(callee.type==="Identifier"){if(evalFunctions.has(callee.name)){if(isGlobalBinding(callee))return callee.name;const aliased=(0,const_value_1.constInitializerOf)(context.sourceCode,callee);return aliased?evalCalleeName(aliased,depth+1):null}const init=(0,const_value_1.constInitializerOf)(context.sourceCode,callee);return init?evalCalleeName(init,depth+1):null}if(callee.type==="SequenceExpression"){const[last]=callee.expressions.slice(-1);return evalCalleeName(last,depth+1)}if(callee.type==="MemberExpression"&&!callee.computed&&callee.object.type==="Identifier"&&GLOBAL_OBJECTS.has(callee.object.name)&&callee.property.type==="Identifier"&&evalFunctions.has(callee.property.name)){return callee.property.name}return null};const checkCallExpression=node=>{const evalName=evalCalleeName(node.callee);if(evalName!==null){if(allowLiteralStrings&&node.arguments.length>0&&isLiteralString(node.arguments[0])){return}if(node.arguments.length>0&&evalName==="eval"&&isLiteralString(node.arguments[0])){return}const expression=extractExpression(node);const pattern=detectPattern(expression);const steps=(0,exports.generateRefactoringSteps)(pattern);context.report({node,messageId:selectStrategyMessage(pattern),data:{expression,patternCategory:pattern?.category||"dynamic code execution",safeAlternative:pattern?.safeAlternative||"Remove eval entirely",steps,effort:pattern?.effort||"15-30 minutes"}})}};const checkNewExpression=node=>{if(node.callee.type==="Identifier"&&node.callee.name==="Function"&&isGlobalBinding(node.callee)){const sourceCode=context.sourceCode;const expression=node.arguments.map(arg=>sourceCode.getText(arg)).join(", ");const pattern=detectPattern(expression);context.report({node,messageId:selectStrategyMessage(pattern),data:{expression:`new Function(${expression})`,patternCategory:"function constructor",safeAlternative:"Arrow function or regular function",steps:[" 1. Replace Function constructor with arrow function"," 2. Use regular function declaration"," 3. Validate any dynamic parts"," 4. Consider module imports instead"].join("\n"),effort:"10 minutes"}})}};const vmBindings=new Map;const vm2Bindings=new Map;const vm2SandboxCandidates=[];const pendingVmCalls=[];const pendingVmNews=[];const findVariable=node=>{let scope=context.sourceCode.getScope(node);while(scope){const found=scope.variables.find(v=>v.name===node.name);if(found)return found;scope=scope.upper}return null};const isStaticCode=node=>{if(!node)return true;if(isStaticStringNode(node))return true;if(node.type!=="Identifier")return false;const variable=findVariable(node);if(!variable)return false;const lastWrite=variable.references.filter(ref=>ref.isWrite()).map(ref=>ref.writeExpr).filter(write=>write!=null).filter(write=>write.range[1]<=node.range[0]).sort((a,b)=>a.range[1]-b.range[1]).at(-1);return lastWrite!==void 0&&isStaticStringNode(lastWrite)};const bindModuleName=(moduleName,local,imported)=>{if(VM_MODULES.has(moduleName))vmBindings.set(local,imported);else if(VM2_MODULES.has(moduleName))vm2Bindings.set(local,imported)};const bindRequire=node=>{const moduleName=requiredModule(node.init);if(moduleName===null)return;if(node.id.type==="Identifier"){bindModuleName(moduleName,node.id.name,NAMESPACE);return}if(node.id.type!=="ObjectPattern")return;for(const property of node.id.properties){if(property.type!=="Property")continue;if(property.key.type!=="Identifier")continue;if(property.value.type!=="Identifier")continue;bindModuleName(moduleName,property.value.name,property.key.name)}};const reportVmSite=(node,code,messageId,api)=>{if(isStaticCode(code))return;context.report({node,messageId,data:{api}})};const trailingName=callee=>{if(callee.type==="MemberExpression"&&callee.computed){return(0,const_value_1.resolveConstantString)(context.sourceCode,callee.property)?.value??null}return calleeTrailingName(callee)};const isVm2Run=(node,sandboxes)=>{const{callee}=node;if(callee.type!=="MemberExpression")return false;if(trailingName(callee)!=="run")return false;if(callee.object.type==="Identifier"){return sandboxes.has(callee.object.name)}if(callee.object.type!=="NewExpression")return false;const constructed=resolveModuleMember(callee.object.callee,vm2Bindings,VM2_MODULES,trailingName(callee.object.callee));return constructed!==null&&VM2_SANDBOX_CONSTRUCTORS.has(constructed)};const judgeVmSites=()=>{const sandboxes=new Set;for(const candidate of vm2SandboxCandidates){const constructed=resolveModuleMember(candidate.init.callee,vm2Bindings,VM2_MODULES,trailingName(candidate.init.callee));if(constructed!==null&&VM2_SANDBOX_CONSTRUCTORS.has(constructed)){sandboxes.add(candidate.local)}}for(const node of pendingVmCalls){const name=trailingName(node.callee);const vmApi=resolveModuleMember(node.callee,vmBindings,VM_MODULES,name);if(vmApi!==null&&VM_CODE_SINK_METHODS.has(vmApi)){reportVmSite(node,node.arguments[0],"vmCodeExecution",vmApi);continue}if(isVm2Run(node,sandboxes)){reportVmSite(node,node.arguments[0],"vm2CodeExecution","run")}}for(const node of pendingVmNews){const name=trailingName(node.callee);const vmCtor=resolveModuleMember(node.callee,vmBindings,VM_MODULES,name);if(vmCtor!==null&&VM_CODE_CONSTRUCTORS.has(vmCtor)){reportVmSite(node,node.arguments[0],"vmCodeExecution",vmCtor);continue}const vm2Ctor=resolveModuleMember(node.callee,vm2Bindings,VM2_MODULES,name);if(vm2Ctor!==null&&VM2_CODE_CONSTRUCTORS.has(vm2Ctor)){reportVmSite(node,node.arguments[0],"vm2CodeExecution",vm2Ctor)}}};return{CallExpression(node){checkCallExpression(node);if(node.callee.type==="Identifier"){pendingVmCalls.push(node);return}const name=trailingName(node.callee);if(name!==null&&(VM_CODE_SINK_METHODS.has(name)||name==="run")){pendingVmCalls.push(node)}},NewExpression(node){checkNewExpression(node);if(node.callee.type==="Identifier"){pendingVmNews.push(node);return}const name=trailingName(node.callee);if(name!==null&&(VM_CODE_CONSTRUCTORS.has(name)||VM2_CODE_CONSTRUCTORS.has(name))){pendingVmNews.push(node)}},ImportDeclaration(node){const moduleName=node.source.value;for(const specifier of node.specifiers){if(specifier.type==="ImportSpecifier"){if(specifier.imported.type!=="Identifier")continue;bindModuleName(moduleName,specifier.local.name,specifier.imported.name);continue}bindModuleName(moduleName,specifier.local.name,NAMESPACE)}},VariableDeclarator(node){bindRequire(node);if(node.id.type==="Identifier"&&node.init?.type==="NewExpression"){vm2SandboxCandidates.push({local:node.id.name,init:node.init})}},"Program:exit":judgeVmSites}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.detectEvalWithExpression=exports.generateRefactoringSteps=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");const const_value_1=require("../../utils/const-value");const EVAL_PATTERNS=[{pattern:"JSON\\.parse|parse\\(.*\\)",category:"json",safeAlternative:"JSON.parse()",example:{bad:`eval('{"key": "' + value + '"}"')`,good:`JSON.parse('{"key": "' + value + '"}"')`},effort:"2 minutes"},{pattern:"Math\\.|parseInt|parseFloat",category:"math",safeAlternative:"Math functions or parseInt/parseFloat",example:{bad:"eval('Math.' + method + '(' + arg + ')')",good:"const mathMethods = {sin: Math.sin, cos: Math.cos}; mathMethods[method](arg)"},effort:"5 minutes"},{pattern:"\\$\\{|template|interpolat",category:"template",safeAlternative:"Template literals or template engine",example:{bad:"eval('Hello ' + userName + '!')",good:"const template = `Hello ${userName}!`;"},effort:"3 minutes"},{pattern:"\\[.*\\]|object\\[|obj\\.|\\.",category:"object",safeAlternative:"Direct property access or Map",example:{bad:"eval('obj.' + property)",good:"const allowedProps = {name: true, age: true}; if (allowedProps[property]) obj[property]"},effort:"8 minutes"}];const VM_CODE_SINK_METHODS=new Set(["runInNewContext","runInThisContext","runInContext","compileFunction"]);const VM_CODE_CONSTRUCTORS=new Set(["Script","SourceTextModule"]);const VM2_SANDBOX_CONSTRUCTORS=new Set(["VM","NodeVM"]);const VM2_CODE_CONSTRUCTORS=new Set(["VMScript"]);const VM_MODULES=new Set(["vm","node:vm"]);const VM2_MODULES=new Set(["vm2"]);const NAMESPACE="*";function requiredModule(node){if(!node||node.type!=="CallExpression")return null;if(node.callee.type!=="Identifier"||node.callee.name!=="require"){return null}const[source]=node.arguments;if(!source||source.type!=="Literal")return null;return typeof source.value==="string"?source.value:null}function resolveModuleMember(callee,bindings,modules,name){if(name===null)return null;if(callee.type==="Identifier"){const bound=bindings.get(name);return bound!==void 0&&bound!==NAMESPACE?bound:null}const{object}=callee;if(object.type==="Identifier"){return bindings.get(object.name)===NAMESPACE?name:null}const inline=requiredModule(object);return inline!==null&&modules.has(inline)?name:null}function calleeTrailingName(callee){if(callee.type==="Identifier")return callee.name;if(callee.type!=="MemberExpression"||callee.computed)return null;return callee.property.type==="Identifier"?callee.property.name:null}function isStaticStringNode(node){if(node.type==="Literal")return(0,eslint_devkit_1.staticString)(node)!==null;if(node.type==="TemplateLiteral")return node.expressions.length===0;return false}const generateRefactoringSteps=pattern=>{if(!pattern){return[" 1. Remove eval() usage entirely"," 2. Identify what the code is trying to achieve"," 3. Use appropriate safe alternative (JSON.parse, Map, etc.)"," 4. Add input validation if dynamic behavior needed"," 5. Test thoroughly for edge cases"].join("\n")}switch(pattern.category){case"json":return[" 1. Replace eval() with JSON.parse()"," 2. Ensure input is valid JSON string"," 3. Add try/catch for JSON parsing errors"," 4. Consider using a JSON schema validator"].join("\n");case"math":return[" 1. Create whitelist of allowed Math functions"," 2. Use direct function calls: Math.sin(x)"," 3. Validate inputs are numbers"," 4. Consider using a math expression parser library"].join("\n");case"template":return[" 1. Use template literals: `Hello ${name}`"," 2. Sanitize variables before interpolation"," 3. Use a template engine like Handlebars if complex"," 4. Validate template structure"].join("\n");case"object":return[" 1. Use Map or plain object for key-value access"," 2. Whitelist allowed property names"," 3. Use hasOwnProperty() check"," 4. Consider Object.create(null) for clean objects"].join("\n");default:return[" 1. Identify the specific use case"," 2. Find a safer alternative approach"," 3. Add comprehensive input validation"," 4. Use static analysis if possible"].join("\n")}};exports.generateRefactoringSteps=generateRefactoringSteps;exports.detectEvalWithExpression=(0,eslint_devkit_2.createRule)({name:"detect-eval-with-expression",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/detect-eval-with-expression.md",description:"Detects strings turned into running code \u2014 eval(variable), the Function constructor, and the vm / vm2 sinks that are mistaken for sandboxes",cwe:"CWE-95",cvss:9.8,confidence:"high"},hasSuggestions:false,messages:{vmCodeExecution:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Code Execution Through the vm Module (CWE-94)",cwe:"CWE-94",cvss:9.8,description:'vm.{{api}}() compiles and runs its first argument as JavaScript, and that argument is not written out in full here. The vm module is NOT a security boundary \u2014 Node documents it as such \u2014 because any object reachable from the context carries a constructor chain back out: `this.constructor.constructor("return process")()`. A string that is not a constant is a string an attacker may be able to steer.',severity:"CRITICAL",fix:"Do not evaluate the value as code. Parse it (JSON.parse, an expression parser) or dispatch through a fixed map of allowed operations; if untrusted code genuinely has to run, isolate it in a separate process with its own privileges \u2014 not in vm.",documentationLink:"https://nodejs.org/api/vm.html#vm-executing-javascript"}),vm2CodeExecution:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Code Execution Through vm2 (CWE-94)",cwe:"CWE-94",cvss:9.8,description:"vm2 is abandoned and was retired by its maintainer after sandbox escapes that it could not fix (CVE-2023-37903, CVE-2023-37466). Running source that is not a constant inside it is arbitrary code execution on the host, not sandboxed execution.",severity:"CRITICAL",fix:"Stop using vm2. Run untrusted code out-of-process under an OS-level boundary (a separate process with dropped privileges, a container, or isolated-vm), or remove the need to execute caller-supplied source.",documentationLink:"https://github.com/patriksimek/vm2/issues/533"}),useJsonParse:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe eval() for JSON parsing",cwe:"CWE-95",description:"Use JSON.parse() instead of eval() for JSON string parsing",severity:"HIGH",fix:"Replace eval() with JSON.parse()",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),useObjectAccess:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe eval() for property access",cwe:"CWE-95",description:"Use direct property access instead of eval() for dynamic property access",severity:"HIGH",fix:"Use obj[key] or Map.get(key) instead of eval()",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),useTemplateLiteral:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe eval() for string interpolation",cwe:"CWE-95",description:"Use template literals instead of eval() for string interpolation",severity:"HIGH",fix:"Replace eval() with template literals: `Hello ${name}`",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),strategyRemove:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Critical eval() security vulnerability",cwe:"CWE-95",description:"eval() usage poses severe security risk",severity:"CRITICAL",fix:"Remove eval() entirely - security risk too high",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),strategyRefactor:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"eval() refactoring required",cwe:"CWE-95",description:"eval() can be refactored to safer alternative",severity:"HIGH",fix:"{{safeAlternative}}",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),strategyValidate:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"eval() input validation needed",cwe:"CWE-95",description:"eval() requires input validation for security",severity:"MEDIUM",fix:"Add input validation before using eval()",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"})},schema:[{type:"object",properties:{allowLiteralStrings:{type:"boolean",default:false,description:"Allow eval with literal strings (false = stricter)"},additionalEvalFunctions:{type:"array",items:{type:"string"},default:[],description:"Additional functions to treat as eval-like"},strategy:{type:"string",enum:["remove","refactor","validate","auto"],default:"auto",description:"Strategy for fixing eval usage (auto = smart detection)"}},additionalProperties:false}]},defaultOptions:[{allowLiteralStrings:false,additionalEvalFunctions:[],strategy:"auto"}],create(context){const options=context.options[0]||{};const{allowLiteralStrings=false,additionalEvalFunctions=[],strategy="auto"}=options;const evalFunctions=new Set(["eval","Function",...additionalEvalFunctions]);const isLiteralString=node=>{return(0,eslint_devkit_1.staticString)(node)!==null};const selectStrategyMessage=pattern=>{switch(strategy){case"remove":return"strategyRemove";case"refactor":return"strategyRefactor";case"validate":return"strategyValidate";case"auto":default:if(pattern&&pattern.category==="json"){return"useJsonParse"}if(pattern&&pattern.category==="object"){return"useObjectAccess"}if(pattern&&pattern.category==="template"){return"useTemplateLiteral"}return"strategyRefactor"}};const detectPattern=expression=>{for(const pattern of EVAL_PATTERNS){if(new RegExp(pattern.pattern,"i").test(expression)){return pattern}}return null};const extractExpression=node=>{const sourceCode=context.sourceCode;if(node.arguments.length>0){return sourceCode.getText(node.arguments[0])}return"dynamic expression"};const GLOBAL_OBJECTS=new Set(["globalThis","global","window","self"]);const isGlobalBinding=identifier=>{const variable=findVariable(identifier);return!variable||variable.defs.length===0};const evalCalleeName=(callee,depth=0)=>{if(depth>3)return null;if(callee.type==="Identifier"){if(evalFunctions.has(callee.name)){if(isGlobalBinding(callee))return callee.name;const aliased=(0,const_value_1.constInitializerOf)(context.sourceCode,callee);return aliased?evalCalleeName(aliased,depth+1):null}const init=(0,const_value_1.constInitializerOf)(context.sourceCode,callee);return init?evalCalleeName(init,depth+1):null}if(callee.type==="SequenceExpression"){const[last]=callee.expressions.slice(-1);return evalCalleeName(last,depth+1)}if(callee.type==="MemberExpression"&&!callee.computed&&callee.object.type==="Identifier"&&GLOBAL_OBJECTS.has(callee.object.name)&&callee.property.type==="Identifier"&&evalFunctions.has(callee.property.name)){return callee.property.name}return null};const checkCallExpression=node=>{const evalName=evalCalleeName(node.callee);if(evalName!==null){if(allowLiteralStrings&&node.arguments.length>0&&isLiteralString(node.arguments[0])){return}if(node.arguments.length>0&&evalName==="eval"&&isLiteralString(node.arguments[0])){return}const expression=extractExpression(node);const pattern=detectPattern(expression);const steps=(0,exports.generateRefactoringSteps)(pattern);context.report({node,messageId:selectStrategyMessage(pattern),data:{expression,patternCategory:pattern?.category||"dynamic code execution",safeAlternative:pattern?.safeAlternative||"Remove eval entirely",steps,effort:pattern?.effort||"15-30 minutes"}})}};const checkNewExpression=node=>{if(node.callee.type==="Identifier"&&node.callee.name==="Function"&&isGlobalBinding(node.callee)){const sourceCode=context.sourceCode;const expression=node.arguments.map(arg=>sourceCode.getText(arg)).join(", ");const pattern=detectPattern(expression);context.report({node,messageId:selectStrategyMessage(pattern),data:{expression:`new Function(${expression})`,patternCategory:"function constructor",safeAlternative:"Arrow function or regular function",steps:[" 1. Replace Function constructor with arrow function"," 2. Use regular function declaration"," 3. Validate any dynamic parts"," 4. Consider module imports instead"].join("\n"),effort:"10 minutes"}})}};const vmBindings=new Map;const vm2Bindings=new Map;const vm2SandboxCandidates=[];const pendingVmCalls=[];const pendingVmNews=[];const findVariable=node=>{let scope=context.sourceCode.getScope(node);while(scope){const found=scope.variables.find(v=>v.name===node.name);if(found)return found;scope=scope.upper}return null};const isStaticCode=node=>{if(!node)return true;if(isStaticStringNode(node))return true;if(node.type!=="Identifier")return false;const variable=findVariable(node);if(!variable)return false;const lastWrite=variable.references.filter(ref=>ref.isWrite()).map(ref=>ref.writeExpr).filter(write=>write!=null).filter(write=>write.range[1]<=node.range[0]).sort((a,b)=>a.range[1]-b.range[1]).at(-1);return lastWrite!==void 0&&isStaticStringNode(lastWrite)};const bindModuleName=(moduleName,local,imported)=>{if(VM_MODULES.has(moduleName))vmBindings.set(local,imported);else if(VM2_MODULES.has(moduleName))vm2Bindings.set(local,imported)};const bindRequire=node=>{const moduleName=requiredModule(node.init);if(moduleName===null)return;if(node.id.type==="Identifier"){bindModuleName(moduleName,node.id.name,NAMESPACE);return}if(node.id.type!=="ObjectPattern")return;for(const property of node.id.properties){if(property.type!=="Property")continue;if(property.key.type!=="Identifier")continue;if(property.value.type!=="Identifier")continue;bindModuleName(moduleName,property.value.name,property.key.name)}};const reportVmSite=(node,code,messageId,api)=>{if(isStaticCode(code))return;context.report({node,messageId,data:{api}})};const trailingName=callee=>{if(callee.type==="MemberExpression"&&callee.computed){return(0,const_value_1.resolveConstantString)(context.sourceCode,callee.property)?.value??null}return calleeTrailingName(callee)};const isVm2Run=(node,sandboxes)=>{const{callee}=node;if(callee.type!=="MemberExpression")return false;if(trailingName(callee)!=="run")return false;if(callee.object.type==="Identifier"){return sandboxes.has(callee.object.name)}if(callee.object.type!=="NewExpression")return false;const constructed=resolveModuleMember(callee.object.callee,vm2Bindings,VM2_MODULES,trailingName(callee.object.callee));return constructed!==null&&VM2_SANDBOX_CONSTRUCTORS.has(constructed)};const judgeVmSites=()=>{const sandboxes=new Set;for(const candidate of vm2SandboxCandidates){const constructed=resolveModuleMember(candidate.init.callee,vm2Bindings,VM2_MODULES,trailingName(candidate.init.callee));if(constructed!==null&&VM2_SANDBOX_CONSTRUCTORS.has(constructed)){sandboxes.add(candidate.local)}}for(const node of pendingVmCalls){const name=trailingName(node.callee);const vmApi=resolveModuleMember(node.callee,vmBindings,VM_MODULES,name);if(vmApi!==null&&VM_CODE_SINK_METHODS.has(vmApi)){reportVmSite(node,node.arguments[0],"vmCodeExecution",vmApi);continue}if(isVm2Run(node,sandboxes)){reportVmSite(node,node.arguments[0],"vm2CodeExecution","run")}}for(const node of pendingVmNews){const name=trailingName(node.callee);const vmCtor=resolveModuleMember(node.callee,vmBindings,VM_MODULES,name);if(vmCtor!==null&&VM_CODE_CONSTRUCTORS.has(vmCtor)){reportVmSite(node,node.arguments[0],"vmCodeExecution",vmCtor);continue}const vm2Ctor=resolveModuleMember(node.callee,vm2Bindings,VM2_MODULES,name);if(vm2Ctor!==null&&VM2_CODE_CONSTRUCTORS.has(vm2Ctor)){reportVmSite(node,node.arguments[0],"vm2CodeExecution",vm2Ctor)}}};return{CallExpression(node){checkCallExpression(node);if(node.callee.type==="Identifier"){pendingVmCalls.push(node);return}const name=trailingName(node.callee);if(name!==null&&(VM_CODE_SINK_METHODS.has(name)||name==="run")){pendingVmCalls.push(node)}},NewExpression(node){checkNewExpression(node);if(node.callee.type==="Identifier"){pendingVmNews.push(node);return}const name=trailingName(node.callee);if(name!==null&&(VM_CODE_CONSTRUCTORS.has(name)||VM2_CODE_CONSTRUCTORS.has(name))){pendingVmNews.push(node)}},ImportDeclaration(node){const moduleName=node.source.value;for(const specifier of node.specifiers){if(specifier.type==="ImportSpecifier"){if(specifier.imported.type!=="Identifier")continue;bindModuleName(moduleName,specifier.local.name,specifier.imported.name);continue}bindModuleName(moduleName,specifier.local.name,NAMESPACE)}},VariableDeclarator(node){bindRequire(node);if(node.id.type==="Identifier"&&node.init?.type==="NewExpression"){vm2SandboxCandidates.push({local:node.id.name,init:node.init})}},"Program:exit":judgeVmSites}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.detectNonLiteralFsFilename=exports.determineRiskLevel=exports.isFsModule=exports.generateRefactoringSteps=void 0;exports.fsMethodName=fsMethodName;exports.isFsRequire=isFsRequire;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");const FS_OPERATIONS=[{method:"readFile",dangerous:true,vulnerability:"file-access",safePattern:"path.resolve(SAFE_DIR, path.basename(userInput))",example:{bad:"fs.readFile(userPath, callback)",good:"const safePath = path.join(SAFE_UPLOADS_DIR, path.basename(userPath)); fs.readFile(safePath, callback)"},effort:"10-15 minutes"},{method:"writeFile",dangerous:true,vulnerability:"file-access",safePattern:"path.resolve(SAFE_DIR, path.basename(userInput))",example:{bad:"fs.writeFile(userPath, data, callback)",good:"const safePath = path.join(SAFE_WRITES_DIR, path.basename(userPath)); fs.writeFile(safePath, data, callback)"},effort:"10-15 minutes"},{method:"stat",dangerous:true,vulnerability:"path-traversal",safePattern:"path.resolve(baseDir, userInput) with validation",example:{bad:"fs.stat(userPath, callback)",good:"const resolvedPath = path.resolve(SAFE_DIR, userPath);\nif (!resolvedPath.startsWith(SAFE_DIR + path.sep)) return;\nfs.stat(resolvedPath, callback)"},effort:"15-20 minutes"},{method:"readdir",dangerous:true,vulnerability:"directory-traversal",safePattern:"Validate directory is within allowed paths",example:{bad:"fs.readdir(userDir, callback)",good:"const resolvedDir = path.resolve(ALLOWED_DIRS, userDir);\nif (!resolvedDir.startsWith(ALLOWED_DIRS + path.sep)) return;\nfs.readdir(resolvedDir, callback)"},effort:"15-20 minutes"}];const hasTraversalPatterns=pathStr=>{return/\.\.[/\\]/.test(pathStr)||/^\.\.[/\\]/.test(pathStr)};const PROCESS_INPUT_MEMBERS=new Set(["env","argv","argv0","execArgv","stdin"]);const SENSITIVE_SEGMENTS=["etc/passwd","etc/shadow","etc/hosts","etc/sudoers","proc/self",".ssh/id_rsa",".ssh/id_dsa",".ssh/authorized_keys",".aws/credentials",".npmrc",".git/config","windows/system32/config/sam"];const targetsSensitiveLocation=pathStr=>{const unquoted=pathStr.replace(/^['"`]|['"`]$/g,"");const normalised=unquoted.replace(/\\/g,"/").toLowerCase();if(!/(^|\/)\.\.(\/|$)/.test(normalised)){return false}const withoutPrefix=normalised.replace(/^(?:\.{1,2}\/)+/,"");return SENSITIVE_SEGMENTS.some(seg=>withoutPrefix===seg||withoutPrefix.endsWith(`/${seg}`)||withoutPrefix.startsWith(`${seg}/`))};const generateRefactoringSteps=operation=>{switch(operation.method){case"readFile":case"writeFile":return[" 1. Define a SAFE_DIR constant for allowed operations"," 2. Use path.basename() to strip directory components"," 3. Combine with SAFE_DIR: path.join(SAFE_DIR, path.basename(userPath))"," 4. Optionally validate file extensions"," 5. Add error handling for invalid paths"].join("\n");case"stat":return[" 1. Use path.resolve() to normalize the path"," 2. Check the resolved path starts with the base PLUS path.sep \u2014 a bare"," prefix lets /safebad through a /safe check"," 3. Reject requests that escape the allowed directory"," 4. Use path.relative() for additional validation"," 5. Log security events for monitoring"].join("\n");case"readdir":return[" 1. Resolve the directory path: path.resolve(ALLOWED_DIRS, userDir)"," 2. Validate resolved path starts with ALLOWED_DIRS"," 3. Check directory exists and is readable"," 4. Consider whitelisting allowed directories"," 5. Add rate limiting to prevent enumeration attacks"].join("\n");default:return[" 1. Identify the specific file operation needed"," 2. Define safe base directories for operations"," 3. Use path.resolve() and validate containment"," 4. Sanitize user input (basename, extension validation)"," 5. Add comprehensive error handling"].join("\n")}};exports.generateRefactoringSteps=generateRefactoringSteps;const FS_MODULE_EQUIVALENTS={"fs-extra":"fs","graceful-fs":"fs","fs/promises":"fs"};const FS_MODULES=new Set(["fs","node:fs","fs/promises","node:fs/promises","fs-extra","graceful-fs"]);const isFsModule=source=>typeof source==="string"&&FS_MODULES.has(source);exports.isFsModule=isFsModule;function fsMethodName(callee,namespaces,named){if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return named.get(callee.name);if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression||callee.computed||callee.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier){return void 0}const object=callee.object;if(object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&namespaces.has(object.name)){return callee.property.name}if(object.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!object.computed&&object.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&namespaces.has(object.object.name)&&object.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&object.property.name==="promises"){return callee.property.name}return void 0}function isFsRequire(node){return node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.name==="require"&&node.arguments.length>0&&node.arguments[0].type===eslint_devkit_1.AST_NODE_TYPES.Literal&&(0,exports.isFsModule)(node.arguments[0].value)}const determineRiskLevel=(operation,pathStr)=>{if(hasTraversalPatterns(pathStr)){return"CRITICAL"}if(operation.dangerous){return"HIGH"}return"MEDIUM"};exports.determineRiskLevel=determineRiskLevel;exports.detectNonLiteralFsFilename=(0,eslint_devkit_2.createRule)({name:"detect-non-literal-fs-filename",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/detect-non-literal-fs-filename.md",description:"Detects variable in filename argument of fs calls, which might allow an attacker to access anything on your system",cwe:"CWE-22",confidence:"medium"},messages:{fsPathTraversal:(0,eslint_devkit_1.formatLLMMessage)({icon:"\u{1F511}",issueName:"Path traversal",cwe:"CWE-22",description:"Path traversal vulnerability",severity:"{{riskLevel}}",fix:"{{safePattern}} \u2014 Not a finding when the value is path.basename()d, checked against an allowlist, or resolved and then prefix-checked WITH a trailing separator",documentationLink:"https://owasp.org/www-community/attacks/Path_Traversal"})},schema:[{type:"object",properties:{taintSources:{type:"array",items:{type:"string"},default:["process"],description:"Identifier roots treated as attacker-reachable. Default: ['process'] (process.argv and process.env). Request roots (req/request/ctx/event) are deliberately NOT included \u2014 no-arbitrary-file-access owns those, and listing them here would double-report one line at two severities. Add them only if you run this rule without that one."},reportUnresolvedPaths:{type:"boolean",default:false,description:"Report paths whose provenance cannot be resolved. Restores the pre-inversion behaviour; measured at 7% precision on real code."},allowLiterals:{type:"boolean",default:false,description:`Allow literal string paths. Default true: a rule named "non-literal" reporting a literal contradicts its contract. Set false to also flag hardcoded paths containing "../" \u2014 measured as this rule's largest FP class on real code.`},additionalMethods:{type:"array",items:{type:"string"},default:[],description:"Additional fs methods to check"}},additionalProperties:false}]},skipTestFiles:true,defaultOptions:[{allowLiterals:false,additionalMethods:[]}],create(context){const options=context.options[0]||{};const{allowLiterals=false,additionalMethods=[]}=options;const DEFAULT_TAINT_ROOTS=["process","req","request","ctx","context","event"];const WHOLE_VALUE_TRUSTED_ROOTS=new Set(["process"]);const AMBIGUOUS_ROOTS=new Set(["ctx","context"]);const REQUEST_SURFACE=new Set(["query","params","body","request","req","headers","cookies","searchParams","originalUrl"]);const resolvesInFile=id=>context.sourceCode.getScope(id).references.find(ref=>ref.identifier===id)?.resolved!=null;const readsRequestSurface=node=>node?.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!node.computed&&node.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&REQUEST_SURFACE.has(node.property.name);const hasRequestEvidence=id=>{if(readsRequestSurface(id.parent))return true;const variable=context.sourceCode.getScope(id).references.find(ref=>ref.identifier===id).resolved;return variable.references.some(ref=>readsRequestSurface(ref.identifier.parent))};const taintRoots=new Set(options.taintSources??DEFAULT_TAINT_ROOTS);const reportUnresolvedPaths=options.reportUnresolvedPaths??false;const PATH_ARGUMENT_INDICES=new Map([["copyFile",[0,1]],["copyFileSync",[0,1]],["cp",[0,1]],["cpSync",[0,1]],["rename",[0,1]],["renameSync",[0,1]],["link",[0,1]],["linkSync",[0,1]],["symlink",[0,1]],["symlinkSync",[0,1]]]);const dangerousMethods=new Set(["readFile","readFileSync","writeFile","writeFileSync","appendFile","appendFileSync","stat","statSync","lstat","lstatSync","readdir","readdirSync","unlink","unlinkSync","mkdir","mkdirSync","rmdir","rmdirSync","access","accessSync","createReadStream","createWriteStream","open","openSync","rm","rmSync","rename","renameSync","copyFile","copyFileSync","cp","cpSync","truncate","truncateSync","chmod","chmodSync","chown","chownSync","lchown","lchownSync","utimes","utimesSync","readlink","readlinkSync","symlink","symlinkSync","link","linkSync","opendir","opendirSync",...additionalMethods]);const isLiteralString=node=>{return node.type==="Literal"&&typeof node.value==="string"};const isLocallyConstructed=id=>{const variable=context.sourceCode.getScope(id).references.find(ref=>ref.identifier===id)?.resolved;if(!variable||variable.defs.length!==1)return false;const def=variable.defs[0];if(def.type!=="Variable")return false;if(variable.references.filter(ref=>ref.isWrite()).length>1){return false}const init=def.node.init;return init?.type===eslint_devkit_1.AST_NODE_TYPES.ObjectExpression||init?.type===eslint_devkit_1.AST_NODE_TYPES.ArrayExpression};const readsTaintSource=(node,depth=0)=>{if(depth>6)return false;switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Identifier:{if(taintRoots.has(node.name)){if(AMBIGUOUS_ROOTS.has(node.name)&&resolvesInFile(node)&&!hasRequestEvidence(node)){return false}return!isLocallyConstructed(node)}const bound=constBindings.get(node.name);return bound!==void 0&&readsTaintSource(bound,depth+1)}case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:{if(!node.computed&&node.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.object.name==="process"&&node.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&!PROCESS_INPUT_MEMBERS.has(node.property.name)){return false}return readsTaintSource(node.object,depth+1)}case eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral:return node.expressions.some(e=>readsTaintSource(e,depth+1));case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:return readsTaintSource(node.left,depth+1)||readsTaintSource(node.right,depth+1);case eslint_devkit_1.AST_NODE_TYPES.CallExpression:{const callee=node.callee;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!callee.computed&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="path"&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.property.name==="basename"){return false}return node.arguments.some(arg=>arg.type!==eslint_devkit_1.AST_NODE_TYPES.SpreadElement&&readsTaintSource(arg,depth+1))}default:return false}};const containsUntrustedRoot=(node,depth=0)=>{if(depth>6)return false;switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Identifier:{if(taintRoots.has(node.name))return!WHOLE_VALUE_TRUSTED_ROOTS.has(node.name);const bound=constBindings.get(node.name);return bound!==void 0&&containsUntrustedRoot(bound,depth+1)}case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:return containsUntrustedRoot(node.object,depth+1);case eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral:return node.expressions.some(e=>containsUntrustedRoot(e,depth+1));case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:return containsUntrustedRoot(node.left,depth+1)||containsUntrustedRoot(node.right,depth+1);case eslint_devkit_1.AST_NODE_TYPES.CallExpression:return node.arguments.some(a=>a.type!==eslint_devkit_1.AST_NODE_TYPES.SpreadElement&&containsUntrustedRoot(a,depth+1));default:return false}};const isWholeTaintValue=node=>{switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Identifier:{if(taintRoots.has(node.name))return WHOLE_VALUE_TRUSTED_ROOTS.has(node.name);const bound=constBindings.get(node.name);return bound!==void 0&&isWholeTaintValue(bound)}case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:return isWholeTaintValue(node.object);case eslint_devkit_1.AST_NODE_TYPES.CallExpression:{const callee=node.callee;const isPathCall=callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="path"||(0,eslint_devkit_1.resolveModuleBinding)(callee,context.sourceCode.getScope(callee),{})?.module==="path";if(!isPathCall)return false;if(node.arguments.length===1)return isWholeTaintValue(node.arguments[0]);return isWholeTaintValue(node.arguments[0])&&!containsUntrustedRoot(node)}default:return false}};const isDangerousPath=(pathNode,pathStr)=>{if(!pathNode)return reportUnresolvedPaths;if(isLiteralString(pathNode)){return!allowLiterals&&targetsSensitiveLocation(pathStr)}if(hasPathValidation(pathNode))return false;if(readsTaintSource(pathNode))return!isWholeTaintValue(pathNode);if(isBuildTimeConstant(pathNode))return false;if(isFreeVariable(pathNode))return true;if(containsFreeVariable(pathNode))return true;return reportUnresolvedPaths};function isFreeVariable(node){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const through=context.sourceCode.getScope(node).through;return through.some(ref=>ref.identifier===node&&ref.resolved===null)}const containsFreeVariable=(node,depth=0)=>{if(depth>4)return false;if(isBuildTimeConstant(node,depth))return false;if(isFreeVariable(node))return true;switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral:return node.expressions.some(e=>containsFreeVariable(e,depth+1));case eslint_devkit_1.AST_NODE_TYPES.CallExpression:return node.arguments.some(a=>a.type!==eslint_devkit_1.AST_NODE_TYPES.SpreadElement&&containsFreeVariable(a,depth+1));case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:return node.operator==="+"&&(containsFreeVariable(node.left,depth+1)||containsFreeVariable(node.right,depth+1));case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:return node.computed&&containsFreeVariable(node.property,depth+1);default:return false}};const isBuildTimeConstant=(node,depth=0)=>{if(depth>4)return false;if(isLiteralString(node)){return!hasTraversalPatterns(node.value)}if(node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){if(node.name==="__dirname"||node.name==="__filename")return true;const bound=constBindings.get(node.name);return bound!==void 0&&isBuildTimeConstant(bound,depth+1)}if(node.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral){const literalText=node.quasis.map(q=>q.value.raw).join("");if(hasTraversalPatterns(literalText))return false;return node.expressions.every(e=>isBuildTimeConstant(e,depth+1))}if(node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){const callee=node.callee;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="process"&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.property.name==="cwd"){return true}if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="path"&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&["join","resolve"].includes(callee.property.name)){return node.arguments.length>0&&node.arguments.every(arg=>isBuildTimeConstant(arg,depth+1))}return false}if(node.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression&&node.operator==="+"){return isBuildTimeConstant(node.left,depth+1)&&isBuildTimeConstant(node.right,depth+1)}return false};const isSeparatorAnchored=arg=>{if(arg===void 0)return false;const endsWithSep=n=>{if(n.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!n.computed&&n.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&n.object.name==="path"&&n.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&n.property.name==="sep"){return true}if(n.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof n.value==="string"){return n.value.endsWith("/")||n.value.endsWith("\\")}return false};if(endsWithSep(arg))return true;if(arg.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression&&arg.operator==="+"){return endsWithSep(arg.right)}if(arg.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral){const last=arg.quasis[arg.quasis.length-1].value.cooked;if(last.endsWith("/")||last.endsWith("\\"))return true;if(last===""){const tail=arg.expressions[arg.expressions.length-1];return tail!==void 0&&endsWithSep(tail)}return false}return false};const hasPathValidation=pathNode=>{if(pathNode.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier){const parts=[];const collect=n=>{if(n.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression&&n.operator==="+"){collect(n.left);collect(n.right);return}if(n.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral){n.expressions.forEach(e=>collect(e));return}if(n.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){n.arguments.forEach(a=>{if(a.type!==eslint_devkit_1.AST_NODE_TYPES.SpreadElement)collect(a)});return}if(readsTaintSource(n))parts.push(n)};collect(pathNode);return parts.length>0&&parts.every(p=>p.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&hasPathValidation(p))}const varName=pathNode.name;const isValidationCall=testNode=>{if(testNode.type===eslint_devkit_1.AST_NODE_TYPES.UnaryExpression&&testNode.operator==="!"&&testNode.argument.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){testNode=testNode.argument}if(testNode.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression){return false}if(testNode.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&testNode.callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&testNode.callee.object.name===varName&&testNode.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&(testNode.callee.property.name==="startsWith"||testNode.callee.property.name==="includes")){return testNode.callee.property.name==="includes"?true:isSeparatorAnchored(testNode.arguments[0])}if(testNode.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&testNode.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&testNode.callee.property.name==="includes"){for(const arg of testNode.arguments){if(arg.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&arg.name===varName){return true}}}if(testNode.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&testNode.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&testNode.callee.property.name==="test"){for(const arg of testNode.arguments){if(arg.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&arg.name===varName){return true}}}return false};const hasEarlyExit=consequent=>{if(consequent.type===eslint_devkit_1.AST_NODE_TYPES.BlockStatement){return consequent.body.some(stmt=>stmt.type===eslint_devkit_1.AST_NODE_TYPES.ThrowStatement||stmt.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement)}return consequent.type===eslint_devkit_1.AST_NODE_TYPES.ThrowStatement||consequent.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement};let current=pathNode.parent;let foundFunctionBody=false;while(current&&!foundFunctionBody){if(current.type===eslint_devkit_1.AST_NODE_TYPES.IfStatement){if(isValidationCall(current.test)){return true}}if(current.type===eslint_devkit_1.AST_NODE_TYPES.BlockStatement&&current.parent&&(current.parent.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration||current.parent.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression||current.parent.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression)){foundFunctionBody=true;const blockBody=current.body;const nodeIndex=blockBody.findIndex(stmt=>{let check=pathNode;while(check){if(check===stmt)return true;check=check.parent}return false});for(let i=0;i<nodeIndex;i++){const stmt=blockBody[i];if(stmt.type===eslint_devkit_1.AST_NODE_TYPES.IfStatement&&isValidationCall(stmt.test)&&hasEarlyExit(stmt.consequent)){return true}}}current=current.parent}return false};const fsNamespaces=new Set(["fs"]);const fsNamedMethods=new Map;const pendingCalls=[];const constBindings=new Map;function bindFsName(local,imported){if(imported==="promises")fsNamespaces.add(local);else fsNamedMethods.set(local,imported)}const checkFsCall=node=>{let methodName=fsMethodName(node.callee,fsNamespaces,fsNamedMethods);if(methodName===void 0){const binding=(0,eslint_devkit_1.resolveModuleBinding)(node.callee,context.sourceCode.getScope(node),{equivalents:FS_MODULE_EQUIVALENTS});if(binding?.module!=="fs")return;const[first,second]=binding.path;methodName=binding.path.length===1?first:binding.path.length===2&&first==="promises"?second:void 0;if(methodName===void 0)return}if(!dangerousMethods.has(methodName)){return}const method=methodName;const indices=PATH_ARGUMENT_INDICES.get(method)??[0];const sourceCode=context.sourceCode;let pathNode=null;let path="";for(const index of indices){const candidate=node.arguments[index];if(candidate===void 0||candidate.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement)continue;if(isDangerousPath(candidate,sourceCode.getText(candidate))){pathNode=candidate;path=sourceCode.getText(candidate);break}}if(pathNode===null){const present=indices.some(index=>node.arguments[index]!==void 0);if(!present&&isDangerousPath(null,"")){context.report({node,messageId:"fsPathTraversal",data:{method,path:"",riskLevel:"MEDIUM",vulnerability:"path traversal",safePattern:"Use path.resolve() with validation",steps:"Review file system access patterns"}})}return}const operation=FS_OPERATIONS.find(op=>op.method===method)??null;const riskLevel=(0,exports.determineRiskLevel)(operation||FS_OPERATIONS[0],path);const steps=operation?(0,exports.generateRefactoringSteps)(operation):"Review file system access patterns";const safePattern=operation?.safePattern||"Use path.resolve() with validation";context.report({node,messageId:"fsPathTraversal",data:{method,path,riskLevel,vulnerability:operation?.vulnerability||"path traversal",safePattern,steps,effort:operation?.effort||"15-20 minutes"}})};return{ImportDeclaration(node){if(!(0,exports.isFsModule)(node.source.value))return;for(const spec of node.specifiers){if(spec.type===eslint_devkit_1.AST_NODE_TYPES.ImportSpecifier){const imported=spec.imported.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?spec.imported.name:spec.imported.value;bindFsName(spec.local.name,imported);continue}fsNamespaces.add(spec.local.name)}},VariableDeclarator(node){if(node.init!==null&&node.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.parent?.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclaration&&node.parent.kind==="const"){constBindings.set(node.id.name,node.init)}if(node.init===null||!isFsRequire(node.init))return;if(node.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){fsNamespaces.add(node.id.name);return}if(node.id.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectPattern)return;for(const prop of node.id.properties){if(prop.type!==eslint_devkit_1.AST_NODE_TYPES.Property||prop.computed)continue;if(prop.value.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)continue;const key=prop.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?prop.key.name:prop.key.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof prop.key.value==="string"?prop.key.value:void 0;if(key===void 0)continue;bindFsName(prop.value.name,key)}},CallExpression(node){pendingCalls.push(node)},"Program:exit"(){for(const call of pendingCalls)checkFsCall(call)}}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.detectNonLiteralFsFilename=exports.determineRiskLevel=exports.isFsModule=exports.generateRefactoringSteps=void 0;exports.fsMethodName=fsMethodName;exports.isFsRequire=isFsRequire;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");const FS_OPERATIONS=[{method:"readFile",dangerous:true,vulnerability:"file-access",safePattern:"path.resolve(SAFE_DIR, path.basename(userInput))",example:{bad:"fs.readFile(userPath, callback)",good:"const safePath = path.join(SAFE_UPLOADS_DIR, path.basename(userPath)); fs.readFile(safePath, callback)"},effort:"10-15 minutes"},{method:"writeFile",dangerous:true,vulnerability:"file-access",safePattern:"path.resolve(SAFE_DIR, path.basename(userInput))",example:{bad:"fs.writeFile(userPath, data, callback)",good:"const safePath = path.join(SAFE_WRITES_DIR, path.basename(userPath)); fs.writeFile(safePath, data, callback)"},effort:"10-15 minutes"},{method:"stat",dangerous:true,vulnerability:"path-traversal",safePattern:"path.resolve(baseDir, userInput) with validation",example:{bad:"fs.stat(userPath, callback)",good:"const resolvedPath = path.resolve(SAFE_DIR, userPath);\nif (!resolvedPath.startsWith(SAFE_DIR + path.sep)) return;\nfs.stat(resolvedPath, callback)"},effort:"15-20 minutes"},{method:"readdir",dangerous:true,vulnerability:"directory-traversal",safePattern:"Validate directory is within allowed paths",example:{bad:"fs.readdir(userDir, callback)",good:"const resolvedDir = path.resolve(ALLOWED_DIRS, userDir);\nif (!resolvedDir.startsWith(ALLOWED_DIRS + path.sep)) return;\nfs.readdir(resolvedDir, callback)"},effort:"15-20 minutes"}];const hasTraversalPatterns=pathStr=>{return/\.\.[/\\]/.test(pathStr)||/^\.\.[/\\]/.test(pathStr)};const PROCESS_INPUT_MEMBERS=new Set(["env","argv","argv0","execArgv","stdin"]);const SENSITIVE_SEGMENTS=["etc/passwd","etc/shadow","etc/hosts","etc/sudoers","proc/self",".ssh/id_rsa",".ssh/id_dsa",".ssh/authorized_keys",".aws/credentials",".npmrc",".git/config","windows/system32/config/sam"];const targetsSensitiveLocation=pathStr=>{const unquoted=pathStr.replace(/^['"`]|['"`]$/g,"");const normalised=unquoted.replace(/\\/g,"/").toLowerCase();if(!/(^|\/)\.\.(\/|$)/.test(normalised)){return false}const withoutPrefix=normalised.replace(/^(?:\.{1,2}\/)+/,"");return SENSITIVE_SEGMENTS.some(seg=>withoutPrefix===seg||withoutPrefix.endsWith(`/${seg}`)||withoutPrefix.startsWith(`${seg}/`))};const generateRefactoringSteps=operation=>{switch(operation.method){case"readFile":case"writeFile":return[" 1. Define a SAFE_DIR constant for allowed operations"," 2. Use path.basename() to strip directory components"," 3. Combine with SAFE_DIR: path.join(SAFE_DIR, path.basename(userPath))"," 4. Optionally validate file extensions"," 5. Add error handling for invalid paths"].join("\n");case"stat":return[" 1. Use path.resolve() to normalize the path"," 2. Check the resolved path starts with the base PLUS path.sep \u2014 a bare"," prefix lets /safebad through a /safe check"," 3. Reject requests that escape the allowed directory"," 4. Use path.relative() for additional validation"," 5. Log security events for monitoring"].join("\n");case"readdir":return[" 1. Resolve the directory path: path.resolve(ALLOWED_DIRS, userDir)"," 2. Validate resolved path starts with ALLOWED_DIRS"," 3. Check directory exists and is readable"," 4. Consider whitelisting allowed directories"," 5. Add rate limiting to prevent enumeration attacks"].join("\n");default:return[" 1. Identify the specific file operation needed"," 2. Define safe base directories for operations"," 3. Use path.resolve() and validate containment"," 4. Sanitize user input (basename, extension validation)"," 5. Add comprehensive error handling"].join("\n")}};exports.generateRefactoringSteps=generateRefactoringSteps;const FS_MODULE_EQUIVALENTS={"fs-extra":"fs","graceful-fs":"fs","fs/promises":"fs"};const FS_MODULES=new Set(["fs","node:fs","fs/promises","node:fs/promises","fs-extra","graceful-fs"]);const isFsModule=source=>typeof source==="string"&&FS_MODULES.has(source);exports.isFsModule=isFsModule;function fsMethodName(callee,namespaces,named){if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return named.get(callee.name);if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression||callee.computed||callee.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier){return void 0}const object=callee.object;if(object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&namespaces.has(object.name)){return callee.property.name}if(object.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!object.computed&&object.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&namespaces.has(object.object.name)&&object.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&object.property.name==="promises"){return callee.property.name}return void 0}function isFsRequire(node){return node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.name==="require"&&node.arguments.length>0&&node.arguments[0].type===eslint_devkit_1.AST_NODE_TYPES.Literal&&(0,exports.isFsModule)(node.arguments[0].value)}const determineRiskLevel=(operation,pathStr)=>{if(hasTraversalPatterns(pathStr)){return"CRITICAL"}if(operation.dangerous){return"HIGH"}return"MEDIUM"};exports.determineRiskLevel=determineRiskLevel;exports.detectNonLiteralFsFilename=(0,eslint_devkit_2.createRule)({name:"detect-non-literal-fs-filename",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/detect-non-literal-fs-filename.md",description:"Detects variable in filename argument of fs calls, which might allow an attacker to access anything on your system",cwe:"CWE-22",confidence:"medium"},messages:{fsPathTraversal:(0,eslint_devkit_1.formatLLMMessage)({icon:"\u{1F511}",issueName:"Path traversal",cwe:"CWE-22",description:"Path traversal vulnerability",severity:"{{riskLevel}}",fix:"{{safePattern}} \u2014 Not a finding when the value is path.basename()d, checked against an allowlist, or resolved and then prefix-checked WITH a trailing separator",documentationLink:"https://owasp.org/www-community/attacks/Path_Traversal"})},schema:[{type:"object",properties:{taintSources:{type:"array",items:{type:"string"},default:["process"],description:"Identifier roots treated as attacker-reachable. Default: ['process'] (process.argv and process.env). Request roots (req/request/ctx/event) are deliberately NOT included \u2014 no-arbitrary-file-access owns those, and listing them here would double-report one line at two severities. Add them only if you run this rule without that one."},reportUnresolvedPaths:{type:"boolean",default:false,description:"Report paths whose provenance cannot be resolved. Restores the pre-inversion behaviour; measured at 7% precision on real code."},allowLiterals:{type:"boolean",default:false,description:`Allow literal string paths. Default true: a rule named "non-literal" reporting a literal contradicts its contract. Set false to also flag hardcoded paths containing "../" \u2014 measured as this rule's largest FP class on real code.`},additionalMethods:{type:"array",items:{type:"string"},default:[],description:"Additional fs methods to check"}},additionalProperties:false}]},skipTestFiles:true,defaultOptions:[{allowLiterals:false,additionalMethods:[]}],create(context){const options=context.options[0]||{};const{allowLiterals=false,additionalMethods=[]}=options;const DEFAULT_TAINT_ROOTS=["process","req","request","ctx","context","event"];const WHOLE_VALUE_TRUSTED_ROOTS=new Set(["process"]);const AMBIGUOUS_ROOTS=new Set(["ctx","context"]);const REQUEST_SURFACE=new Set(["query","params","body","request","req","headers","cookies","searchParams","originalUrl"]);const resolvesInFile=id=>context.sourceCode.getScope(id).references.find(ref=>ref.identifier===id)?.resolved!=null;const readsRequestSurface=node=>node?.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!node.computed&&node.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&REQUEST_SURFACE.has(node.property.name);const hasRequestEvidence=id=>{if(readsRequestSurface(id.parent))return true;const variable=context.sourceCode.getScope(id).references.find(ref=>ref.identifier===id).resolved;return variable.references.some(ref=>readsRequestSurface(ref.identifier.parent))};const taintRoots=new Set(options.taintSources??DEFAULT_TAINT_ROOTS);const reportUnresolvedPaths=options.reportUnresolvedPaths??false;const PATH_ARGUMENT_INDICES=new Map([["copyFile",[0,1]],["copyFileSync",[0,1]],["cp",[0,1]],["cpSync",[0,1]],["rename",[0,1]],["renameSync",[0,1]],["link",[0,1]],["linkSync",[0,1]],["symlink",[0,1]],["symlinkSync",[0,1]]]);const dangerousMethods=new Set(["readFile","readFileSync","writeFile","writeFileSync","appendFile","appendFileSync","stat","statSync","lstat","lstatSync","readdir","readdirSync","unlink","unlinkSync","mkdir","mkdirSync","rmdir","rmdirSync","access","accessSync","createReadStream","createWriteStream","open","openSync","rm","rmSync","rename","renameSync","copyFile","copyFileSync","cp","cpSync","truncate","truncateSync","chmod","chmodSync","chown","chownSync","lchown","lchownSync","utimes","utimesSync","readlink","readlinkSync","symlink","symlinkSync","link","linkSync","opendir","opendirSync",...additionalMethods]);const isLiteralString=node=>{return(0,eslint_devkit_1.staticString)(node)!==null};const isLocallyConstructed=id=>{const variable=context.sourceCode.getScope(id).references.find(ref=>ref.identifier===id)?.resolved;if(!variable||variable.defs.length!==1)return false;const def=variable.defs[0];if(def.type!=="Variable")return false;if(variable.references.filter(ref=>ref.isWrite()).length>1){return false}const init=def.node.init;return init?.type===eslint_devkit_1.AST_NODE_TYPES.ObjectExpression||init?.type===eslint_devkit_1.AST_NODE_TYPES.ArrayExpression};const readsTaintSource=(node,depth=0)=>{if(depth>6)return false;switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Identifier:{if(taintRoots.has(node.name)){if(AMBIGUOUS_ROOTS.has(node.name)&&resolvesInFile(node)&&!hasRequestEvidence(node)){return false}return!isLocallyConstructed(node)}const bound=constBindings.get(node.name);return bound!==void 0&&readsTaintSource(bound,depth+1)}case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:{if(!node.computed&&node.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.object.name==="process"&&node.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&!PROCESS_INPUT_MEMBERS.has(node.property.name)){return false}return readsTaintSource(node.object,depth+1)}case eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral:return node.expressions.some(e=>readsTaintSource(e,depth+1));case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:return readsTaintSource(node.left,depth+1)||readsTaintSource(node.right,depth+1);case eslint_devkit_1.AST_NODE_TYPES.CallExpression:{const callee=node.callee;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!callee.computed&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="path"&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.property.name==="basename"){return false}return node.arguments.some(arg=>arg.type!==eslint_devkit_1.AST_NODE_TYPES.SpreadElement&&readsTaintSource(arg,depth+1))}default:return false}};const containsUntrustedRoot=(node,depth=0)=>{if(depth>6)return false;switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Identifier:{if(taintRoots.has(node.name))return!WHOLE_VALUE_TRUSTED_ROOTS.has(node.name);const bound=constBindings.get(node.name);return bound!==void 0&&containsUntrustedRoot(bound,depth+1)}case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:return containsUntrustedRoot(node.object,depth+1);case eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral:return node.expressions.some(e=>containsUntrustedRoot(e,depth+1));case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:return containsUntrustedRoot(node.left,depth+1)||containsUntrustedRoot(node.right,depth+1);case eslint_devkit_1.AST_NODE_TYPES.CallExpression:return node.arguments.some(a=>a.type!==eslint_devkit_1.AST_NODE_TYPES.SpreadElement&&containsUntrustedRoot(a,depth+1));default:return false}};const isWholeTaintValue=node=>{switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Identifier:{if(taintRoots.has(node.name))return WHOLE_VALUE_TRUSTED_ROOTS.has(node.name);const bound=constBindings.get(node.name);return bound!==void 0&&isWholeTaintValue(bound)}case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:return isWholeTaintValue(node.object);case eslint_devkit_1.AST_NODE_TYPES.CallExpression:{const callee=node.callee;const isPathCall=callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="path"||(0,eslint_devkit_1.resolveModuleBinding)(callee,context.sourceCode.getScope(callee),{})?.module==="path";if(!isPathCall)return false;if(node.arguments.length===1)return isWholeTaintValue(node.arguments[0]);return isWholeTaintValue(node.arguments[0])&&!containsUntrustedRoot(node)}default:return false}};const isDangerousPath=(pathNode,pathStr)=>{if(!pathNode)return reportUnresolvedPaths;if(isLiteralString(pathNode)){return!allowLiterals&&targetsSensitiveLocation(pathStr)}if(hasPathValidation(pathNode))return false;if(readsTaintSource(pathNode))return!isWholeTaintValue(pathNode);if(isBuildTimeConstant(pathNode))return false;if(isFreeVariable(pathNode))return true;if(containsFreeVariable(pathNode))return true;return reportUnresolvedPaths};function isFreeVariable(node){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const through=context.sourceCode.getScope(node).through;return through.some(ref=>ref.identifier===node&&ref.resolved===null)}const containsFreeVariable=(node,depth=0)=>{if(depth>4)return false;if(isBuildTimeConstant(node,depth))return false;if(isFreeVariable(node))return true;switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral:return node.expressions.some(e=>containsFreeVariable(e,depth+1));case eslint_devkit_1.AST_NODE_TYPES.CallExpression:return node.arguments.some(a=>a.type!==eslint_devkit_1.AST_NODE_TYPES.SpreadElement&&containsFreeVariable(a,depth+1));case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:return node.operator==="+"&&(containsFreeVariable(node.left,depth+1)||containsFreeVariable(node.right,depth+1));case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:return node.computed&&containsFreeVariable(node.property,depth+1);default:return false}};const isBuildTimeConstant=(node,depth=0)=>{if(depth>4)return false;if(isLiteralString(node)){return!hasTraversalPatterns(node.value)}if(node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){if(node.name==="__dirname"||node.name==="__filename")return true;const bound=constBindings.get(node.name);return bound!==void 0&&isBuildTimeConstant(bound,depth+1)}if(node.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral){const literalText=node.quasis.map(q=>q.value.raw).join("");if(hasTraversalPatterns(literalText))return false;return node.expressions.every(e=>isBuildTimeConstant(e,depth+1))}if(node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){const callee=node.callee;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="process"&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.property.name==="cwd"){return true}if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="path"&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&["join","resolve"].includes(callee.property.name)){return node.arguments.length>0&&node.arguments.every(arg=>isBuildTimeConstant(arg,depth+1))}return false}if(node.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression&&node.operator==="+"){return isBuildTimeConstant(node.left,depth+1)&&isBuildTimeConstant(node.right,depth+1)}return false};const isSeparatorAnchored=arg=>{if(arg===void 0)return false;const endsWithSep=n=>{if(n.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!n.computed&&n.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&n.object.name==="path"&&n.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&n.property.name==="sep"){return true}const staticText=(0,eslint_devkit_1.staticString)(n);if(staticText!==null){return staticText.endsWith("/")||staticText.endsWith("\\")}return false};if(endsWithSep(arg))return true;if(arg.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression&&arg.operator==="+"){return endsWithSep(arg.right)}if(arg.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral){const last=arg.quasis[arg.quasis.length-1].value.cooked;if(last.endsWith("/")||last.endsWith("\\"))return true;if(last===""){const tail=arg.expressions[arg.expressions.length-1];return tail!==void 0&&endsWithSep(tail)}return false}return false};const hasPathValidation=pathNode=>{if(pathNode.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier){const parts=[];const collect=n=>{if(n.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression&&n.operator==="+"){collect(n.left);collect(n.right);return}if(n.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral){n.expressions.forEach(e=>collect(e));return}if(n.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){n.arguments.forEach(a=>{if(a.type!==eslint_devkit_1.AST_NODE_TYPES.SpreadElement)collect(a)});return}if(readsTaintSource(n))parts.push(n)};collect(pathNode);return parts.length>0&&parts.every(p=>p.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&hasPathValidation(p))}const varName=pathNode.name;const isValidationCall=testNode=>{if(testNode.type===eslint_devkit_1.AST_NODE_TYPES.UnaryExpression&&testNode.operator==="!"&&testNode.argument.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){testNode=testNode.argument}if(testNode.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression){return false}if(testNode.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&testNode.callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&testNode.callee.object.name===varName&&testNode.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&(testNode.callee.property.name==="startsWith"||testNode.callee.property.name==="includes")){return testNode.callee.property.name==="includes"?true:isSeparatorAnchored(testNode.arguments[0])}if(testNode.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&testNode.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&testNode.callee.property.name==="includes"){for(const arg of testNode.arguments){if(arg.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&arg.name===varName){return true}}}if(testNode.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&testNode.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&testNode.callee.property.name==="test"){for(const arg of testNode.arguments){if(arg.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&arg.name===varName){return true}}}return false};const hasEarlyExit=consequent=>{if(consequent.type===eslint_devkit_1.AST_NODE_TYPES.BlockStatement){return consequent.body.some(stmt=>stmt.type===eslint_devkit_1.AST_NODE_TYPES.ThrowStatement||stmt.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement)}return consequent.type===eslint_devkit_1.AST_NODE_TYPES.ThrowStatement||consequent.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement};let current=pathNode.parent;let foundFunctionBody=false;while(current&&!foundFunctionBody){if(current.type===eslint_devkit_1.AST_NODE_TYPES.IfStatement){if(isValidationCall(current.test)){return true}}if(current.type===eslint_devkit_1.AST_NODE_TYPES.BlockStatement&&current.parent&&(current.parent.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration||current.parent.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression||current.parent.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression)){foundFunctionBody=true;const blockBody=current.body;const nodeIndex=blockBody.findIndex(stmt=>{let check=pathNode;while(check){if(check===stmt)return true;check=check.parent}return false});for(let i=0;i<nodeIndex;i++){const stmt=blockBody[i];if(stmt.type===eslint_devkit_1.AST_NODE_TYPES.IfStatement&&isValidationCall(stmt.test)&&hasEarlyExit(stmt.consequent)){return true}}}current=current.parent}return false};const fsNamespaces=new Set(["fs"]);const fsNamedMethods=new Map;const pendingCalls=[];const constBindings=new Map;function bindFsName(local,imported){if(imported==="promises")fsNamespaces.add(local);else fsNamedMethods.set(local,imported)}const checkFsCall=node=>{let methodName=fsMethodName(node.callee,fsNamespaces,fsNamedMethods);if(methodName===void 0){const binding=(0,eslint_devkit_1.resolveModuleBinding)(node.callee,context.sourceCode.getScope(node),{equivalents:FS_MODULE_EQUIVALENTS});if(binding?.module!=="fs")return;const[first,second]=binding.path;methodName=binding.path.length===1?first:binding.path.length===2&&first==="promises"?second:void 0;if(methodName===void 0)return}if(!dangerousMethods.has(methodName)){return}const method=methodName;const indices=PATH_ARGUMENT_INDICES.get(method)??[0];const sourceCode=context.sourceCode;let pathNode=null;let path="";for(const index of indices){const candidate=node.arguments[index];if(candidate===void 0||candidate.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement)continue;if(isDangerousPath(candidate,sourceCode.getText(candidate))){pathNode=candidate;path=sourceCode.getText(candidate);break}}if(pathNode===null){const present=indices.some(index=>node.arguments[index]!==void 0);if(!present&&isDangerousPath(null,"")){context.report({node,messageId:"fsPathTraversal",data:{method,path:"",riskLevel:"MEDIUM",vulnerability:"path traversal",safePattern:"Use path.resolve() with validation",steps:"Review file system access patterns"}})}return}const operation=FS_OPERATIONS.find(op=>op.method===method)??null;const riskLevel=(0,exports.determineRiskLevel)(operation||FS_OPERATIONS[0],path);const steps=operation?(0,exports.generateRefactoringSteps)(operation):"Review file system access patterns";const safePattern=operation?.safePattern||"Use path.resolve() with validation";context.report({node,messageId:"fsPathTraversal",data:{method,path,riskLevel,vulnerability:operation?.vulnerability||"path traversal",safePattern,steps,effort:operation?.effort||"15-20 minutes"}})};return{ImportDeclaration(node){if(!(0,exports.isFsModule)(node.source.value))return;for(const spec of node.specifiers){if(spec.type===eslint_devkit_1.AST_NODE_TYPES.ImportSpecifier){const imported=spec.imported.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?spec.imported.name:spec.imported.value;bindFsName(spec.local.name,imported);continue}fsNamespaces.add(spec.local.name)}},VariableDeclarator(node){if(node.init!==null&&node.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.parent?.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclaration&&node.parent.kind==="const"){constBindings.set(node.id.name,node.init)}if(node.init===null||!isFsRequire(node.init))return;if(node.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){fsNamespaces.add(node.id.name);return}if(node.id.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectPattern)return;for(const prop of node.id.properties){if(prop.type!==eslint_devkit_1.AST_NODE_TYPES.Property||prop.computed)continue;if(prop.value.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)continue;const key=prop.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?prop.key.name:(0,eslint_devkit_1.staticString)(prop.key)??void 0;if(key===void 0)continue;bindFsName(prop.value.name,key)}},CallExpression(node){pendingCalls.push(node)},"Program:exit"(){for(const call of pendingCalls)checkFsCall(call)}}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noDataInTempStorage=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const provenance_1=require("../../utils/provenance");const DEFAULT_TEMP_PATHS=["/tmp","/var/tmp","temp/","/temp"];const FS_WRITE_FUNCTIONS=["writeFile","writeFileSync","appendFile","appendFileSync","createWriteStream","outputFile","outputFileSync"];const FS_MKDTEMP_FUNCTIONS=["mkdtemp","mkdtempSync"];const FS_EQUIVALENTS={"fs-extra":"fs","graceful-fs":"fs"};function containsPathSegments(haystack,needle){const split=value=>value.split(/[/\\]/).filter(segment=>segment.length>0);const target=split(needle);if(target.length===0)return false;const segments=split(haystack);for(let start=0;start+target.length<=segments.length;start+=1){if(target.every((segment,offset)=>segments[start+offset]===segment))return true}return false}function isTmpdirCall(node){return node.type==="CallExpression"&&node.callee.type==="MemberExpression"&&!node.callee.computed&&node.callee.object.type==="Identifier"&&node.callee.object.name==="os"&&node.callee.property.type==="Identifier"&&node.callee.property.name==="tmpdir"}function isStaticTmpdirJoin(node){const{callee}=node;if(callee.type!=="MemberExpression"||callee.computed)return false;if(callee.property.type!=="Identifier")return false;if(callee.property.name!=="join"&&callee.property.name!=="resolve"){return false}if(callee.object.type!=="Identifier"||callee.object.name!=="path"){return false}if(node.arguments.length<2)return false;if(!isTmpdirCall(node.arguments[0]))return false;return node.arguments.slice(1).every(arg=>arg.type==="Literal"&&typeof arg.value==="string")}function isStaticTmpdirTemplate(node){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral)return false;let sawTmpdir=false;for(const expression of node.expressions){if(isTmpdirCall(expression)){sawTmpdir=true;continue}if(expression.type!==eslint_devkit_1.AST_NODE_TYPES.Literal||typeof expression.value!=="string"){return false}}return sawTmpdir}function isStaticTmpdirConcat(node){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.BinaryExpression||node.operator!=="+")return false;const side=part=>{if(isTmpdirCall(part))return"tmpdir";if(part.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof part.value==="string")return"static";if(isStaticTmpdirConcat(part))return"tmpdir";return"other"};const left=side(node.left);const right=side(node.right);if(left==="other"||right==="other")return false;return left==="tmpdir"||right==="tmpdir"}function isPredictableTempPath(node){return node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&isStaticTmpdirJoin(node)||isStaticTmpdirTemplate(node)||isStaticTmpdirConcat(node)}exports.noDataInTempStorage=(0,eslint_devkit_1.createRule)({name:"no-data-in-temp-storage",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-data-in-temp-storage.md",description:"Prevent sensitive data in temp directories",cwe:"CWE-312",cvss:5.5},messages:{violationDetected:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Temp Storage Data",cwe:"CWE-312",description:"Sensitive data written to temp directory - not secure",severity:"HIGH",fix:"Use secure storage location or encrypt data before writing",documentationLink:"https://cwe.mitre.org/data/definitions/312.html"}),predictableTempPath:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Predictable Temp File Name (CWE-377)",cwe:"CWE-377",cvss:7.5,description:"path.join(os.tmpdir(), \u2026) with a constant name resolves to the same path on every run, in a directory every local user can write. An attacker who pre-creates that name \u2014 or symlinks it at a file they want clobbered \u2014 wins the race before the write happens.",severity:"HIGH",fix:"Create the file inside a fresh directory from fs.mkdtemp/mkdtempSync, or add a crypto.randomUUID() segment to the name.",documentationLink:"https://cwe.mitre.org/data/definitions/377.html"})},schema:[{type:"object",properties:{tempPaths:{type:"array",items:{type:"string"},default:DEFAULT_TEMP_PATHS,description:"Temporary path prefixes to flag. Replaces the built-in list rather than extending it."},ignoreFiles:{type:"array",items:{type:"string"},default:[],description:"List of files or patterns to ignore"}},additionalProperties:false}]},defaultOptions:[{}],create(context){const options=context.options[0]||{};const tempPaths=options.tempPaths||DEFAULT_TEMP_PATHS;const ignoreFiles=options.ignoreFiles||[];const filename=context.filename;if(ignoreFiles.some(pattern=>filename.includes(pattern))){return{}}function report(node){context.report({node,messageId:"violationDetected"})}function fsEntryPoint(node){const callee=node.callee;const scope=context.sourceCode.getScope(node);const binding=callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.computed&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof callee.property.value==="string"?(base=>base&&{module:base.module,path:[...base.path,callee.property.value]})((0,eslint_devkit_1.resolveModuleBinding)(callee.object,scope,{equivalents:FS_EQUIVALENTS})):(0,eslint_devkit_1.resolveModuleBinding)(callee,scope,{equivalents:FS_EQUIVALENTS});if(binding){const fn=binding.path.at(-1);const prefix=binding.path.slice(0,-1);if(fn===void 0)return void 0;const reachable=binding.module==="fs"&&(prefix.length===0||prefix.length===1&&prefix[0]==="promises")||binding.module==="fs/promises"&&prefix.length===0;return reachable?fn:void 0}if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!callee.computed&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="fs"&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return callee.property.name}return void 0}function isFsWriteCall(node){const fn=fsEntryPoint(node);return fn!==void 0&&FS_WRITE_FUNCTIONS.includes(fn)}function flowsIntoMkdtemp(identifier){const variable=(0,provenance_1.findVariable)(context.sourceCode,identifier);if(!variable)return false;return variable.references.some(reference=>{const parent=reference.identifier.parent;if(parent?.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return false;if(parent.arguments[0]!==reference.identifier)return false;const fn=fsEntryPoint(parent);return fn!==void 0&&FS_MKDTEMP_FUNCTIONS.includes(fn)})}function reportIfPredictable(candidate,bound){if(!candidate)return;if(!isPredictableTempPath(candidate))return;if(bound!==null&&bound.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&flowsIntoMkdtemp(bound)){return}context.report({node:candidate,messageId:"predictableTempPath"})}function staticPathRuns(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.Literal){return typeof node.value==="string"?[node.value]:[]}if(node.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral){return node.quasis.map(quasi=>quasi.value.raw)}if(node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!node.callee.computed&&node.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&(node.callee.property.name==="join"||node.callee.property.name==="resolve")){return node.arguments.filter(arg=>arg.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof arg.value==="string").map(arg=>arg.value)}return[]}function resolvedPathRuns(node){const direct=staticPathRuns(node);if(direct.length>0)return direct;if(node.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return[];const variable=(0,provenance_1.findVariable)(context.sourceCode,node);if(!variable)return[];const priorWrites=variable.references.map(reference=>reference.writeExpr).filter(write=>write!=null).filter(write=>write.range[1]<=node.range[0]).sort((a,b)=>a.range[1]-b.range[1]);const lastWrite=priorWrites.at(-1);return lastWrite?staticPathRuns(lastWrite):[]}return{CallExpression(node){if(!isFsWriteCall(node))return;const pathArg=node.arguments[0];if(pathArg){const runs=resolvedPathRuns(pathArg);if(runs.some(run=>tempPaths.some(tp=>containsPathSegments(run,tp)))){report(pathArg);return}}reportIfPredictable(pathArg,null)},VariableDeclarator(node){reportIfPredictable(node.init,node.id)},AssignmentExpression(node){reportIfPredictable(node.right,node.left)}}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noDataInTempStorage=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const provenance_1=require("../../utils/provenance");const DEFAULT_TEMP_PATHS=["/tmp","/var/tmp","temp/","/temp"];const FS_WRITE_FUNCTIONS=new Set(["writeFile","writeFileSync","appendFile","appendFileSync","createWriteStream","outputFile","outputFileSync"]);const FS_MKDTEMP_FUNCTIONS=new Set(["mkdtemp","mkdtempSync"]);const FS_EQUIVALENTS={"fs-extra":"fs","graceful-fs":"fs"};function containsPathSegments(haystack,needle){const split=value=>value.split(/[/\\]/).filter(segment=>segment.length>0);const target=split(needle);if(target.length===0)return false;const segments=split(haystack);for(let start=0;start+target.length<=segments.length;start+=1){if(target.every((segment,offset)=>segments[start+offset]===segment))return true}return false}function isTmpdirCall(node){return node.type==="CallExpression"&&node.callee.type==="MemberExpression"&&!node.callee.computed&&node.callee.object.type==="Identifier"&&node.callee.object.name==="os"&&node.callee.property.type==="Identifier"&&node.callee.property.name==="tmpdir"}function isStaticTmpdirJoin(node){const{callee}=node;if(callee.type!=="MemberExpression"||callee.computed)return false;if(callee.property.type!=="Identifier")return false;if(callee.property.name!=="join"&&callee.property.name!=="resolve"){return false}if(callee.object.type!=="Identifier"||callee.object.name!=="path"){return false}if(node.arguments.length<2)return false;if(!isTmpdirCall(node.arguments[0]))return false;return node.arguments.slice(1).every(arg=>(0,eslint_devkit_1.staticString)(arg)!==null)}function isStaticTmpdirTemplate(node){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral)return false;let sawTmpdir=false;for(const expression of node.expressions){if(isTmpdirCall(expression)){sawTmpdir=true;continue}if(expression.type!==eslint_devkit_1.AST_NODE_TYPES.Literal||typeof expression.value!=="string"){return false}}return sawTmpdir}function isStaticTmpdirConcat(node){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.BinaryExpression||node.operator!=="+")return false;const side=part=>{if(isTmpdirCall(part))return"tmpdir";if((0,eslint_devkit_1.staticString)(part)!==null)return"static";if(isStaticTmpdirConcat(part))return"tmpdir";return"other"};const left=side(node.left);const right=side(node.right);if(left==="other"||right==="other")return false;return left==="tmpdir"||right==="tmpdir"}function isPredictableTempPath(node){return node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&isStaticTmpdirJoin(node)||isStaticTmpdirTemplate(node)||isStaticTmpdirConcat(node)}exports.noDataInTempStorage=(0,eslint_devkit_1.createRule)({name:"no-data-in-temp-storage",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-data-in-temp-storage.md",description:"Prevent sensitive data in temp directories",cwe:"CWE-312",cvss:5.5},messages:{violationDetected:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Temp Storage Data",cwe:"CWE-312",description:"Sensitive data written to temp directory - not secure",severity:"HIGH",fix:"Use secure storage location or encrypt data before writing",documentationLink:"https://cwe.mitre.org/data/definitions/312.html"}),predictableTempPath:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Predictable Temp File Name (CWE-377)",cwe:"CWE-377",cvss:7.5,description:"path.join(os.tmpdir(), \u2026) with a constant name resolves to the same path on every run, in a directory every local user can write. An attacker who pre-creates that name \u2014 or symlinks it at a file they want clobbered \u2014 wins the race before the write happens.",severity:"HIGH",fix:"Create the file inside a fresh directory from fs.mkdtemp/mkdtempSync, or add a crypto.randomUUID() segment to the name.",documentationLink:"https://cwe.mitre.org/data/definitions/377.html"})},schema:[{type:"object",properties:{tempPaths:{type:"array",items:{type:"string"},default:DEFAULT_TEMP_PATHS,description:"Temporary path prefixes to flag. Replaces the built-in list rather than extending it."},ignoreFiles:{type:"array",items:{type:"string"},default:[],description:"List of files or patterns to ignore"}},additionalProperties:false}]},defaultOptions:[{}],create(context){const options=context.options[0]||{};const tempPaths=options.tempPaths||DEFAULT_TEMP_PATHS;const ignoreFiles=options.ignoreFiles||[];const filename=context.filename;if(ignoreFiles.some(pattern=>filename.includes(pattern))){return{}}function report(node){context.report({node,messageId:"violationDetected"})}function fsEntryPoint(node){const callee=node.callee;const scope=context.sourceCode.getScope(node);const binding=callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.computed&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof callee.property.value==="string"?(base=>base&&{module:base.module,path:[...base.path,callee.property.value]})((0,eslint_devkit_1.resolveModuleBinding)(callee.object,scope,{equivalents:FS_EQUIVALENTS})):(0,eslint_devkit_1.resolveModuleBinding)(callee,scope,{equivalents:FS_EQUIVALENTS});if(binding){const fn=binding.path.at(-1);const prefix=binding.path.slice(0,-1);if(fn===void 0)return void 0;const reachable=binding.module==="fs"&&(prefix.length===0||prefix.length===1&&prefix[0]==="promises")||binding.module==="fs/promises"&&prefix.length===0;return reachable?fn:void 0}if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!callee.computed&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="fs"&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return callee.property.name}return void 0}function isFsWriteCall(node){const fn=fsEntryPoint(node);return fn!==void 0&&FS_WRITE_FUNCTIONS.has(fn)}function flowsIntoMkdtemp(identifier){const variable=(0,provenance_1.findVariable)(context.sourceCode,identifier);if(!variable)return false;return variable.references.some(reference=>{const parent=reference.identifier.parent;if(parent?.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return false;if(parent.arguments[0]!==reference.identifier)return false;const fn=fsEntryPoint(parent);return fn!==void 0&&FS_MKDTEMP_FUNCTIONS.has(fn)})}function reportIfPredictable(candidate,bound){if(!candidate)return;if(!isPredictableTempPath(candidate))return;if(bound!==null&&bound.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&flowsIntoMkdtemp(bound)){return}context.report({node:candidate,messageId:"predictableTempPath"})}function staticPathRuns(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.Literal){return typeof node.value==="string"?[node.value]:[]}if(node.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral){return node.quasis.map(quasi=>quasi.value.raw)}if(node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!node.callee.computed&&node.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&(node.callee.property.name==="join"||node.callee.property.name==="resolve")){return node.arguments.map(arg=>(0,eslint_devkit_1.staticString)(arg)).filter(text=>text!==null)}return[]}function resolvedPathRuns(node){const direct=staticPathRuns(node);if(direct.length>0)return direct;if(node.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return[];const variable=(0,provenance_1.findVariable)(context.sourceCode,node);if(!variable)return[];const priorWrites=variable.references.map(reference=>reference.writeExpr).filter(write=>write!=null).filter(write=>write.range[1]<=node.range[0]).sort((a,b)=>a.range[1]-b.range[1]);const lastWrite=priorWrites.at(-1);return lastWrite?staticPathRuns(lastWrite):[]}return{CallExpression(node){if(!isFsWriteCall(node))return;const pathArg=node.arguments[0];if(pathArg){const runs=resolvedPathRuns(pathArg);if(runs.some(run=>tempPaths.some(tp=>containsPathSegments(run,tp)))){report(pathArg);return}}reportIfPredictable(pathArg,null)},VariableDeclarator(node){reportIfPredictable(node.init,node.id)},AssignmentExpression(node){reportIfPredictable(node.right,node.left)}}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noEnvInjection=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const DEFAULT_REQUEST_ROOTS=["req","request","ctx","event"];const MAX_TRACE_DEPTH=3;function isProcessEnv(node){return node.type==="MemberExpression"&&!node.computed&&node.object.type==="Identifier"&&node.object.name==="process"&&node.property.type==="Identifier"&&node.property.name==="env"}function memberChainRoot(node){let current=node;while(current.type==="MemberExpression"){current=current.object}return current.type==="Identifier"?current:null}exports.noEnvInjection=(0,eslint_devkit_1.createRule)({name:"no-env-injection",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-env-injection.md",description:"Disallow writing process.env under a key the caller controls, which can overwrite PATH, NODE_OPTIONS or LD_PRELOAD (CWE-99)",cwe:"CWE-99",cvss:8.8,confidence:"high"},messages:{envKeyInjection:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Environment Variable Injection (CWE-99)",cwe:"CWE-99",cvss:8.8,description:'process.env[\u2026] is written under a key that traces back to the request, so the caller decides WHICH variable is set. PATH changes which binary every later spawn/exec resolves; NODE_OPTIONS="--require ./x.js" and LD_PRELOAD run attacker code in every child process. The value being validated does not help \u2014 the name is the vulnerability.',severity:"HIGH",fix:'Map the caller\'s input through a fixed allowlist before it reaches the key: `const ALLOWED = { locale: "APP_LOCALE" }; const name = ALLOWED[input]; if (!name) return; process.env[name] = value;`. Better still, keep request-scoped settings out of the process environment entirely.',documentationLink:"https://cwe.mitre.org/data/definitions/99.html"}),envBulkInjection:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Bulk Environment Overwrite (CWE-99)",cwe:"CWE-99",cvss:8.8,description:"Object.assign(process.env, \u2026) copies every key of a request-derived object into the environment, so the caller chooses the names AND the values wholesale \u2014 PATH, NODE_OPTIONS and LD_PRELOAD included.",severity:"HIGH",fix:"Copy only the keys you name yourself, from a fixed allowlist, instead of spreading the request object into the environment.",documentationLink:"https://cwe.mitre.org/data/definitions/99.html"})},schema:[{type:"object",properties:{extraRequestRoots:{type:"array",items:{type:"string"},default:[],description:"Extra identifiers to treat as roots of request-controlled data. Added to the built-in roots, not a replacement for them."}},additionalProperties:false}]},defaultOptions:[{}],create(context,[options]){const{extraRequestRoots}=options;const requestRoots=new Set([...DEFAULT_REQUEST_ROOTS,...extraRequestRoots??[]]);function findVariable(node){let scope=context.sourceCode.getScope(node);while(scope){const found=scope.variables.find(v=>v.name===node.name);if(found)return found;scope=scope.upper}return null}function isRequestDerived(node,depth){if(depth>MAX_TRACE_DEPTH)return false;if(node.type==="MemberExpression"){const root=memberChainRoot(node);return root!==null&&requestRoots.has(root.name)}if(node.type!=="Identifier")return false;if(requestRoots.has(node.name))return true;const variable=findVariable(node);if(!variable)return false;if(variable.references.filter(ref=>ref.isWrite()).length!==1){return false}const[def]=variable.defs;if(!def||def.node.type!=="VariableDeclarator")return false;const init=def.node.init;return init!=null&&isRequestDerived(init,depth+1)}return{AssignmentExpression(node){const{left}=node;if(left.type!=="MemberExpression"||!left.computed)return;if(!isProcessEnv(left.object))return;if(!isRequestDerived(left.property,0))return;context.report({node:left.property,messageId:"envKeyInjection"})},CallExpression(node){const{callee}=node;if(callee.type!=="MemberExpression"||callee.computed)return;if(callee.property.type!=="Identifier")return;if(callee.property.name!=="assign")return;if(callee.object.type!=="Identifier")return;if(callee.object.name!=="Object")return;const[target,...sources]=node.arguments;if(!target||!isProcessEnv(target))return;for(const source of sources){if(isRequestDerived(source,0)){context.report({node:source,messageId:"envBulkInjection"});return}}}}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noEnvInjection=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const DEFAULT_REQUEST_ROOTS=["req","request","ctx","event"];const MAX_TRACE_DEPTH=3;function isProcessEnv(node){return node.type==="MemberExpression"&&!node.computed&&node.object.type==="Identifier"&&node.object.name==="process"&&node.property.type==="Identifier"&&node.property.name==="env"}function memberChainRoot(node){let current=node;while(current.type==="MemberExpression"){current=current.object}return current.type==="Identifier"?current:null}exports.noEnvInjection=(0,eslint_devkit_1.createRule)({name:"no-env-injection",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-env-injection.md",description:"Disallow writing process.env under a key the caller controls, which can overwrite PATH, NODE_OPTIONS or LD_PRELOAD (CWE-99)",cwe:"CWE-99",cvss:8.8,confidence:"high"},messages:{envKeyInjection:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Environment Variable Injection (CWE-99)",cwe:"CWE-99",cvss:8.8,description:'process.env[\u2026] is written under a key that traces back to the request, so the caller decides WHICH variable is set. PATH changes which binary every later spawn/exec resolves; NODE_OPTIONS="--require ./x.js" and LD_PRELOAD run attacker code in every child process. The value being validated does not help \u2014 the name is the vulnerability.',severity:"HIGH",fix:'Map the caller\'s input through a fixed allowlist before it reaches the key: `const ALLOWED = { locale: "APP_LOCALE" }; const name = ALLOWED[input]; if (!name) return; process.env[name] = value;`. Better still, keep request-scoped settings out of the process environment entirely.',documentationLink:"https://cwe.mitre.org/data/definitions/99.html"}),envBulkInjection:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Bulk Environment Overwrite (CWE-99)",cwe:"CWE-99",cvss:8.8,description:"Object.assign(process.env, \u2026) copies every key of a request-derived object into the environment, so the caller chooses the names AND the values wholesale \u2014 PATH, NODE_OPTIONS and LD_PRELOAD included.",severity:"HIGH",fix:"Copy only the keys you name yourself, from a fixed allowlist, instead of spreading the request object into the environment.",documentationLink:"https://cwe.mitre.org/data/definitions/99.html"})},schema:[{type:"object",properties:{requestRootNames:{type:"array",items:{type:"string"},default:[...DEFAULT_REQUEST_ROOTS],description:"Identifiers that name the request at the top of a handler. Replaces the default."},extraRequestRoots:{type:"array",items:{type:"string"},default:[],description:"Extra identifiers to treat as roots of request-controlled data. Added to the built-in roots, not a replacement for them."}},additionalProperties:false}]},defaultOptions:[{}],create(context,[options]){const{extraRequestRoots,requestRootNames}=options;const requestRoots=new Set([...requestRootNames??DEFAULT_REQUEST_ROOTS,...extraRequestRoots??[]]);function findVariable(node){let scope=context.sourceCode.getScope(node);while(scope){const found=scope.variables.find(v=>v.name===node.name);if(found)return found;scope=scope.upper}return null}function isRequestDerived(node,depth){if(depth>MAX_TRACE_DEPTH)return false;if(node.type==="MemberExpression"){const root=memberChainRoot(node);return root!==null&&requestRoots.has(root.name)}if(node.type!=="Identifier")return false;if(requestRoots.has(node.name))return true;const variable=findVariable(node);if(!variable)return false;if(variable.references.filter(ref=>ref.isWrite()).length!==1){return false}const[def]=variable.defs;if(!def||def.node.type!=="VariableDeclarator")return false;const init=def.node.init;return init!=null&&isRequestDerived(init,depth+1)}return{AssignmentExpression(node){const{left}=node;if(left.type!=="MemberExpression"||!left.computed)return;if(!isProcessEnv(left.object))return;if(!isRequestDerived(left.property,0))return;context.report({node:left.property,messageId:"envKeyInjection"})},CallExpression(node){const{callee}=node;if(callee.type!=="MemberExpression"||callee.computed)return;if(callee.property.type!=="Identifier")return;if(callee.property.name!=="assign")return;if(callee.object.type!=="Identifier")return;if(callee.object.name!=="Object")return;const[target,...sources]=node.arguments;if(!target||!isProcessEnv(target))return;for(const source of sources){if(isRequestDerived(source,0)){context.report({node:source,messageId:"envBulkInjection"});return}}}}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noMathRandomCrypto=void 0;const names_1=require("../../utils/names");const provenance_1=require("../../utils/provenance");const eslint_devkit_1=require("@interlace/eslint-devkit");const CSPRNG_CALLS=new Set(["getRandomValues","randomBytes","randomUUID","randomFillSync","randomFill","generateKey","subtle"]);const CRYPTO_WORDS=["token","tokens","key","keys","secret","secrets","password","passwd","salt","iv","nonce","seed","hash","cipher","auth","session","csrf","otp","pin","code","codes","verify","signature","credential","jwt","encryption","apikey","passphrase","mnemonic"];const CRYPTO_WORD_SET=new Set(CRYPTO_WORDS);const DURATION_TAILS=new Set(["delay","timeout","interval","jitter","backoff","ms","millis","milliseconds","seconds","duration","wait","sleep","ttl","deadline","elapsed","latency","budget"]);const QUANTITY_TAILS=new Set(["count","counts","length","size","total","limit","quota","offset","index","rank","score","percent","ratio","rate","version","page"]);const NON_SECURITY_QUALIFIERS=new Map([["code",new Set(["http","status","error","err","exit","country","zip","postal","area","language","lang","locale","currency","promo","coupon","discount","color","colour","qr","bar","region","iso","mime","media","sort","source","char","unicode","ascii","airport"])],["key",new Set(["cache","map","object","row","index","partition","primary","foreign","sort","storage","translation","locale","i18n","react","idempotency","shortcut","keyboard","press","modifier"])]]);const baseNameSuggestsCrypto=(0,names_1.makeNameTest)(CRYPTO_WORDS);function nameSuggestsCrypto(name){if(!baseNameSuggestsCrypto(name))return false;const words=(0,names_1.identifierWords)(name);const tail=words[words.length-1];if(DURATION_TAILS.has(tail)||QUANTITY_TAILS.has(tail))return false;const matched=words.filter(word=>CRYPTO_WORD_SET.has(word));if(matched.length===0)return true;return!matched.every(word=>{const qualifiers=NON_SECURITY_QUALIFIERS.get(word);return qualifiers!==void 0&&words.some(other=>qualifiers.has(other))})}function isMathRandomProperty(node){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return false;if(node.object.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;if(node.object.name!=="Math")return false;const property=node.property;if(!node.computed){return property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&property.name==="random"}return property.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&property.value==="random"}const NON_SECURITY_ID_QUALIFIERS=new Set(["request","req","correlation","trace","span","transaction","txn","message","msg","event","job","run","task","batch","element","node","row","record","instance","component","widget","dom","operation","call","invocation","frame","render"]);function isCorrelationIdFactory(name){const words=(0,names_1.identifierWords)(name);if(words[words.length-1]!=="id")return false;if(words.some(word=>CRYPTO_WORD_SET.has(word)))return false;return words.some(word=>NON_SECURITY_ID_QUALIFIERS.has(word))}function functionNameSuggestsCrypto(name){if(!CRYPTO_FUNCTION_PATTERNS.some(pattern=>pattern.test(name)))return false;return!isCorrelationIdFactory(name)}const CRYPTO_FUNCTION_PATTERNS=[/generate.*token/i,/generate.*key/i,/generate.*id/i,/create.*secret/i,/create.*token/i,/random.*string/i,/get.*random.*(string|bytes|token|key|secret|value|id)/i,/make.*salt/i,/gen.*password/i];exports.noMathRandomCrypto=(0,eslint_devkit_1.createRule)({name:"no-math-random-crypto",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-math-random-crypto.md",description:"Disallow Math.random() for cryptographic purposes",cwe:"CWE-338",cvss:7.5,confidence:"medium"},messages:{pseudoRandomBytes:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Non-cryptographic random bytes",cwe:"CWE-338",owasp:"A02:2021",cvss:7.5,description:"crypto.pseudoRandomBytes() is not cryptographically secure. The name is the API's own warning: it was deprecated in Node 4 precisely because callers assumed otherwise.",severity:"HIGH",compliance:["SOC2","PCI-DSS","ISO27001"],fix:"Use crypto.randomBytes(n), or crypto.randomUUID() for identifiers.",documentationLink:"https://cwe.mitre.org/data/definitions/338.html"}),mathRandomCrypto:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Math.random() used for crypto",cwe:"CWE-338",description:"Math.random() is not cryptographically secure. It uses a PRNG that can be predicted. Never use it for tokens, keys, passwords, or any security-sensitive values.",severity:"CRITICAL",fix:"Use crypto.randomBytes() or crypto.randomUUID() instead",documentationLink:"https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html#secure-random-number-generation"})},schema:[{type:"object",properties:{allowInTests:{type:"boolean",default:true,description:"Allow Math.random() in test files. Default: true"}},additionalProperties:false}]},defaultOptions:[{allowInTests:true}],create(context,[options={}]){const{allowInTests=true}=options;const sourceCode=context.sourceCode;const filename=context.filename;const isTestFile=allowInTests&&(0,eslint_devkit_1.isTestFilePath)(filename);function stableDeclarator(id){const variable=(0,provenance_1.findVariable)(sourceCode,id);if(!variable||variable.defs.length!==1)return void 0;const def=variable.defs[0];if(def.type!=="Variable")return void 0;const reassigned=variable.references.some(reference=>reference.writeExpr!=null&&!reference.init);return reassigned?void 0:def.node}function isCsprngFallback(node){let scope=node.parent;while(scope!==void 0&&scope.type!==eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration&&scope.type!==eslint_devkit_1.AST_NODE_TYPES.FunctionExpression&&scope.type!==eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression&&scope.type!==eslint_devkit_1.AST_NODE_TYPES.Program){scope=scope.parent}let found=false;const stack=[scope];while(stack.length>0&&!found){const current=stack.pop();if(current.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&current.range[1]<=node.range[0]){const callee=current.callee;const name=callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!callee.computed&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?callee.property.name:callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?callee.name:"";if(CSPRNG_CALLS.has(name))found=true}for(const key of Object.keys(current)){if(key==="parent")continue;const value=current[key];for(const child of Array.isArray(value)?value:[value]){if(child!==null&&typeof child==="object"&&typeof child.type==="string"){stack.push(child)}}}}return found}function isMathRandomCallee(callee){if(isMathRandomProperty(callee))return true;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!callee.computed&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const init2=stableDeclarator(callee.object)?.init;if(!init2||init2.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectExpression)return false;const wanted=callee.property.name;return init2.properties.some(property=>property.type===eslint_devkit_1.AST_NODE_TYPES.Property&&!property.computed&&property.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&property.key.name===wanted&&isMathRandomProperty(property.value))}if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const declarator=stableDeclarator(callee);const init=declarator?.init;if(!declarator||!init)return false;if(declarator.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return isMathRandomProperty(init)}if(declarator.id.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectPattern)return false;if(init.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier||init.name!=="Math")return false;return declarator.id.properties.some(property=>{if(property.type!==eslint_devkit_1.AST_NODE_TYPES.Property||property.computed)return false;if(property.value.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;if(property.value.name!==callee.name)return false;const key=property.key;if(key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return key.name==="random";return key.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&key.value==="random"})}const MAX_BINDING_HOPS=2;function usedInCryptoContext(id,depth){const variable=(0,provenance_1.findVariable)(sourceCode,id);if(!variable)return false;return variable.references.some(reference=>!reference.init&&isCryptoContext(reference.identifier,depth))}function escapesFunction(fn,cameFrom,passedReturn){if(fn.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression&&fn.body===cameFrom){return true}return passedReturn}function isCryptoContext(node,depth=0){let child=node;let current=node.parent;let namesThisValue=true;let passedReturn=false;while(current){if(current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration||current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression||current.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression){if(!escapesFunction(current,child,passedReturn))namesThisValue=false;passedReturn=false}if(current.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement)passedReturn=true;if(namesThisValue&&current.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclarator){if(current.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const varName=current.id.name;if(nameSuggestsCrypto(varName)){return true}if(depth<MAX_BINDING_HOPS&&usedInCryptoContext(current.id,depth+1)){return true}}}if(current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration&&current.id){const funcName=current.id.name;if(functionNameSuggestsCrypto(funcName)){return true}}if(namesThisValue&&current.type===eslint_devkit_1.AST_NODE_TYPES.AssignmentExpression){if(current.left.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&current.left.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const propName=current.left.property.name;if(nameSuggestsCrypto(propName)){return true}}if(current.left.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){if(nameSuggestsCrypto(current.left.name)){return true}}}if(namesThisValue&&current.type===eslint_devkit_1.AST_NODE_TYPES.Property){if(current.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const propName=current.key.name;if(nameSuggestsCrypto(propName)){return true}}}if(current.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement){const func=findContainingFunction(current);if(func){if((func.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration||func.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression)&&func.id?.name){const funcName=func.id.name;if(functionNameSuggestsCrypto(funcName)||nameSuggestsCrypto(funcName)){return true}}}}child=current;current=current.parent}return false}function findContainingFunction(node){let current=node.parent;while(current){if(current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration||current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression||current.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression){return current}current=current.parent}return null}return{CallExpression(node){if(isTestFile)return;if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!node.callee.computed&&node.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.property.name==="pseudoRandomBytes"){context.report({node,messageId:"pseudoRandomBytes"});return}if(isMathRandomCallee(node.callee)){if(isCryptoContext(node)&&!isCsprngFallback(node)){context.report({node,messageId:"mathRandomCrypto"})}}}}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noMathRandomCrypto=void 0;const names_1=require("../../utils/names");const provenance_1=require("../../utils/provenance");const eslint_devkit_1=require("@interlace/eslint-devkit");const CSPRNG_CALLS=new Set(["getRandomValues","randomBytes","randomUUID","randomFillSync","randomFill","generateKey","subtle"]);const CRYPTO_WORDS=["token","tokens","key","keys","secret","secrets","password","passwd","salt","iv","nonce","seed","hash","cipher","auth","session","csrf","otp","pin","code","codes","verify","signature","credential","jwt","encryption","apikey","passphrase","mnemonic"];const DURATION_TAILS=new Set(["delay","timeout","interval","jitter","backoff","ms","millis","milliseconds","seconds","duration","wait","sleep","ttl","deadline","elapsed","latency","budget"]);const QUANTITY_TAILS=new Set(["count","counts","length","size","total","limit","quota","offset","index","rank","score","percent","ratio","rate","version","page"]);const NON_SECURITY_QUALIFIERS=new Map([["code",new Set(["http","status","error","err","exit","country","zip","postal","area","language","lang","locale","currency","promo","coupon","discount","color","colour","qr","bar","region","iso","mime","media","sort","source","char","unicode","ascii","airport"])],["key",new Set(["cache","map","object","row","index","partition","primary","foreign","sort","storage","translation","locale","i18n","react","idempotency","shortcut","keyboard","press","modifier"])]]);function nameSuggestsCrypto(name,vocab){if(!vocab.suggestsCrypto(name))return false;const words=(0,names_1.identifierWords)(name);const tail=words[words.length-1];if(DURATION_TAILS.has(tail)||QUANTITY_TAILS.has(tail))return false;const matched=words.filter(word=>vocab.words.has(word));if(matched.length===0)return true;return!matched.every(word=>{const qualifiers=NON_SECURITY_QUALIFIERS.get(word);return qualifiers!==void 0&&words.some(other=>qualifiers.has(other))})}function isMathRandomProperty(node){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return false;if(node.object.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;if(node.object.name!=="Math")return false;const property=node.property;if(!node.computed){return property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&property.name==="random"}return property.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&property.value==="random"}const NON_SECURITY_ID_QUALIFIERS=new Set(["request","req","correlation","trace","span","transaction","txn","message","msg","event","job","run","task","batch","element","node","row","record","instance","component","widget","dom","operation","call","invocation","frame","render"]);function isCorrelationIdFactory(name,vocab){const words=(0,names_1.identifierWords)(name);if(words[words.length-1]!=="id")return false;if(words.some(word=>vocab.words.has(word)))return false;return words.some(word=>vocab.correlationWords.has(word))}function functionNameSuggestsCrypto(name,vocab){if(!CRYPTO_FUNCTION_PATTERNS.some(pattern=>pattern.test(name)))return false;return!isCorrelationIdFactory(name,vocab)}const CRYPTO_FUNCTION_PATTERNS=[/generate.*token/i,/generate.*key/i,/generate.*id/i,/create.*secret/i,/create.*token/i,/random.*string/i,/get.*random.*(string|bytes|token|key|secret|value|id)/i,/make.*salt/i,/gen.*password/i];exports.noMathRandomCrypto=(0,eslint_devkit_1.createRule)({name:"no-math-random-crypto",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-math-random-crypto.md",description:"Disallow Math.random() for cryptographic purposes",cwe:"CWE-338",cvss:7.5,confidence:"medium"},messages:{pseudoRandomBytes:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Non-cryptographic random bytes",cwe:"CWE-338",owasp:"A02:2021",cvss:7.5,description:"crypto.pseudoRandomBytes() is not cryptographically secure. The name is the API's own warning: it was deprecated in Node 4 precisely because callers assumed otherwise.",severity:"HIGH",compliance:["SOC2","PCI-DSS","ISO27001"],fix:"Use crypto.randomBytes(n), or crypto.randomUUID() for identifiers.",documentationLink:"https://cwe.mitre.org/data/definitions/338.html"}),mathRandomCrypto:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Math.random() used for crypto",cwe:"CWE-338",description:"Math.random() is not cryptographically secure. It uses a PRNG that can be predicted. Never use it for tokens, keys, passwords, or any security-sensitive values.",severity:"CRITICAL",fix:"Use crypto.randomBytes() or crypto.randomUUID() instead",documentationLink:"https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html#secure-random-number-generation"})},schema:[{type:"object",properties:{allowInTests:{type:"boolean",default:true,description:"Allow Math.random() in test files. Default: true"},secretWords:{type:"array",items:{type:"string"},default:[...CRYPTO_WORDS],description:"Words that name a security value in this codebase. Replaces the default list."},correlationIdWords:{type:"array",items:{type:"string"},default:[...NON_SECURITY_ID_QUALIFIERS],description:"Words that mark a trailing `id` as a correlation id rather than a credential. Replaces the default list."}},additionalProperties:false}]},defaultOptions:[{allowInTests:true,secretWords:[...CRYPTO_WORDS],correlationIdWords:[...NON_SECURITY_ID_QUALIFIERS]}],create(context,[options={}]){const{allowInTests=true,secretWords=CRYPTO_WORDS,correlationIdWords=[...NON_SECURITY_ID_QUALIFIERS]}=options;const vocab={words:new Set(secretWords),correlationWords:new Set(correlationIdWords),suggestsCrypto:(0,names_1.makeNameTest)(secretWords)};const sourceCode=context.sourceCode;const filename=context.filename;const isTestFile=allowInTests&&(0,eslint_devkit_1.isTestFilePath)(filename);function stableDeclarator(id){const variable=(0,provenance_1.findVariable)(sourceCode,id);if(!variable||variable.defs.length!==1)return void 0;const def=variable.defs[0];if(def.type!=="Variable")return void 0;const reassigned=variable.references.some(reference=>reference.writeExpr!=null&&!reference.init);return reassigned?void 0:def.node}function isCsprngFallback(node){let scope=node.parent;while(scope!==void 0&&scope.type!==eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration&&scope.type!==eslint_devkit_1.AST_NODE_TYPES.FunctionExpression&&scope.type!==eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression&&scope.type!==eslint_devkit_1.AST_NODE_TYPES.Program){scope=scope.parent}let found=false;const stack=[scope];while(stack.length>0&&!found){const current=stack.pop();if(current.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&current.range[1]<=node.range[0]){const callee=current.callee;const name=callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!callee.computed&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?callee.property.name:callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?callee.name:"";if(CSPRNG_CALLS.has(name))found=true}for(const key of Object.keys(current)){if(key==="parent")continue;const value=current[key];for(const child of Array.isArray(value)?value:[value]){if(child!==null&&typeof child==="object"&&typeof child.type==="string"){stack.push(child)}}}}return found}function isMathRandomCallee(callee){if(isMathRandomProperty(callee))return true;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!callee.computed&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const init2=stableDeclarator(callee.object)?.init;if(!init2||init2.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectExpression)return false;const wanted=callee.property.name;return init2.properties.some(property=>property.type===eslint_devkit_1.AST_NODE_TYPES.Property&&!property.computed&&property.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&property.key.name===wanted&&isMathRandomProperty(property.value))}if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const declarator=stableDeclarator(callee);const init=declarator?.init;if(!declarator||!init)return false;if(declarator.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return isMathRandomProperty(init)}if(declarator.id.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectPattern)return false;if(init.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier||init.name!=="Math")return false;return declarator.id.properties.some(property=>{if(property.type!==eslint_devkit_1.AST_NODE_TYPES.Property||property.computed)return false;if(property.value.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;if(property.value.name!==callee.name)return false;const key=property.key;if(key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return key.name==="random";return key.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&key.value==="random"})}const MAX_BINDING_HOPS=2;function usedInCryptoContext(id,depth){const variable=(0,provenance_1.findVariable)(sourceCode,id);if(!variable)return false;return variable.references.some(reference=>!reference.init&&isCryptoContext(reference.identifier,depth))}function escapesFunction(fn,cameFrom,passedReturn){if(fn.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression&&fn.body===cameFrom){return true}return passedReturn}function isCryptoContext(node,depth=0){let child=node;let current=node.parent;let namesThisValue=true;let passedReturn=false;while(current){if(current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration||current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression||current.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression){if(!escapesFunction(current,child,passedReturn))namesThisValue=false;passedReturn=false}if(current.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement)passedReturn=true;if(namesThisValue&&current.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclarator){if(current.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const varName=current.id.name;if(nameSuggestsCrypto(varName,vocab)){return true}if(depth<MAX_BINDING_HOPS&&usedInCryptoContext(current.id,depth+1)){return true}}}if(current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration&&current.id){const funcName=current.id.name;if(functionNameSuggestsCrypto(funcName,vocab)){return true}}if(namesThisValue&&current.type===eslint_devkit_1.AST_NODE_TYPES.AssignmentExpression){if(current.left.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&current.left.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const propName=current.left.property.name;if(nameSuggestsCrypto(propName,vocab)){return true}}if(current.left.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){if(nameSuggestsCrypto(current.left.name,vocab)){return true}}}if(namesThisValue&&current.type===eslint_devkit_1.AST_NODE_TYPES.Property){if(current.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const propName=current.key.name;if(nameSuggestsCrypto(propName,vocab)){return true}}}if(current.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement){const func=findContainingFunction(current);if(func){if((func.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration||func.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression)&&func.id?.name){const funcName=func.id.name;if(functionNameSuggestsCrypto(funcName,vocab)||nameSuggestsCrypto(funcName,vocab)){return true}}}}child=current;current=current.parent}return false}function findContainingFunction(node){let current=node.parent;while(current){if(current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration||current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression||current.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression){return current}current=current.parent}return null}return{CallExpression(node){if(isTestFile)return;if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!node.callee.computed&&node.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.property.name==="pseudoRandomBytes"){context.report({node,messageId:"pseudoRandomBytes"});return}if(isMathRandomCallee(node.callee)){if(isCryptoContext(node)&&!isCsprngFallback(node)){context.report({node,messageId:"mathRandomCrypto"})}}}}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noSsrf=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const provenance_1=require("../../utils/provenance");const HTTP_CLIENT_FUNCTIONS=new Set(["fetch","got","nodeFetch","undici"]);const HTTP_CLIENT_METHODS=new Set(["get","post","put","patch","delete","head","options","request"]);const HTTP_CLIENT_OBJECTS=new Set(["axios","got","superagent","request","http","https","undici","needle"]);const VALIDATION_FUNCTION_NAMES=new Set(["validateUrl","validateURL","isValidUrl","isSafeUrl","isAllowed","isValidURL","checkUrl","checkURL","sanitizeUrl","sanitizeURL"]);const USER_INPUT_SUBSTRINGS=["url","endpoint","uri","href","link","target","dest","source","host","user","input","param"];function isUserInputParamName(name){const lower=name.toLowerCase();return USER_INPUT_SUBSTRINGS.some(sub=>lower.includes(sub))}const REQUEST_ROOT_NAMES=new Set(["req","request","ctx","event"]);const URL_OPTION_KEYS=new Set(["url","href","uri"]);function isRequestSourced(node){let current=node;while(current.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){current=current.object}return current!==node&&current.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&REQUEST_ROOT_NAMES.has(current.name.toLowerCase())}function makeCarriesUntrustedUrl(sourceCode,reportUnresolvedUrls){const carries=(node,depth)=>{if(depth>6)return false;const bare=(0,eslint_devkit_1.unwrapTypeSyntax)(node);if(bare!==node)return carries(bare,depth+1);switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Identifier:{const init=(0,provenance_1.bindingInit)(sourceCode,node);if(init!==void 0)return carries(init,depth+1);return reportUnresolvedUrls&&isUserInputParamName(node.name)}case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:return isRequestSourced(node)||carries(node.object,depth+1);case eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral:return node.expressions.some(expression=>carries(expression,depth+1));case eslint_devkit_1.AST_NODE_TYPES.CallExpression:case eslint_devkit_1.AST_NODE_TYPES.NewExpression:return node.arguments.some(argument=>carries(argument,depth+1));case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:return carries(node.left,depth+1)||carries(node.right,depth+1);case eslint_devkit_1.AST_NODE_TYPES.ObjectExpression:return node.properties.some(property=>{if(property.type!==eslint_devkit_1.AST_NODE_TYPES.Property)return false;const value=property.value;if(isRequestSourced(value))return true;const key=property.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?property.key.name:property.key.type===eslint_devkit_1.AST_NODE_TYPES.Literal?String(property.key.value):"";return URL_OPTION_KEYS.has(key.toLowerCase())&&carries(value,depth+1)});default:return false}};return node=>carries(node,0)}function nodeContainsValidation(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.NewExpression&&node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.name==="URL"){return true}if(node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&VALIDATION_FUNCTION_NAMES.has(node.callee.name)){return true}if(node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const method=node.callee.property.name;if(method==="includes"||method==="has"||method==="startsWith"||method==="test"||method==="some"){return true}}if(node.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression&&(node.operator==="==="||node.operator==="==")&&(node.left.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.left.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&(node.left.property.name==="hostname"||node.left.property.name==="host")||node.right.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.right.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&(node.right.property.name==="hostname"||node.right.property.name==="host"))){return true}if(node.type===eslint_devkit_1.AST_NODE_TYPES.ThrowStatement){return true}const SKIP_KEYS=new Set(["parent","range","loc","tokens","comments","start","end"]);for(const key of Object.keys(node)){if(SKIP_KEYS.has(key))continue;const value=node[key];if(value&&typeof value==="object"&&"type"in value){if(nodeContainsValidation(value))return true}if(Array.isArray(value)){for(const item of value){if(item&&typeof item==="object"&&"type"in item){if(nodeContainsValidation(item))return true}}}}return false}function hasValidationBefore(node){let current=node.parent;while(current){const parent=current.parent;if(!parent)break;if(parent.type===eslint_devkit_1.AST_NODE_TYPES.BlockStatement||parent.type===eslint_devkit_1.AST_NODE_TYPES.Program){const body=parent.body;const idx=body.indexOf(current);for(let i=idx-1;i>=0&&i>=idx-10;i--){if(nodeContainsValidation(body[i])){return true}}}if(parent.type===eslint_devkit_1.AST_NODE_TYPES.IfStatement&&parent.test){if(nodeContainsValidation(parent.test)){return true}}current=parent}return false}exports.noSsrf=(0,eslint_devkit_1.createRule)({name:"no-ssrf",meta:{type:"suggestion",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-ssrf.md",description:"Flags HTTP calls whose URL argument is a user-input-named identifier or reads off a request object \u2014 a heuristic prompt for code review, not a proof of SSRF",cwe:"CWE-918",cvss:9.1},messages:{ssrfVulnerability:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Possible SSRF \u2014 heuristic (CWE-918)",cwe:"CWE-918",description:"HTTP call whose URL argument name suggests user input. This is a naming heuristic, not data-flow analysis \u2014 review whether the URL could originate from an untrusted source at runtime.",severity:"LOW",fix:"If the URL comes from user input, validate it against an allowlist of permitted hosts before making the request.",documentationLink:"https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html"})},schema:[{type:"object",properties:{allowInTests:{type:"boolean",default:true},reportUnresolvedUrls:{type:"boolean",default:false,description:"Report a URL argument that is a user-input-named identifier with no traceable request source. Restores the pre-inversion naming heuristic."}},additionalProperties:false}]},defaultOptions:[{allowInTests:true}],create(context,[options={}]){const{allowInTests=true,reportUnresolvedUrls=false}=options||{};const carriesUntrustedUrl=makeCarriesUntrustedUrl(context.sourceCode,reportUnresolvedUrls);const filename=context.filename;const isTestFile=allowInTests&&(0,eslint_devkit_1.isTestFilePath)(filename);if(isTestFile)return{};return{CallExpression(node){let isHttpCall=false;if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&HTTP_CLIENT_FUNCTIONS.has(node.callee.name)){isHttpCall=true}if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&HTTP_CLIENT_OBJECTS.has(node.callee.object.name)&&HTTP_CLIENT_METHODS.has(node.callee.property.name)){isHttpCall=true}if(!isHttpCall)return;const urlArg=node.arguments[0];if(!urlArg)return;if(hasValidationBefore(node)){return}if(!carriesUntrustedUrl(urlArg)){return}context.report({node,messageId:"ssrfVulnerability"})}}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noSsrf=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const provenance_1=require("../../utils/provenance");const HTTP_CLIENT_FUNCTIONS=new Set(["fetch","got","nodeFetch","undici"]);const HTTP_CLIENT_METHODS=new Set(["get","post","put","patch","delete","head","options","request"]);const HTTP_CLIENT_MODULES=["axios","got","superagent","request","http","https","node:http","node:https","undici","needle"];const VALIDATION_FUNCTION_NAMES=new Set(["validateUrl","validateURL","isValidUrl","isSafeUrl","isAllowed","isValidURL","checkUrl","checkURL","sanitizeUrl","sanitizeURL"]);const USER_INPUT_SUBSTRINGS=["url","endpoint","uri","href","link","target","dest","source","host","user","input","param"];function isUserInputParamName(name){const lower=name.toLowerCase();return USER_INPUT_SUBSTRINGS.some(sub=>lower.includes(sub))}const REQUEST_ROOT_NAMES=new Set(["req","request","ctx","event"]);const URL_OPTION_KEYS=new Set(["url","href","uri"]);function isRequestSourced(node,sourceCode){if((0,eslint_devkit_1.readsRequestShape)(node,sourceCode))return true;let current=node;while(current.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){current=current.object}return current!==node&&current.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&REQUEST_ROOT_NAMES.has(current.name.toLowerCase())}function makeCarriesUntrustedUrl(sourceCode,reportUnresolvedUrls){const carries=(node,depth)=>{if(depth>6)return false;const bare=(0,eslint_devkit_1.unwrapTypeSyntax)(node);if(bare!==node)return carries(bare,depth+1);switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Identifier:{const init=(0,provenance_1.bindingInit)(sourceCode,node);if(init!==void 0)return carries(init,depth+1);return reportUnresolvedUrls&&isUserInputParamName(node.name)}case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:return isRequestSourced(node,sourceCode)||carries(node.object,depth+1);case eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral:return node.expressions.some(expression=>carries(expression,depth+1));case eslint_devkit_1.AST_NODE_TYPES.CallExpression:case eslint_devkit_1.AST_NODE_TYPES.NewExpression:return node.arguments.some(argument=>carries(argument,depth+1));case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:return carries(node.left,depth+1)||carries(node.right,depth+1);case eslint_devkit_1.AST_NODE_TYPES.ObjectExpression:return node.properties.some(property=>{if(property.type!==eslint_devkit_1.AST_NODE_TYPES.Property)return false;const value=property.value;if(isRequestSourced(value,sourceCode))return true;const key=property.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?property.key.name:property.key.type===eslint_devkit_1.AST_NODE_TYPES.Literal?String(property.key.value):"";return URL_OPTION_KEYS.has(key.toLowerCase())&&carries(value,depth+1)});default:return false}};return node=>carries(node,0)}function nodeContainsValidation(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.NewExpression&&node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.name==="URL"){return true}if(node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&VALIDATION_FUNCTION_NAMES.has(node.callee.name)){return true}if(node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const method=node.callee.property.name;if(method==="includes"||method==="has"||method==="startsWith"||method==="test"||method==="some"){return true}}if(node.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression&&(node.operator==="==="||node.operator==="==")&&(node.left.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.left.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&(node.left.property.name==="hostname"||node.left.property.name==="host")||node.right.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.right.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&(node.right.property.name==="hostname"||node.right.property.name==="host"))){return true}if(node.type===eslint_devkit_1.AST_NODE_TYPES.ThrowStatement){return true}const SKIP_KEYS=new Set(["parent","range","loc","tokens","comments","start","end"]);for(const key of Object.keys(node)){if(SKIP_KEYS.has(key))continue;const value=node[key];if(value&&typeof value==="object"&&"type"in value){if(nodeContainsValidation(value))return true}if(Array.isArray(value)){for(const item of value){if(item&&typeof item==="object"&&"type"in item){if(nodeContainsValidation(item))return true}}}}return false}function hasValidationBefore(node){let current=node.parent;while(current){const parent=current.parent;if(!parent)break;if(parent.type===eslint_devkit_1.AST_NODE_TYPES.BlockStatement||parent.type===eslint_devkit_1.AST_NODE_TYPES.Program){const body=parent.body;const idx=body.indexOf(current);for(let i=idx-1;i>=0&&i>=idx-10;i--){if(nodeContainsValidation(body[i])){return true}}}if(parent.type===eslint_devkit_1.AST_NODE_TYPES.IfStatement&&parent.test){if(nodeContainsValidation(parent.test)){return true}}current=parent}return false}exports.noSsrf=(0,eslint_devkit_1.createRule)({name:"no-ssrf",meta:{type:"suggestion",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-ssrf.md",description:"Flags HTTP calls whose URL argument is a user-input-named identifier or reads off a request object \u2014 a heuristic prompt for code review, not a proof of SSRF",cwe:"CWE-918",cvss:9.1},messages:{ssrfVulnerability:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Possible SSRF \u2014 heuristic (CWE-918)",cwe:"CWE-918",description:"HTTP call whose URL argument name suggests user input. This is a naming heuristic, not data-flow analysis \u2014 review whether the URL could originate from an untrusted source at runtime.",severity:"LOW",fix:"If the URL comes from user input, validate it against an allowlist of permitted hosts before making the request.",documentationLink:"https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html"})},schema:[{type:"object",properties:{allowInTests:{type:"boolean",default:true},reportUnresolvedUrls:{type:"boolean",default:false,description:"Report a URL argument that is a user-input-named identifier with no traceable request source. Restores the pre-inversion naming heuristic."}},additionalProperties:false}]},defaultOptions:[{allowInTests:true}],create(context,[options={}]){const{allowInTests=true,reportUnresolvedUrls=false}=options||{};const carriesUntrustedUrl=makeCarriesUntrustedUrl(context.sourceCode,reportUnresolvedUrls);const filename=context.filename;const isTestFile=allowInTests&&(0,eslint_devkit_1.isTestFilePath)(filename);if(isTestFile)return{};return{CallExpression(node){let isHttpCall=false;if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&HTTP_CLIENT_FUNCTIONS.has(node.callee.name)){isHttpCall=true}if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&HTTP_CLIENT_METHODS.has(node.callee.property.name)&&HTTP_CLIENT_MODULES.some(mod=>(0,eslint_devkit_1.isModuleBinding)(node.callee.object,context.sourceCode.getScope(node),mod))){isHttpCall=true}if(!isHttpCall)return;const urlArg=node.arguments[0];if(!urlArg)return;if(hasValidationBefore(node)){return}if(!carriesUntrustedUrl(urlArg)){return}context.report({node,messageId:"ssrfVulnerability"})}}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noStaticIv=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const const_value_1=require("../../utils/const-value");const provenance_1=require("../../utils/provenance");const STATIC_IV_PATTERNS=[/^[0-9a-f]+$/i,/^[A-Za-z0-9+/]+=*$/];const CIPHERIV_FACTORIES=["createCipheriv","createDecipheriv"];const TYPED_ARRAY_CONSTRUCTORS=new Set(["Uint8Array","Uint8ClampedArray","Int8Array"]);const RANDOM_FILL_FUNCTIONS=new Set(["randomFill","randomFillSync","getRandomValues"]);function calleeMemberName(callee){if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return null;if(callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&!callee.computed){return callee.property.name}return callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof callee.property.value==="string"?callee.property.value:null}exports.noStaticIv=(0,eslint_devkit_1.createRule)({name:"no-static-iv",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-static-iv.md",description:"Disallow static or hardcoded initialization vectors (IVs)",cwe:"CWE-329",cvss:7.5},hasSuggestions:false,messages:{staticIv:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Static IV detected",cwe:"CWE-329",description:"Hardcoded IV detected. Using static IVs makes encryption deterministic, allowing attackers to detect repeated plaintexts.",severity:"HIGH",fix:"Generate IV dynamically using crypto.randomBytes(16)",documentationLink:"https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html#initialization-vectors"})},schema:[{type:"object",properties:{allowInTests:{type:"boolean",default:false,description:"Allow static IVs in test files"}},additionalProperties:false}]},defaultOptions:[{allowInTests:false}],create(context,[options={}]){const{allowInTests=false}=options;const filename=context.filename;const isTestFile=allowInTests&&(0,eslint_devkit_1.isTestFilePath)(filename);function isCipherivCall(node){const callee=node.callee;const spelled=calleeMemberName(callee)??(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?callee.name:null);if(spelled!==null&&CIPHERIV_FACTORIES.includes(spelled))return true;const scope=context.sourceCode.getScope(node);return CIPHERIV_FACTORIES.some(fn=>(0,eslint_devkit_1.isModuleBinding)(callee,scope,"crypto",[fn]))}function checkCallExpression(node){if(isTestFile)return;if(isCipherivCall(node)&&node.arguments.length>=3){const ivArg=node.arguments[2];checkIvArgument(ivArg)}}function isStaticByteArray(node){return node.type===eslint_devkit_1.AST_NODE_TYPES.ArrayExpression&&node.elements.every(el=>el?.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof el.value==="number")}function isRandomlyFilled(variable){return variable.references.some(ref=>{const parent=ref.identifier.parent;if(parent?.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return false;if(!parent.arguments.includes(ref.identifier))return false;const callee=parent.callee;const name=calleeMemberName(callee)??(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?callee.name:null);return name!==null&&RANDOM_FILL_FUNCTIONS.has(name)})}function isStaticIvValue(value,binding){const constant=(0,const_value_1.resolveConstantString)(context.sourceCode,value);if(constant!==null){const text=constant.value;return STATIC_IV_PATTERNS.some(p=>p.test(text))||text.length>=8}if(value.type===eslint_devkit_1.AST_NODE_TYPES.NewExpression){return value.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&TYPED_ARRAY_CONSTRUCTORS.has(value.callee.name)&&isStaticBufferSource(value.arguments[0],binding)}if(value.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&value.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&value.callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&value.callee.object.name==="Buffer"&&(calleeMemberName(value.callee)==="from"||calleeMemberName(value.callee)==="alloc")){return isStaticBufferSource(value.arguments[0],binding)}return false}function isStaticBufferSource(source,binding){if(source===void 0)return false;if((0,const_value_1.resolveConstantString)(context.sourceCode,source)!==null)return true;if(isStaticByteArray(source))return true;const size=(0,const_value_1.resolveConstant)(context.sourceCode,source);if(size===null||typeof size.value!=="number")return false;return binding===null||!isRandomlyFilled(binding)}function ivCandidates(identifier,variable){const constInit=(0,const_value_1.constInitializerOf)(context.sourceCode,identifier);if(constInit!==null)return[constInit];if(variable.defs.length!==1)return null;const def=variable.defs[0];if(def.type!=="Variable"||def.parent.kind==="const")return null;if(def.node.id.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return null;const candidates=def.node.init?[def.node.init]:[];for(const ref of variable.references){if(!ref.isWrite())continue;if(!ref.writeExpr)return null;if(ref.writeExpr!==def.node.init)candidates.push(ref.writeExpr)}return candidates.length>0?candidates:null}function checkIvArgument(ivArg){const unwrapped=(0,eslint_devkit_1.unwrapTypeSyntax)(ivArg);if(unwrapped.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const binding=(0,provenance_1.findVariable)(context.sourceCode,unwrapped);if(binding===null)return;const candidates=ivCandidates(unwrapped,binding);if(candidates===null)return;if(candidates.every(c=>isStaticIvValue((0,eslint_devkit_1.unwrapTypeSyntax)(c),binding))){reportStaticIv(unwrapped)}return}if(isStaticIvValue(unwrapped,null))reportStaticIv(unwrapped)}function reportStaticIv(node){context.report({node,messageId:"staticIv"})}return{CallExpression:checkCallExpression}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noStaticIv=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const const_value_1=require("../../utils/const-value");const provenance_1=require("../../utils/provenance");const STATIC_IV_PATTERNS=[/^[0-9a-f]+$/i,/^[A-Za-z0-9+/]+=*$/];const CIPHERIV_FACTORIES=["createCipheriv","createDecipheriv"];const TYPED_ARRAY_CONSTRUCTORS=new Set(["Uint8Array","Uint8ClampedArray","Int8Array"]);const RANDOM_FILL_FUNCTIONS=new Set(["randomFill","randomFillSync","getRandomValues"]);function calleeMemberName(callee){if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return null;if(callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&!callee.computed){return callee.property.name}return(0,eslint_devkit_1.staticString)(callee.property)!==null?(0,eslint_devkit_1.staticString)(callee.property):null}exports.noStaticIv=(0,eslint_devkit_1.createRule)({name:"no-static-iv",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-static-iv.md",description:"Disallow static or hardcoded initialization vectors (IVs)",cwe:"CWE-329",cvss:7.5},hasSuggestions:false,messages:{staticIv:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Static IV detected",cwe:"CWE-329",description:"Hardcoded IV detected. Using static IVs makes encryption deterministic, allowing attackers to detect repeated plaintexts.",severity:"HIGH",fix:"Generate IV dynamically using crypto.randomBytes(16)",documentationLink:"https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html#initialization-vectors"})},schema:[{type:"object",properties:{allowInTests:{type:"boolean",default:false,description:"Allow static IVs in test files"}},additionalProperties:false}]},defaultOptions:[{allowInTests:false}],create(context,[options={}]){const{allowInTests=false}=options;const filename=context.filename;const isTestFile=allowInTests&&(0,eslint_devkit_1.isTestFilePath)(filename);function isCipherivCall(node){const callee=node.callee;const spelled=calleeMemberName(callee)??(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?callee.name:null);if(spelled!==null&&CIPHERIV_FACTORIES.includes(spelled))return true;const scope=context.sourceCode.getScope(node);return CIPHERIV_FACTORIES.some(fn=>(0,eslint_devkit_1.isModuleBinding)(callee,scope,"crypto",[fn]))}function checkCallExpression(node){if(isTestFile)return;if(isCipherivCall(node)&&node.arguments.length>=3){const ivArg=node.arguments[2];checkIvArgument(ivArg)}}function isStaticByteArray(node){return node.type===eslint_devkit_1.AST_NODE_TYPES.ArrayExpression&&node.elements.every(el=>el?.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof el.value==="number")}function isRandomlyFilled(variable){return variable.references.some(ref=>{const parent=ref.identifier.parent;if(parent?.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return false;if(!parent.arguments.includes(ref.identifier))return false;const callee=parent.callee;const name=calleeMemberName(callee)??(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?callee.name:null);return name!==null&&RANDOM_FILL_FUNCTIONS.has(name)})}function isStaticIvValue(value,binding){const constant=(0,const_value_1.resolveConstantString)(context.sourceCode,value);if(constant!==null){const text=constant.value;return STATIC_IV_PATTERNS.some(p=>p.test(text))||text.length>=8}if(value.type===eslint_devkit_1.AST_NODE_TYPES.NewExpression){return value.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&TYPED_ARRAY_CONSTRUCTORS.has(value.callee.name)&&isStaticBufferSource(value.arguments[0],binding)}if(value.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&value.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&value.callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&value.callee.object.name==="Buffer"&&(calleeMemberName(value.callee)==="from"||calleeMemberName(value.callee)==="alloc")){return isStaticBufferSource(value.arguments[0],binding)}return false}function isStaticBufferSource(source,binding){if(source===void 0)return false;if((0,const_value_1.resolveConstantString)(context.sourceCode,source)!==null)return true;if(isStaticByteArray(source))return true;const size=(0,const_value_1.resolveConstant)(context.sourceCode,source);if(size===null||typeof size.value!=="number")return false;return binding===null||!isRandomlyFilled(binding)}function ivCandidates(identifier,variable){const constInit=(0,const_value_1.constInitializerOf)(context.sourceCode,identifier);if(constInit!==null)return[constInit];if(variable.defs.length!==1)return null;const def=variable.defs[0];if(def.type!=="Variable"||def.parent.kind==="const")return null;if(def.node.id.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return null;const candidates=def.node.init?[def.node.init]:[];for(const ref of variable.references){if(!ref.isWrite())continue;if(!ref.writeExpr)return null;if(ref.writeExpr!==def.node.init)candidates.push(ref.writeExpr)}return candidates.length>0?candidates:null}function checkIvArgument(ivArg){const unwrapped=(0,eslint_devkit_1.unwrapTypeSyntax)(ivArg);if(unwrapped.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const binding=(0,provenance_1.findVariable)(context.sourceCode,unwrapped);if(binding===null)return;const candidates=ivCandidates(unwrapped,binding);if(candidates===null)return;if(candidates.every(c=>isStaticIvValue((0,eslint_devkit_1.unwrapTypeSyntax)(c),binding))){reportStaticIv(unwrapped)}return}if(isStaticIvValue(unwrapped,null))reportStaticIv(unwrapped)}function reportStaticIv(node){context.report({node,messageId:"staticIv"})}return{CallExpression:checkCallExpression}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noTimingUnsafeCompare=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const provenance_1=require("../../utils/provenance");const names_1=require("../../utils/names");const DEFAULT_SECRET_PATTERNS=["token","secret","password","hash","signature","mac","hmac","digest","apiKey","api_key","api-key","auth","credential","bearer","jwt","csrf","nonce","ssn","social_security","social-security","pii","private_key","private-key","privateKey","access_token","access-token","accessToken","refresh_token","refresh-token","refreshToken","session_id","session-id","sessionId","auth_token","auth-token","authToken","encryption_key","encryption-key","encryptionKey"];function isSourceConstant(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.name==="undefined")return true;if(node.type===eslint_devkit_1.AST_NODE_TYPES.Literal)return true;if(node.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral)return node.expressions.length===0;return false}const BOOLEAN_PREDICATE_NAME=/^(?:is|has|should|can|did|was|will|does)[A-Z]/;const NAMED_CONSTANT=/^[A-Z][A-Z0-9_]*$/;const DEFAULT_UNTRUSTED_SOURCES=["req","request","ctx","event"];const NAMESPACE_NAME=/^(?:[A-Z][A-Z0-9_]*|[A-Z][a-zA-Z0-9]*)$/;function isNamedConstant(node){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return false;if(node.computed)return false;if(node.object.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;if(node.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;return NAMESPACE_NAME.test(node.object.name)&&NAMED_CONSTANT.test(node.property.name)}function memberRoot(node){let current=node;for(;;){if(current.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){current=current.object;continue}if(current.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){current=current.callee;continue}break}return current.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?current:null}function isSelfComparison(left,right){const pair=(bare,derived)=>{if(bare.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;if(derived.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;return memberRoot(derived)?.name===bare.name};return pair(left,right)||pair(right,left)}const NON_SECRET_MEMBERS=new Map([["hash",new Set(["location"])]]);const DEFAULT_NON_SECRET_WORDS=["author","authors","authored","authoring","authorship","hashtag","hashtags"];const DEFAULT_NON_SECRET_TAILS=["count","counts","limit","limits","usage","total","size","length","price","cost","quota","address","addresses","index","rank","percent","path","paths","pathname","pathnames","endpoint","endpoints","route","routes","hostname","host","port","origin","kind","kinds","type","types","flag","flags","category"];const CRYPTO_DERIVATIONS=new Set(["createHmac","createHash","createSign","pbkdf2Sync","scryptSync","hkdfSync","digest","sign","hmac"]);const RECEIVER_COMPARE_METHODS=new Set(["equals","startsWith","endsWith","localeCompare"]);const BINARY_EQUALITY_FUNCTIONS=new Set(["isEqual","isEqualWith","deepEqual","fastDeepEqual","shallowEqual"]);const SERVER_STATE_REQUEST_PROPERTIES=new Set(["session","user","locals","app","state"]);function unwrapChain(node){return node.type===eslint_devkit_1.AST_NODE_TYPES.ChainExpression?node.expression:node}function memberPropertyName(node){const property=node.property;if(!node.computed){return property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?property.name:null}return property.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof property.value==="string"?property.value:null}function isNonSecretMember(node){const propertyName=memberPropertyName(node);if(propertyName===null)return false;const receivers=NON_SECRET_MEMBERS.get(propertyName.toLowerCase());if(!receivers)return false;const owner=node.object;const ownerName=owner.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?owner.name:owner.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!owner.computed&&owner.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?owner.property.name:"";return receivers.has(ownerName.toLowerCase())}exports.noTimingUnsafeCompare=(0,eslint_devkit_1.createRule)({name:"no-timing-unsafe-compare",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-timing-unsafe-compare.md",description:"Disallow timing-unsafe comparison of secrets",cwe:"CWE-208",cvss:5.9},messages:{timingUnsafeCompare:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Timing-unsafe comparison",cwe:"CWE-208",description:"Using === to compare secrets enables timing attacks. The comparison short-circuits on first mismatch, leaking information about the secret.",severity:"HIGH",fix:"Use crypto.timingSafeEqual() for constant-time comparison",documentationLink:"https://nodejs.org/api/crypto.html#cryptotimingsafeequala-b"})},schema:[{type:"object",properties:{secretPatterns:{type:"array",items:{type:"string"},default:DEFAULT_SECRET_PATTERNS,description:"Variable name patterns that indicate secrets"},untrustedSources:{type:"array",items:{type:"string"},default:DEFAULT_UNTRUSTED_SOURCES,description:"Identifier roots treated as attacker-controlled (default: req, request, ctx, event)"},reportUnverifiedComparisons:{type:"boolean",default:false,description:"Report on a secret-looking name alone, without an attacker-controlled operand. Restores the pre-inversion behaviour."},nonSecretWords:{type:"array",items:{type:"string"},default:[...DEFAULT_NON_SECRET_WORDS],description:`Whole words that mean a secretPatterns match was a collision (default: ${DEFAULT_NON_SECRET_WORDS.join(", ")}). Replaces the list.`},nonSecretTails:{type:"array",items:{type:"string"},default:[...DEFAULT_NON_SECRET_TAILS],description:`Trailing words that make the value a measurement or location rather than a credential (default: ${DEFAULT_NON_SECRET_TAILS.join(", ")}). Replaces the list.`}},additionalProperties:false}]},defaultOptions:[{secretPatterns:DEFAULT_SECRET_PATTERNS,nonSecretWords:[...DEFAULT_NON_SECRET_WORDS],nonSecretTails:[...DEFAULT_NON_SECRET_TAILS]}],create(context,[options={}]){const{secretPatterns=DEFAULT_SECRET_PATTERNS,untrustedSources=DEFAULT_UNTRUSTED_SOURCES,reportUnverifiedComparisons=false,nonSecretWords=[...DEFAULT_NON_SECRET_WORDS],nonSecretTails=[...DEFAULT_NON_SECRET_TAILS]}=options;const nonSecretWordSet=new Set(nonSecretWords.map(word=>word.toLowerCase()));const nonSecretTailSet=new Set(nonSecretTails.map(word=>word.toLowerCase()));const sourceCode=context.sourceCode;const readsUntrusted=(0,provenance_1.makeReadsTaintSource)(sourceCode,new Set(untrustedSources.map(source=>source.toLowerCase())));const patterns=(0,eslint_devkit_1.compileUserPatterns)(secretPatterns,"i");function nameLooksSecret(name){if(BOOLEAN_PREDICATE_NAME.test(name))return false;const words=(0,names_1.identifierWords)(name);if(words.some(word=>nonSecretWordSet.has(word)))return false;if(words.length>0&&nonSecretTailSet.has(words[words.length-1])){return false}return patterns.some(p=>p.test(name))}function isCryptoDerivation(node){const bare=(0,eslint_devkit_1.unwrapTypeSyntax)(node);if(bare!==node)return isCryptoDerivation(bare);if(node.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return false;const callee=node.callee;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return CRYPTO_DERIVATIONS.has(callee.name)}if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression||callee.computed)return false;if(callee.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;if(CRYPTO_DERIVATIONS.has(callee.property.name))return true;return isCryptoDerivation(callee.object)}function isCryptoSecret(node){if(isCryptoDerivation(node))return true;const bare=(0,eslint_devkit_1.unwrapTypeSyntax)(node);if(bare.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const init=(0,provenance_1.bindingInit)(sourceCode,bare);return init!==void 0&&containsCryptoDerivation(init)}function containsCryptoDerivation(node){if(isCryptoDerivation(node))return true;if(node.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral){return node.expressions.some(expression=>containsCryptoDerivation(expression))}if(node.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression){return containsCryptoDerivation(node.left)||containsCryptoDerivation(node.right)}return false}function isServerDerived(node,depth=0){if(depth>4)return false;const bare=(0,eslint_devkit_1.unwrapTypeSyntax)(node);if(bare!==node)return isServerDerived(bare,depth+1);if(node.type===eslint_devkit_1.AST_NODE_TYPES.AwaitExpression)return true;if(containsCryptoDerivation(node))return true;if(node.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){const propertyName=memberPropertyName(node);if(propertyName!==null&&SERVER_STATE_REQUEST_PROPERTIES.has(propertyName)){return true}return isServerDerived(node.object,depth+1)}if(node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const init=(0,provenance_1.bindingInit)(sourceCode,node);return init!==void 0&&isServerDerived(init,depth+1)}if(node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){const callee=unwrapChain(node.callee);if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return false;return!readsUntrusted(callee.object)||isServerDerived(callee.object,depth+1)}return false}function isSecretIdentifier(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return nameLooksSecret(node.name)}if(node.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){if(isNonSecretMember(node))return false;const propertyName=memberPropertyName(node);if(propertyName!==null){return nameLooksSecret(propertyName)}}return false}function isResolvedConstant(node){if(isSourceConstant(node))return true;if(node.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;return(0,provenance_1.constLiteralOf)(sourceCode,node)!==void 0}function resolveLocalFunction(callee){const variable=(0,provenance_1.findVariable)(sourceCode,callee);if(!variable||variable.defs.length!==1)return null;const def=variable.defs[0];if(def.type==="FunctionName")return def.node;if(def.type!=="Variable")return null;const init=def.node.init;if(!init)return null;const bare=(0,eslint_devkit_1.unwrapTypeSyntax)(init);if(bare.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression||bare.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression){return bare}return null}function comparesLengths(node){const isLength=operand=>operand.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&memberPropertyName(operand)==="length";return isLength(node.left)||isLength(node.right)}function equalityWrapperParams(callee){const fn=resolveLocalFunction(callee);if(!fn||fn.params.length<2)return null;const paramIndex=new Map;fn.params.forEach((param,index)=>{if(param.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)paramIndex.set(param.name,index)});let found=null;const visit=node=>{if(found||!node||typeof node.type!=="string")return;if(node.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression){const isEquality=node.operator==="==="||node.operator==="!=="||node.operator==="=="||node.operator==="!=";if(isEquality&&!comparesLengths(node)){const leftIndex=paramIndex.get(memberRoot(node.left)?.name??"");const rightIndex=paramIndex.get(memberRoot(node.right)?.name??"");if(leftIndex!==void 0&&rightIndex!==void 0&&leftIndex!==rightIndex){found=[leftIndex,rightIndex];return}}}for(const[key,value]of Object.entries(node)){if(key==="parent")continue;if(Array.isArray(value)){for(const entry of value)visit(entry)}else if(value&&typeof value==="object"&&"type"in value){visit(value)}}};visit(fn.body);return found}function comparisonOperands(node){const args=node.arguments;if(args.some(argument=>argument.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement)){return null}const callee=node.callee;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){if(BINARY_EQUALITY_FUNCTIONS.has(callee.name)&&args.length===2){return[args[0],args[1]]}const wrapped=equalityWrapperParams(callee);if(!wrapped)return null;const[leftIndex,rightIndex]=wrapped;const leftArg=args[leftIndex];const rightArg=args[rightIndex];if(!leftArg||!rightArg)return null;return[leftArg,rightArg]}if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression||callee.computed)return null;if(callee.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return null;const method=callee.property.name;if(method==="compare"&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="Buffer"&&args.length===2){return[args[0],args[1]]}if(BINARY_EQUALITY_FUNCTIONS.has(method)&&args.length===2){return[args[0],args[1]]}if(RECEIVER_COMPARE_METHODS.has(method)&&args.length===1){return[callee.object,args[0]]}return null}function checkComparison(node,rawLeft,rawRight){const left=unwrapChain(rawLeft);const right=unwrapChain(rawRight);if(isResolvedConstant(left)||isResolvedConstant(right)){return}if(isNamedConstant(left)||isNamedConstant(right)){return}if(isSelfComparison(left,right)){return}const leftIsSecret=isSecretIdentifier(left)||isCryptoSecret(left);const rightIsSecret=isSecretIdentifier(right)||isCryptoSecret(right);if(leftIsSecret||rightIsSecret){if(!reportUnverifiedComparisons){const leftUntrusted=readsUntrusted(left);const rightUntrusted=readsUntrusted(right);if(leftUntrusted===rightUntrusted){if(!leftUntrusted)return;if(!isServerDerived(left)&&!isServerDerived(right))return}}context.report({node,messageId:"timingUnsafeCompare"})}}function checkBinaryExpression(node){if(node.operator!=="==="&&node.operator!=="=="&&node.operator!=="!=="&&node.operator!=="!="){return}checkComparison(node,node.left,node.right)}function checkCallExpression(node){const operands=comparisonOperands(node);if(!operands)return;checkComparison(node,operands[0],operands[1])}return{BinaryExpression:checkBinaryExpression,CallExpression:checkCallExpression}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noTimingUnsafeCompare=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const provenance_1=require("../../utils/provenance");const names_1=require("../../utils/names");const DEFAULT_SECRET_PATTERNS=["token","secret","password","hash","signature","mac","hmac","digest","apiKey","api_key","api-key","auth","credential","bearer","jwt","csrf","nonce","ssn","social_security","social-security","pii","private_key","private-key","privateKey","access_token","access-token","accessToken","refresh_token","refresh-token","refreshToken","session_id","session-id","sessionId","auth_token","auth-token","authToken","encryption_key","encryption-key","encryptionKey"];function isSourceConstant(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.name==="undefined")return true;if(node.type===eslint_devkit_1.AST_NODE_TYPES.Literal)return true;if(node.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral)return node.expressions.length===0;return false}const BOOLEAN_PREDICATE_NAME=/^(?:is|has|should|can|did|was|will|does)[A-Z]/;const NAMED_CONSTANT=/^[A-Z][A-Z0-9_]*$/;const DEFAULT_UNTRUSTED_SOURCES=["req","request","ctx","event"];const NAMESPACE_NAME=/^(?:[A-Z][A-Z0-9_]*|[A-Z][a-zA-Z0-9]*)$/;function isNamedConstant(node){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return false;if(node.computed)return false;if(node.object.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;if(node.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;return NAMESPACE_NAME.test(node.object.name)&&NAMED_CONSTANT.test(node.property.name)}function memberRoot(node){let current=node;for(;;){if(current.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){current=current.object;continue}if(current.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){current=current.callee;continue}break}return current.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?current:null}function isSelfComparison(left,right){const pair=(bare,derived)=>{if(bare.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;if(derived.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;return memberRoot(derived)?.name===bare.name};return pair(left,right)||pair(right,left)}const NON_SECRET_MEMBERS=new Map([["hash",new Set(["location"])]]);const DEFAULT_NON_SECRET_WORDS=["author","authors","authored","authoring","authorship","hashtag","hashtags"];const DEFAULT_NON_SECRET_TAILS=["count","counts","limit","limits","usage","total","size","length","price","cost","quota","address","addresses","index","rank","percent","path","paths","pathname","pathnames","endpoint","endpoints","route","routes","hostname","host","port","origin","kind","kinds","type","types","flag","flags","category"];const CRYPTO_DERIVATIONS=new Set(["createHmac","createHash","createSign","pbkdf2Sync","scryptSync","hkdfSync","digest","sign","hmac"]);const RECEIVER_COMPARE_METHODS=new Set(["equals","startsWith","endsWith","localeCompare"]);const BINARY_EQUALITY_FUNCTIONS=new Set(["isEqual","isEqualWith","deepEqual","fastDeepEqual","shallowEqual"]);const SERVER_STATE_REQUEST_PROPERTIES=new Set(["session","user","locals","app","state"]);function unwrapChain(node){return node.type===eslint_devkit_1.AST_NODE_TYPES.ChainExpression?node.expression:node}function memberPropertyName(node){const property=node.property;if(!node.computed){return property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?property.name:null}return(0,eslint_devkit_1.staticString)(property)!==null?(0,eslint_devkit_1.staticString)(property):null}function isNonSecretMember(node){const propertyName=memberPropertyName(node);if(propertyName===null)return false;const receivers=NON_SECRET_MEMBERS.get(propertyName.toLowerCase());if(!receivers)return false;const owner=node.object;const ownerName=owner.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?owner.name:owner.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!owner.computed&&owner.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?owner.property.name:"";return receivers.has(ownerName.toLowerCase())}exports.noTimingUnsafeCompare=(0,eslint_devkit_1.createRule)({name:"no-timing-unsafe-compare",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-timing-unsafe-compare.md",description:"Disallow timing-unsafe comparison of secrets",cwe:"CWE-208",cvss:5.9},messages:{timingUnsafeCompare:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Timing-unsafe comparison",cwe:"CWE-208",description:"Using === to compare secrets enables timing attacks. The comparison short-circuits on first mismatch, leaking information about the secret.",severity:"HIGH",fix:"Use crypto.timingSafeEqual() for constant-time comparison",documentationLink:"https://nodejs.org/api/crypto.html#cryptotimingsafeequala-b"})},schema:[{type:"object",properties:{secretPatterns:{type:"array",items:{type:"string"},default:DEFAULT_SECRET_PATTERNS,description:"Variable name patterns that indicate secrets"},untrustedSources:{type:"array",items:{type:"string"},default:DEFAULT_UNTRUSTED_SOURCES,description:"Identifier roots treated as attacker-controlled (default: req, request, ctx, event)"},reportUnverifiedComparisons:{type:"boolean",default:false,description:"Report on a secret-looking name alone, without an attacker-controlled operand. Restores the pre-inversion behaviour."},nonSecretWords:{type:"array",items:{type:"string"},default:[...DEFAULT_NON_SECRET_WORDS],description:`Whole words that mean a secretPatterns match was a collision (default: ${DEFAULT_NON_SECRET_WORDS.join(", ")}). Replaces the list.`},nonSecretTails:{type:"array",items:{type:"string"},default:[...DEFAULT_NON_SECRET_TAILS],description:`Trailing words that make the value a measurement or location rather than a credential (default: ${DEFAULT_NON_SECRET_TAILS.join(", ")}). Replaces the list.`}},additionalProperties:false}]},defaultOptions:[{secretPatterns:DEFAULT_SECRET_PATTERNS,nonSecretWords:[...DEFAULT_NON_SECRET_WORDS],nonSecretTails:[...DEFAULT_NON_SECRET_TAILS]}],create(context,[options={}]){const{secretPatterns=DEFAULT_SECRET_PATTERNS,untrustedSources=DEFAULT_UNTRUSTED_SOURCES,reportUnverifiedComparisons=false,nonSecretWords=[...DEFAULT_NON_SECRET_WORDS],nonSecretTails=[...DEFAULT_NON_SECRET_TAILS]}=options;const nonSecretWordSet=new Set(nonSecretWords.map(word=>word.toLowerCase()));const nonSecretTailSet=new Set(nonSecretTails.map(word=>word.toLowerCase()));const sourceCode=context.sourceCode;const readsUntrusted=(0,provenance_1.makeReadsTaintSource)(sourceCode,new Set(untrustedSources.map(source=>source.toLowerCase())));const patterns=(0,eslint_devkit_1.compileUserPatterns)(secretPatterns,"i");function nameLooksSecret(name){if(BOOLEAN_PREDICATE_NAME.test(name))return false;const words=(0,names_1.identifierWords)(name);if(words.some(word=>nonSecretWordSet.has(word)))return false;if(words.length>0&&nonSecretTailSet.has(words[words.length-1])){return false}return patterns.some(p=>p.test(name))}function isCryptoDerivation(node){const bare=(0,eslint_devkit_1.unwrapTypeSyntax)(node);if(bare!==node)return isCryptoDerivation(bare);if(node.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return false;const callee=node.callee;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return CRYPTO_DERIVATIONS.has(callee.name)}if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression||callee.computed)return false;if(callee.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;if(CRYPTO_DERIVATIONS.has(callee.property.name))return true;return isCryptoDerivation(callee.object)}function isCryptoSecret(node){if(isCryptoDerivation(node))return true;const bare=(0,eslint_devkit_1.unwrapTypeSyntax)(node);if(bare.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const init=(0,provenance_1.bindingInit)(sourceCode,bare);return init!==void 0&&containsCryptoDerivation(init)}function containsCryptoDerivation(node){if(isCryptoDerivation(node))return true;if(node.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral){return node.expressions.some(expression=>containsCryptoDerivation(expression))}if(node.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression){return containsCryptoDerivation(node.left)||containsCryptoDerivation(node.right)}return false}function isServerDerived(node,depth=0){if(depth>4)return false;const bare=(0,eslint_devkit_1.unwrapTypeSyntax)(node);if(bare!==node)return isServerDerived(bare,depth+1);if(node.type===eslint_devkit_1.AST_NODE_TYPES.AwaitExpression)return true;if(containsCryptoDerivation(node))return true;if(node.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){const propertyName=memberPropertyName(node);if(propertyName!==null&&SERVER_STATE_REQUEST_PROPERTIES.has(propertyName)){return true}return isServerDerived(node.object,depth+1)}if(node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const init=(0,provenance_1.bindingInit)(sourceCode,node);return init!==void 0&&isServerDerived(init,depth+1)}if(node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){const callee=unwrapChain(node.callee);if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return false;return!readsUntrusted(callee.object)||isServerDerived(callee.object,depth+1)}return false}function isSecretIdentifier(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return nameLooksSecret(node.name)}if(node.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){if(isNonSecretMember(node))return false;const propertyName=memberPropertyName(node);if(propertyName!==null){return nameLooksSecret(propertyName)}}return false}function isResolvedConstant(node){if(isSourceConstant(node))return true;if(node.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;return(0,provenance_1.constLiteralOf)(sourceCode,node)!==void 0}function resolveLocalFunction(callee){const variable=(0,provenance_1.findVariable)(sourceCode,callee);if(!variable||variable.defs.length!==1)return null;const def=variable.defs[0];if(def.type==="FunctionName")return def.node;if(def.type!=="Variable")return null;const init=def.node.init;if(!init)return null;const bare=(0,eslint_devkit_1.unwrapTypeSyntax)(init);if(bare.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression||bare.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression){return bare}return null}function comparesLengths(node){const isLength=operand=>operand.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&memberPropertyName(operand)==="length";return isLength(node.left)||isLength(node.right)}function equalityWrapperParams(callee){const fn=resolveLocalFunction(callee);if(!fn||fn.params.length<2)return null;const paramIndex=new Map;fn.params.forEach((param,index)=>{if(param.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)paramIndex.set(param.name,index)});let found=null;const visit=node=>{if(found||!node||typeof node.type!=="string")return;if(node.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression){const isEquality=node.operator==="==="||node.operator==="!=="||node.operator==="=="||node.operator==="!=";if(isEquality&&!comparesLengths(node)){const leftIndex=paramIndex.get(memberRoot(node.left)?.name??"");const rightIndex=paramIndex.get(memberRoot(node.right)?.name??"");if(leftIndex!==void 0&&rightIndex!==void 0&&leftIndex!==rightIndex){found=[leftIndex,rightIndex];return}}}for(const[key,value]of Object.entries(node)){if(key==="parent")continue;if(Array.isArray(value)){for(const entry of value)visit(entry)}else if(value&&typeof value==="object"&&"type"in value){visit(value)}}};visit(fn.body);return found}function comparisonOperands(node){const args=node.arguments;if(args.some(argument=>argument.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement)){return null}const callee=node.callee;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){if(BINARY_EQUALITY_FUNCTIONS.has(callee.name)&&args.length===2){return[args[0],args[1]]}const wrapped=equalityWrapperParams(callee);if(!wrapped)return null;const[leftIndex,rightIndex]=wrapped;const leftArg=args[leftIndex];const rightArg=args[rightIndex];if(!leftArg||!rightArg)return null;return[leftArg,rightArg]}if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression||callee.computed)return null;if(callee.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return null;const method=callee.property.name;if(method==="compare"&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="Buffer"&&args.length===2){return[args[0],args[1]]}if(BINARY_EQUALITY_FUNCTIONS.has(method)&&args.length===2){return[args[0],args[1]]}if(RECEIVER_COMPARE_METHODS.has(method)&&args.length===1){return[callee.object,args[0]]}return null}function checkComparison(node,rawLeft,rawRight){const left=unwrapChain(rawLeft);const right=unwrapChain(rawRight);if(isResolvedConstant(left)||isResolvedConstant(right)){return}if(isNamedConstant(left)||isNamedConstant(right)){return}if(isSelfComparison(left,right)){return}const leftIsSecret=isSecretIdentifier(left)||isCryptoSecret(left);const rightIsSecret=isSecretIdentifier(right)||isCryptoSecret(right);if(leftIsSecret||rightIsSecret){if(!reportUnverifiedComparisons){const leftUntrusted=readsUntrusted(left);const rightUntrusted=readsUntrusted(right);if(leftUntrusted===rightUntrusted){if(!leftUntrusted)return;if(!isServerDerived(left)&&!isServerDerived(right))return}}context.report({node,messageId:"timingUnsafeCompare"})}}function checkBinaryExpression(node){if(node.operator!=="==="&&node.operator!=="=="&&node.operator!=="!=="&&node.operator!=="!="){return}checkComparison(node,node.left,node.right)}function checkCallExpression(node){const operands=comparisonOperands(node);if(!operands)return;checkComparison(node,operands[0],operands[1])}return{BinaryExpression:checkBinaryExpression,CallExpression:checkCallExpression}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noUnsafeBufferAlloc=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const const_value_1=require("../../utils/const-value");const provenance_1=require("../../utils/provenance");const UNSAFE_ALLOCATORS=new Set(["allocUnsafe","allocUnsafeSlow"]);const SIZED_ALLOCATORS=new Set(["Array","Buffer","Uint8Array","Uint16Array","Uint32Array","Int8Array","Int16Array","Int32Array","Float32Array","Float64Array","BigInt64Array","BigUint64Array","ArrayBuffer","SharedArrayBuffer"]);function looksNumeric(node){switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Literal:return typeof node.value==="number";case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:return["+","-","*","/","%","<<",">>",">>>"].includes(node.operator);case eslint_devkit_1.AST_NODE_TYPES.Identifier:return COUNT_NAMES.has(node.name.toLowerCase());case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:return!node.computed&&node.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&COUNT_NAMES.has(node.property.name.toLowerCase());case eslint_devkit_1.AST_NODE_TYPES.CallExpression:{if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.object.name==="Math"){return true}const name=calleeName(node.callee);return name!==null&&/^(read|len|size|count|decode|parse)/i.test(name)}default:return false}}const COUNT_NAMES=new Set(["length","len","size","count","n","num","total","capacity","bytelength"]);const BUFFER_ALLOCATORS=new Set(["alloc","allocUnsafe","allocUnsafeSlow"]);const WIRE_NAMES=new Set(["chunk","chunks","buffer","buf","payload","frame","packet","raw","message","msg"]);const REQUEST_ROOTS=new Set(["req","request","ctx","event"]);function isWriteMethod(name){return name==="fill"||name==="set"||name.startsWith("write")}const DESTINATION_ARGUMENT_CALLS=new Set(["copy","randomFill","randomFillSync"]);const METADATA_PROPERTIES=new Set(["length","byteLength","byteOffset","buffer"]);const WRITE_WIDTHS=new Map([["writeUInt8",1],["writeInt8",1],["writeUInt16LE",2],["writeUInt16BE",2],["writeInt16LE",2],["writeInt16BE",2],["writeUInt32LE",4],["writeUInt32BE",4],["writeInt32LE",4],["writeInt32BE",4],["writeFloatLE",4],["writeFloatBE",4],["writeDoubleLE",8],["writeDoubleBE",8],["writeBigInt64LE",8],["writeBigInt64BE",8],["writeBigUInt64LE",8],["writeBigUInt64BE",8]]);const MAX_TRACKED_ALLOCATION=4096;const BUFFER_MODULES=new Set(["buffer","node:buffer"]);const CRYPTO_MODULES=new Set(["crypto","node:crypto"]);const RANDOM_FILL_CALLS=new Set(["randomFill","randomFillSync"]);function isCryptoModuleRequire(node){return node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.name==="require"&&node.arguments[0]?.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof node.arguments[0].value==="string"&&CRYPTO_MODULES.has(node.arguments[0].value)}function isBufferModuleRequire(node){return node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.name==="require"&&node.arguments[0]?.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof node.arguments[0].value==="string"&&BUFFER_MODULES.has(node.arguments[0].value)}function isBufferObject(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return node.name==="Buffer";return node.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!node.computed&&node.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.property.name==="Buffer"&&isBufferModuleRequire(node.object)}function isInsideLoop(node){let current=node.parent;while(current){if(current.type===eslint_devkit_1.AST_NODE_TYPES.ForStatement||current.type===eslint_devkit_1.AST_NODE_TYPES.ForOfStatement||current.type===eslint_devkit_1.AST_NODE_TYPES.ForInStatement||current.type===eslint_devkit_1.AST_NODE_TYPES.WhileStatement||current.type===eslint_devkit_1.AST_NODE_TYPES.DoWhileStatement){return true}current=current.parent}return false}function coversWholeBuffer(call,method){if(method==="fill")return true;const width=WRITE_WIDTHS.get(method);if(width!==void 0){const offset2=call.arguments[1];return offset2!==void 0&&offset2.type!==eslint_devkit_1.AST_NODE_TYPES.Literal&&isInsideLoop(call)}if(call.arguments.length===1)return true;const offset=call.arguments[1];return offset.type!==eslint_devkit_1.AST_NODE_TYPES.Literal&&isInsideLoop(call)}function fixedWriteSpan(call,method){const width=WRITE_WIDTHS.get(method);if(width===void 0)return null;const offset=call.arguments[1];if(offset===void 0)return{start:0,end:width};if(offset.type!==eslint_devkit_1.AST_NODE_TYPES.Literal||typeof offset.value!=="number"){return null}return{start:offset.value,end:offset.value+width}}function calleeName(callee){if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return callee.name;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!callee.computed){return callee.property.name}return null}function declaredName(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration&&node.id)return node.id.name;const parent=node.parent;if(parent?.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclarator){return parent.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?parent.id.name:null}if((parent?.type===eslint_devkit_1.AST_NODE_TYPES.MethodDefinition||parent?.type===eslint_devkit_1.AST_NODE_TYPES.Property)&&!parent.computed){const key=parent.key;return key.name}return null}exports.noUnsafeBufferAlloc=(0,eslint_devkit_1.createRule)({name:"no-unsafe-buffer-alloc",meta:{type:"problem",hasSuggestions:true,docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-unsafe-buffer-alloc.md",description:"Disallow `Buffer.allocUnsafe()` and `Buffer.allocUnsafeSlow()`, which return uninitialized memory",cwe:"CWE-908",cweJustification:"CWE-908 (Use of Uninitialized Resource) \u2014 allocUnsafe returns a view over non-zeroed heap memory; any byte not overwritten before the buffer is read or transmitted discloses prior process memory.",cvss:7.5,confidence:"high"},messages:{unsafeAlloc:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Uninitialized Buffer Allocation",cwe:"CWE-908",cvss:7.5,description:"`Buffer.allocUnsafe(size)` returns memory that has not been zeroed. Every byte not overwritten before the buffer is read or sent leaks whatever the allocator previously stored there.",severity:"HIGH",fix:"Use `Buffer.alloc(size)` (zero-filled), or keep `allocUnsafe` only where the very next statement overwrites the whole buffer \u2014 `Buffer.allocUnsafe(size).fill(0)` is accepted by this rule.",documentationLink:"https://nodejs.org/api/buffer.html#static-method-bufferallocunsafesize"}),unsafeAllocSlow:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Uninitialized Buffer Allocation (allocUnsafeSlow)",cwe:"CWE-908",cvss:7.5,description:"`Buffer.allocUnsafeSlow(size)` allocates outside the shared pool but is equally uninitialized \u2014 the returned bytes are whatever was last in that memory.",severity:"HIGH",fix:"Use `Buffer.alloc(size)`, or append `.fill(0)` to zero the allocation at the call site.",documentationLink:"https://nodejs.org/api/buffer.html#static-method-bufferallocunsafeslowsize"}),useSafeAlloc:"Replace with `Buffer.alloc()` (zero-filled).",unboundedAllocation:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Allocation Sized By Untrusted Input",cwe:"CWE-789",cvss:7.5,description:'The allocation size is read off the wire, so the peer picks it. For `new Array(n)` the hazard is a narrow band, not "large n": V8 keeps a packed backing store only up to ~33.5M elements, and a length just under that turns a 12-byte length prefix into a 229MB allocation (measured: `new Array(3e7)` = 228.9MB in 20.7ms). Past the threshold V8 switches the array to dictionary mode and the allocation costs nothing at all (`new Array(4e7)` and `new Array(1e9)` are both 0.0MB in ~0.005ms). A typed allocation \u2014 `Buffer.alloc(n)`, `new Uint8Array(n)` \u2014 has no such threshold and commits n bytes at any n.',severity:"HIGH",fix:'Clamp the length against the maximum the protocol actually permits, before allocating: `if (length > MAX_LENGTH) throw new Error("too long")`, or `new Array(Math.min(length, MAX_LENGTH))`. A guard that only rejects implausibly huge values is not a fix for `new Array` \u2014 those are the sizes V8 makes free. The damaging lengths are the plausible ones just below the packed-elements limit.',documentationLink:"https://cwe.mitre.org/data/definitions/789.html"})},schema:[]},defaultOptions:[],create(context){const sourceCode=context.sourceCode;const wireParams=new Map;const pendingAllocations=[];const pendingCallSites=[];function readsWire(node,depth=0){if(depth>10)return false;const bare=(0,eslint_devkit_1.unwrapTypeSyntax)(node);if(bare!==node)return readsWire(bare,depth+1);switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Identifier:{if(node.name==="Buffer"){const bound=(0,provenance_1.findVariable)(sourceCode,node);if(bound===null||bound.defs.length===0)return false;if(bound.defs[0].type==="ImportBinding")return false}const lower=node.name.toLowerCase();if(WIRE_NAMES.has(lower)||REQUEST_ROOTS.has(lower)){const bound=(0,provenance_1.findVariable)(sourceCode,node);const def=bound===null?void 0:bound.defs[0];if(def!==void 0&&def.type==="Variable"&&def.node.init!==null&&def.node.init!==void 0){return readsWire(def.node.init,depth+1)}return true}const owner=enclosingFunction(node);if(owner!==null){const name=declaredName(owner);const indices=name===null?void 0:wireParams.get(name);if(indices!==void 0){const index=paramIndexOf(node,owner);if(index!==null&&indices.has(index))return true}}const variable=(0,provenance_1.findVariable)(sourceCode,node);const lastWrite=(variable?.references??[]).map(reference=>reference.writeExpr).filter(write=>write!=null).filter(write=>write.range[1]<=node.range[0]).sort((a,b)=>a.range[1]-b.range[1]).at(-1);return lastWrite!==void 0&&readsWire(lastWrite,depth+1)}case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:return readsWire(node.object,depth+1);case eslint_devkit_1.AST_NODE_TYPES.NewExpression:return node.arguments.some(argument=>argument.type!==eslint_devkit_1.AST_NODE_TYPES.SpreadElement&&readsWire(argument,depth+1));case eslint_devkit_1.AST_NODE_TYPES.CallExpression:return readsWire(node.callee,depth+1)||node.arguments.some(argument=>argument.type!==eslint_devkit_1.AST_NODE_TYPES.SpreadElement&&readsWire(argument,depth+1));case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:return readsWire(node.left,depth+1)||readsWire(node.right,depth+1);default:return false}}function enclosingFunction(node){let current=node.parent;while(current){if(current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration||current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression||current.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression){return current}current=current.parent}return null}function paramIndexOf(node,owner){const params=owner.params;const index=params.findIndex(param=>param.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&param.name===node.name);return index===-1?null:index}function isClamped(size){if(size.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&size.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&size.callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&size.callee.object.name==="Math"&&size.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&size.callee.property.name==="min"){return size.arguments.some(argument=>argument.type!==eslint_devkit_1.AST_NODE_TYPES.SpreadElement&&!readsWire(argument))}if(size.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const name=size.name;let current=size.parent;while(current){if(current.type===eslint_devkit_1.AST_NODE_TYPES.IfStatement){if(mentionsInComparison(current.test,name))return true}if(current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration||current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression||current.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression){const body=current.body;if(body.type===eslint_devkit_1.AST_NODE_TYPES.BlockStatement){for(const statement of body.body){if(statement.type===eslint_devkit_1.AST_NODE_TYPES.IfStatement&&mentionsInComparison(statement.test,name)){return true}}}return false}current=current.parent}return false}function mentionsInComparison(test,name){if(test.type===eslint_devkit_1.AST_NODE_TYPES.LogicalExpression){return mentionsInComparison(test.left,name)||mentionsInComparison(test.right,name)}if(test.type!==eslint_devkit_1.AST_NODE_TYPES.BinaryExpression)return false;if(!["<","<=",">",">="].includes(test.operator))return false;const named=side=>side.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&side.name===name;return named(test.left)||named(test.right)}function allocationSize(node){const size=node.arguments[0];if(size===void 0||size.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement)return null;const callee=node.callee;if(node.type===eslint_devkit_1.AST_NODE_TYPES.NewExpression&&callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&SIZED_ALLOCATORS.has(callee.name)){return looksNumeric(size)?size:null}if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!callee.computed&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="Buffer"&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&BUFFER_ALLOCATORS.has(callee.property.name)){return size}return null}function recordCallSite(node){const name=calleeName(node.callee);if(name===null)return;node.arguments.forEach((argument,index)=>{if(argument.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement)return;if(!readsWire(argument))return;const indices=wireParams.get(name)??new Set;indices.add(index);wireParams.set(name,indices)})}function judgeAllocation(node,size){if(isClamped(size))return;if(!readsWire(size))return;context.report({node,messageId:"unboundedAllocation"})}function classifyUse(identifier){const parent=identifier.parent;if(parent.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&parent.object===identifier){if(parent.computed){const grandparent2=parent.parent;const isWrite=grandparent2.type===eslint_devkit_1.AST_NODE_TYPES.AssignmentExpression&&grandparent2.left===parent;if(!isWrite)return"read";return parent.property.type!==eslint_devkit_1.AST_NODE_TYPES.Literal&&isInsideLoop(parent)?"covering":"partial"}if(parent.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return"read";const name=parent.property.name;if(METADATA_PROPERTIES.has(name))return"metadata";const grandparent=parent.parent;if(grandparent.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression||grandparent.callee!==parent||!isWriteMethod(name)){return"read"}return coversWholeBuffer(grandparent,name)?"covering":"partial"}if(parent.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&parent.arguments[0]===identifier){const method=destinationArgumentCallee(parent.callee);if(method===null)return"read";return coversWholeBuffer(parent,method)?"covering":"partial"}return"read"}function destinationArgumentCallee(callee){if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!callee.computed&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&DESTINATION_ARGUMENT_CALLS.has(callee.property.name)){return callee.property.name}if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier||!RANDOM_FILL_CALLS.has(callee.name)){return null}const variable=(0,provenance_1.findVariable)(sourceCode,callee);if(variable===null||variable.defs.length===0)return null;const def=variable.defs[0];if(def.type==="ImportBinding"){const declaration=def.parent;return declaration.type===eslint_devkit_1.AST_NODE_TYPES.ImportDeclaration&&CRYPTO_MODULES.has(declaration.source.value)?callee.name:null}return def.type==="Variable"&&def.node.init!==null&&isCryptoModuleRequire(def.node.init)?callee.name:null}function isCoveredBeforeRead(call){const parent=call.parent;let target;let variable;if(parent.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclarator&&parent.init===call&&parent.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){target=parent.id;variable=context.sourceCode.getDeclaredVariables(parent)[0]}else if(parent.type===eslint_devkit_1.AST_NODE_TYPES.AssignmentExpression&&parent.operator==="="&&parent.right===call&&parent.left.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){target=parent.left;variable=(0,provenance_1.findVariable)(context.sourceCode,target)??void 0}if(target===void 0||variable===void 0){return false}const uses=variable.references.filter(reference=>reference.identifier!==target&&reference.identifier.range[0]>call.range[1]).sort((a,b)=>a.identifier.range[0]-b.identifier.range[0]);const covered=byteMapFor(call);let remaining=covered===null?-1:covered.length;for(const use of uses){const kind=classifyUse(use.identifier);if(kind==="metadata")continue;if(kind==="partial"){if(covered!==null){remaining-=markFixedWrite(covered,use.identifier);if(remaining===0)return true}continue}return kind==="covering"}return false}function byteMapFor(call){const argument=call.arguments[0];if(argument===void 0||argument.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement){return null}const resolved=(0,const_value_1.resolveConstant)(sourceCode,argument);if(resolved===null||typeof resolved.value!=="number")return null;const size=resolved.value;if(!Number.isInteger(size)||size<=0||size>MAX_TRACKED_ALLOCATION){return null}return new Uint8Array(size)}function markFixedWrite(covered,identifier){const member=identifier.parent;if(member.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression||member.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier){return 0}const call=member.parent;if(call.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return 0;const span=fixedWriteSpan(call,member.property.name);if(span===null||span.start<0||span.end>covered.length)return 0;let added=0;for(let index=span.start;index<span.end;index+=1){if(covered[index]===0){covered[index]=1;added+=1}}return added}function unsafeAllocator(callee){if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){if(!isBufferObject(callee.object))return null;const name=callee.computed?(0,const_value_1.resolveConstantString)(sourceCode,callee.property)?.value:callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?callee.property.name:void 0;return name!==void 0&&UNSAFE_ALLOCATORS.has(name)?name:null}if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return null;const variable=(0,provenance_1.findVariable)(sourceCode,callee);if(variable===null||variable.defs.length===0)return null;const def=variable.defs[0];if(def.type!=="Variable"||def.node.init===null)return null;if(!isBufferObject(def.node.init))return null;const property=def.name.parent;if(property.type!==eslint_devkit_1.AST_NODE_TYPES.Property||property.computed||property.key.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier){return null}return UNSAFE_ALLOCATORS.has(property.key.name)?property.key.name:null}function isFilledInPlace(node){const parent=node.parent;if(parent?.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression||parent.object!==node||parent.computed||parent.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier||parent.property.name!=="fill"){return false}const grandparent=parent.parent;return grandparent?.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&grandparent.callee===parent}return{NewExpression(node){pendingCallSites.push(node);const size=allocationSize(node);if(size!==null)pendingAllocations.push({node,size})},"Program:exit"(){for(const call of pendingCallSites)recordCallSite(call);for(const{node,size}of pendingAllocations)judgeAllocation(node,size)},CallExpression(node){pendingCallSites.push(node);const size=allocationSize(node);if(size!==null)pendingAllocations.push({node,size});const callee=node.callee;const allocator=unsafeAllocator(callee);if(allocator===null)return;if(isFilledInPlace(node))return;if(isCoveredBeforeRead(node))return;const rewritable=callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!callee.computed&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?callee.property:null;context.report({node,messageId:allocator==="allocUnsafe"?"unsafeAlloc":"unsafeAllocSlow",...rewritable===null?{}:{suggest:[{messageId:"useSafeAlloc",fix:fixer=>fixer.replaceText(rewritable,"alloc")}]}})}}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noUnsafeBufferAlloc=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const const_value_1=require("../../utils/const-value");const provenance_1=require("../../utils/provenance");const UNSAFE_ALLOCATORS=new Set(["allocUnsafe","allocUnsafeSlow"]);const SIZED_ALLOCATORS=new Set(["Array","Buffer","Uint8Array","Uint16Array","Uint32Array","Int8Array","Int16Array","Int32Array","Float32Array","Float64Array","BigInt64Array","BigUint64Array","ArrayBuffer","SharedArrayBuffer"]);function looksNumeric(node,countNames){switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Literal:return typeof node.value==="number";case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:return["+","-","*","/","%","<<",">>",">>>"].includes(node.operator);case eslint_devkit_1.AST_NODE_TYPES.Identifier:return countNames.has(node.name.toLowerCase());case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:{const prop=(0,eslint_devkit_1.propertyName)(node);return prop!==null&&countNames.has(prop.toLowerCase())}case eslint_devkit_1.AST_NODE_TYPES.CallExpression:{if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.object.name==="Math"){return true}const name=calleeName(node.callee);return name!==null&&/^(read|len|size|count|decode|parse)/i.test(name)}default:return false}}function toLowerSet(names){return new Set(names.map(n=>n.toLowerCase()))}const COUNT_NAMES=new Set(["length","len","size","count","n","num","total","capacity","bytelength"]);const BUFFER_ALLOCATORS=new Set(["alloc","allocUnsafe","allocUnsafeSlow"]);const WIRE_NAMES=new Set(["chunk","chunks","buffer","buf","payload","frame","packet","raw","message","msg"]);const REQUEST_ROOTS=new Set(["req","request","ctx","event"]);function isWriteMethod(name){return name==="fill"||name==="set"||name.startsWith("write")}const DESTINATION_ARGUMENT_CALLS=new Set(["copy","randomFill","randomFillSync"]);const METADATA_PROPERTIES=new Set(["length","byteLength","byteOffset","buffer"]);const WRITE_WIDTHS=new Map([["writeUInt8",1],["writeInt8",1],["writeUInt16LE",2],["writeUInt16BE",2],["writeInt16LE",2],["writeInt16BE",2],["writeUInt32LE",4],["writeUInt32BE",4],["writeInt32LE",4],["writeInt32BE",4],["writeFloatLE",4],["writeFloatBE",4],["writeDoubleLE",8],["writeDoubleBE",8],["writeBigInt64LE",8],["writeBigInt64BE",8],["writeBigUInt64LE",8],["writeBigUInt64BE",8]]);const MAX_TRACKED_ALLOCATION=4096;const BUFFER_MODULES=new Set(["buffer","node:buffer"]);const CRYPTO_MODULES=new Set(["crypto","node:crypto"]);const RANDOM_FILL_CALLS=new Set(["randomFill","randomFillSync"]);function isCryptoModuleRequire(node){return node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.name==="require"&&node.arguments[0]?.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof node.arguments[0].value==="string"&&CRYPTO_MODULES.has(node.arguments[0].value)}function isBufferModuleRequire(node){return node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.name==="require"&&node.arguments[0]?.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof node.arguments[0].value==="string"&&BUFFER_MODULES.has(node.arguments[0].value)}function isBufferObject(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return node.name==="Buffer";return node.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!node.computed&&node.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.property.name==="Buffer"&&isBufferModuleRequire(node.object)}function isInsideLoop(node){let current=node.parent;while(current){if(current.type===eslint_devkit_1.AST_NODE_TYPES.ForStatement||current.type===eslint_devkit_1.AST_NODE_TYPES.ForOfStatement||current.type===eslint_devkit_1.AST_NODE_TYPES.ForInStatement||current.type===eslint_devkit_1.AST_NODE_TYPES.WhileStatement||current.type===eslint_devkit_1.AST_NODE_TYPES.DoWhileStatement){return true}current=current.parent}return false}function coversWholeBuffer(call,method){if(method==="fill")return true;const width=WRITE_WIDTHS.get(method);if(width!==void 0){const offset2=call.arguments[1];return offset2!==void 0&&offset2.type!==eslint_devkit_1.AST_NODE_TYPES.Literal&&isInsideLoop(call)}if(call.arguments.length===1)return true;const offset=call.arguments[1];return offset.type!==eslint_devkit_1.AST_NODE_TYPES.Literal&&isInsideLoop(call)}function fixedWriteSpan(call,method){const width=WRITE_WIDTHS.get(method);if(width===void 0)return null;const offset=call.arguments[1];if(offset===void 0)return{start:0,end:width};if(offset.type!==eslint_devkit_1.AST_NODE_TYPES.Literal||typeof offset.value!=="number"){return null}return{start:offset.value,end:offset.value+width}}function calleeName(callee){if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return callee.name;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!callee.computed){return callee.property.name}return null}function declaredName(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration&&node.id)return node.id.name;const parent=node.parent;if(parent?.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclarator){return parent.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?parent.id.name:null}if((parent?.type===eslint_devkit_1.AST_NODE_TYPES.MethodDefinition||parent?.type===eslint_devkit_1.AST_NODE_TYPES.Property)&&!parent.computed){const key=parent.key;return key.name}return null}exports.noUnsafeBufferAlloc=(0,eslint_devkit_1.createRule)({name:"no-unsafe-buffer-alloc",meta:{type:"problem",hasSuggestions:true,docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-unsafe-buffer-alloc.md",description:"Disallow `Buffer.allocUnsafe()` and `Buffer.allocUnsafeSlow()`, which return uninitialized memory",cwe:"CWE-908",cweJustification:"CWE-908 (Use of Uninitialized Resource) \u2014 allocUnsafe returns a view over non-zeroed heap memory; any byte not overwritten before the buffer is read or transmitted discloses prior process memory.",cvss:7.5,confidence:"high"},messages:{unsafeAlloc:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Uninitialized Buffer Allocation",cwe:"CWE-908",cvss:7.5,description:"`Buffer.allocUnsafe(size)` returns memory that has not been zeroed. Every byte not overwritten before the buffer is read or sent leaks whatever the allocator previously stored there.",severity:"HIGH",fix:"Use `Buffer.alloc(size)` (zero-filled), or keep `allocUnsafe` only where the very next statement overwrites the whole buffer \u2014 `Buffer.allocUnsafe(size).fill(0)` is accepted by this rule.",documentationLink:"https://nodejs.org/api/buffer.html#static-method-bufferallocunsafesize"}),unsafeAllocSlow:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Uninitialized Buffer Allocation (allocUnsafeSlow)",cwe:"CWE-908",cvss:7.5,description:"`Buffer.allocUnsafeSlow(size)` allocates outside the shared pool but is equally uninitialized \u2014 the returned bytes are whatever was last in that memory.",severity:"HIGH",fix:"Use `Buffer.alloc(size)`, or append `.fill(0)` to zero the allocation at the call site.",documentationLink:"https://nodejs.org/api/buffer.html#static-method-bufferallocunsafeslowsize"}),useSafeAlloc:"Replace with `Buffer.alloc()` (zero-filled).",unboundedAllocation:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Allocation Sized By Untrusted Input",cwe:"CWE-789",cvss:7.5,description:'The allocation size is read off the wire, so the peer picks it. For `new Array(n)` the hazard is a narrow band, not "large n": V8 keeps a packed backing store only up to ~33.5M elements, and a length just under that turns a 12-byte length prefix into a 229MB allocation (measured: `new Array(3e7)` = 228.9MB in 20.7ms). Past the threshold V8 switches the array to dictionary mode and the allocation costs nothing at all (`new Array(4e7)` and `new Array(1e9)` are both 0.0MB in ~0.005ms). A typed allocation \u2014 `Buffer.alloc(n)`, `new Uint8Array(n)` \u2014 has no such threshold and commits n bytes at any n.',severity:"HIGH",fix:'Clamp the length against the maximum the protocol actually permits, before allocating: `if (length > MAX_LENGTH) throw new Error("too long")`, or `new Array(Math.min(length, MAX_LENGTH))`. A guard that only rejects implausibly huge values is not a fix for `new Array` \u2014 those are the sizes V8 makes free. The damaging lengths are the plausible ones just below the packed-elements limit.',documentationLink:"https://cwe.mitre.org/data/definitions/789.html"})},schema:[{type:"object",properties:{wireNames:{type:"array",items:{type:"string"},default:[...WIRE_NAMES],description:"Binding names that carry bytes off the wire. Replaces the default; pass [] to turn the name arm off."},requestRootNames:{type:"array",items:{type:"string"},default:[...REQUEST_ROOTS],description:"Root identifiers treated as a request. Replaces the default."},countNames:{type:"array",items:{type:"string"},default:[...COUNT_NAMES],description:"Identifiers that hold an allocation size rather than a payload. Replaces the default."}},additionalProperties:false}]},defaultOptions:[{}],create(context,[options={}]){const sourceCode=context.sourceCode;const vocab={wireNames:toLowerSet(options.wireNames??[...WIRE_NAMES]),requestRoots:toLowerSet(options.requestRootNames??[...REQUEST_ROOTS]),countNames:toLowerSet(options.countNames??[...COUNT_NAMES])};const wireParams=new Map;const pendingAllocations=[];const pendingCallSites=[];function readsWire(node,depth=0){if(depth>10)return false;const bare=(0,eslint_devkit_1.unwrapTypeSyntax)(node);if(bare!==node)return readsWire(bare,depth+1);switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Identifier:{if(node.name==="Buffer"){const bound=(0,provenance_1.findVariable)(sourceCode,node);if(bound===null||bound.defs.length===0)return false;if(bound.defs[0].type==="ImportBinding")return false}const lower=node.name.toLowerCase();if(vocab.wireNames.has(lower)||vocab.requestRoots.has(lower)){const bound=(0,provenance_1.findVariable)(sourceCode,node);const def=bound===null?void 0:bound.defs[0];if(def!==void 0&&def.type==="Variable"&&def.node.init!==null&&def.node.init!==void 0){return readsWire(def.node.init,depth+1)}return true}const owner=enclosingFunction(node);if(owner!==null){const name=declaredName(owner);const indices=name===null?void 0:wireParams.get(name);if(indices!==void 0){const index=paramIndexOf(node,owner);if(index!==null&&indices.has(index))return true}}const variable=(0,provenance_1.findVariable)(sourceCode,node);const lastWrite=(variable?.references??[]).map(reference=>reference.writeExpr).filter(write=>write!=null).filter(write=>write.range[1]<=node.range[0]).sort((a,b)=>a.range[1]-b.range[1]).at(-1);return lastWrite!==void 0&&readsWire(lastWrite,depth+1)}case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:if((0,eslint_devkit_1.readsRequestShape)(node,sourceCode))return true;return readsWire(node.object,depth+1);case eslint_devkit_1.AST_NODE_TYPES.NewExpression:return node.arguments.some(argument=>argument.type!==eslint_devkit_1.AST_NODE_TYPES.SpreadElement&&readsWire(argument,depth+1));case eslint_devkit_1.AST_NODE_TYPES.CallExpression:return readsWire(node.callee,depth+1)||node.arguments.some(argument=>argument.type!==eslint_devkit_1.AST_NODE_TYPES.SpreadElement&&readsWire(argument,depth+1));case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:return readsWire(node.left,depth+1)||readsWire(node.right,depth+1);default:return false}}function enclosingFunction(node){let current=node.parent;while(current){if(current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration||current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression||current.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression){return current}current=current.parent}return null}function paramIndexOf(node,owner){const params=owner.params;const index=params.findIndex(param=>param.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&param.name===node.name);return index===-1?null:index}function isClamped(size){if(size.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&size.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&size.callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&size.callee.object.name==="Math"&&size.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&size.callee.property.name==="min"){return size.arguments.some(argument=>argument.type!==eslint_devkit_1.AST_NODE_TYPES.SpreadElement&&!readsWire(argument))}if(size.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const name=size.name;let current=size.parent;while(current){if(current.type===eslint_devkit_1.AST_NODE_TYPES.IfStatement){if(mentionsInComparison(current.test,name))return true}if(current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration||current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression||current.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression){const body=current.body;if(body.type===eslint_devkit_1.AST_NODE_TYPES.BlockStatement){for(const statement of body.body){if(statement.type===eslint_devkit_1.AST_NODE_TYPES.IfStatement&&mentionsInComparison(statement.test,name)){return true}}}return false}current=current.parent}return false}function mentionsInComparison(test,name){if(test.type===eslint_devkit_1.AST_NODE_TYPES.LogicalExpression){return mentionsInComparison(test.left,name)||mentionsInComparison(test.right,name)}if(test.type!==eslint_devkit_1.AST_NODE_TYPES.BinaryExpression)return false;if(!["<","<=",">",">="].includes(test.operator))return false;const named=side=>side.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&side.name===name;return named(test.left)||named(test.right)}function allocationSize(node){const size=node.arguments[0];if(size===void 0||size.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement)return null;const callee=node.callee;if(node.type===eslint_devkit_1.AST_NODE_TYPES.NewExpression&&callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&SIZED_ALLOCATORS.has(callee.name)){return looksNumeric(size,vocab.countNames)?size:null}if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!callee.computed&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="Buffer"&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&BUFFER_ALLOCATORS.has(callee.property.name)){return size}return null}function recordCallSite(node){const name=calleeName(node.callee);if(name===null)return;node.arguments.forEach((argument,index)=>{if(argument.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement)return;if(!readsWire(argument))return;const indices=wireParams.get(name)??new Set;indices.add(index);wireParams.set(name,indices)})}function judgeAllocation(node,size){if(isClamped(size))return;if(!readsWire(size))return;context.report({node,messageId:"unboundedAllocation"})}function classifyUse(identifier){const parent=identifier.parent;if(parent.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&parent.object===identifier){if(parent.computed){const grandparent2=parent.parent;const isWrite=grandparent2.type===eslint_devkit_1.AST_NODE_TYPES.AssignmentExpression&&grandparent2.left===parent;if(!isWrite)return"read";return parent.property.type!==eslint_devkit_1.AST_NODE_TYPES.Literal&&isInsideLoop(parent)?"covering":"partial"}if(parent.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return"read";const name=parent.property.name;if(METADATA_PROPERTIES.has(name))return"metadata";const grandparent=parent.parent;if(grandparent.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression||grandparent.callee!==parent||!isWriteMethod(name)){return"read"}return coversWholeBuffer(grandparent,name)?"covering":"partial"}if(parent.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&parent.arguments[0]===identifier){const method=destinationArgumentCallee(parent.callee);if(method===null)return"read";return coversWholeBuffer(parent,method)?"covering":"partial"}return"read"}function destinationArgumentCallee(callee){if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!callee.computed&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&DESTINATION_ARGUMENT_CALLS.has(callee.property.name)){return callee.property.name}if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier||!RANDOM_FILL_CALLS.has(callee.name)){return null}const variable=(0,provenance_1.findVariable)(sourceCode,callee);if(variable===null||variable.defs.length===0)return null;const def=variable.defs[0];if(def.type==="ImportBinding"){const declaration=def.parent;return declaration.type===eslint_devkit_1.AST_NODE_TYPES.ImportDeclaration&&CRYPTO_MODULES.has(declaration.source.value)?callee.name:null}return def.type==="Variable"&&def.node.init!==null&&isCryptoModuleRequire(def.node.init)?callee.name:null}function isCoveredBeforeRead(call){const parent=call.parent;let target;let variable;if(parent.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclarator&&parent.init===call&&parent.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){target=parent.id;variable=context.sourceCode.getDeclaredVariables(parent)[0]}else if(parent.type===eslint_devkit_1.AST_NODE_TYPES.AssignmentExpression&&parent.operator==="="&&parent.right===call&&parent.left.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){target=parent.left;variable=(0,provenance_1.findVariable)(context.sourceCode,target)??void 0}if(target===void 0||variable===void 0){return false}const uses=variable.references.filter(reference=>reference.identifier!==target&&reference.identifier.range[0]>call.range[1]).sort((a,b)=>a.identifier.range[0]-b.identifier.range[0]);const covered=byteMapFor(call);let remaining=covered===null?-1:covered.length;for(const use of uses){const kind=classifyUse(use.identifier);if(kind==="metadata")continue;if(kind==="partial"){if(covered!==null){remaining-=markFixedWrite(covered,use.identifier);if(remaining===0)return true}continue}return kind==="covering"}return false}function byteMapFor(call){const argument=call.arguments[0];if(argument===void 0||argument.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement){return null}const resolved=(0,const_value_1.resolveConstant)(sourceCode,argument);if(resolved===null||typeof resolved.value!=="number")return null;const size=resolved.value;if(!Number.isInteger(size)||size<=0||size>MAX_TRACKED_ALLOCATION){return null}return new Uint8Array(size)}function markFixedWrite(covered,identifier){const member=identifier.parent;if(member.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression||member.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier){return 0}const call=member.parent;if(call.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return 0;const span=fixedWriteSpan(call,member.property.name);if(span===null||span.start<0||span.end>covered.length)return 0;let added=0;for(let index=span.start;index<span.end;index+=1){if(covered[index]===0){covered[index]=1;added+=1}}return added}function unsafeAllocator(callee){if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){if(!isBufferObject(callee.object))return null;const name=callee.computed?(0,const_value_1.resolveConstantString)(sourceCode,callee.property)?.value:callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?callee.property.name:void 0;return name!==void 0&&UNSAFE_ALLOCATORS.has(name)?name:null}if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return null;const variable=(0,provenance_1.findVariable)(sourceCode,callee);if(variable===null||variable.defs.length===0)return null;const def=variable.defs[0];if(def.type!=="Variable"||def.node.init===null)return null;if(!isBufferObject(def.node.init))return null;const property=def.name.parent;if(property.type!==eslint_devkit_1.AST_NODE_TYPES.Property||property.computed||property.key.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier){return null}return UNSAFE_ALLOCATORS.has(property.key.name)?property.key.name:null}function isFilledInPlace(node){const parent=node.parent;if(parent?.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression||parent.object!==node||parent.computed||parent.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier||parent.property.name!=="fill"){return false}const grandparent=parent.parent;return grandparent?.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&grandparent.callee===parent}return{NewExpression(node){pendingCallSites.push(node);const size=allocationSize(node);if(size!==null)pendingAllocations.push({node,size})},"Program:exit"(){for(const call of pendingCallSites)recordCallSite(call);for(const{node,size}of pendingAllocations)judgeAllocation(node,size)},CallExpression(node){pendingCallSites.push(node);const size=allocationSize(node);if(size!==null)pendingAllocations.push({node,size});const callee=node.callee;const allocator=unsafeAllocator(callee);if(allocator===null)return;if(isFilledInPlace(node))return;if(isCoveredBeforeRead(node))return;const rewritable=callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!callee.computed&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?callee.property:null;context.report({node,messageId:allocator==="allocUnsafe"?"unsafeAlloc":"unsafeAllocSlow",...rewritable===null?{}:{suggest:[{messageId:"useSafeAlloc",fix:fixer=>fixer.replaceText(rewritable,"alloc")}]}})}}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noZipSlip=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");const AMBIGUOUS_EXTRACTORS=new Set(["extract","extractAll","unzip","untar"]);const DEFAULT_ARCHIVE_MODULES=["adm-zip","unzipper","yauzl","yazl","tar","tar-fs","tar-stream","extract-zip","node-stream-zip","jszip","archiver","decompress","unzip-stream","zip-stream","gunzip-maybe","7zip-min","node-7z"];const containsPathTraversal=pathText=>{return/\.\.\//.test(pathText)||/\.\.\\/.test(pathText)||pathText.startsWith("..")||/\/\.\./.test(pathText)};const isDangerousDestination=destText=>{if(destText.startsWith("/tmp")||destText.includes("os.tmpdir")||destText.includes("TMPDIR")){return false}return destText.includes("/var")||destText.includes("/usr")||destText.includes("/etc")||destText.includes("/root")||destText.includes("/home")||destText.includes("C:\\Windows")||destText.includes("C:\\Program Files")||destText.includes("C:\\Users")};exports.noZipSlip=(0,eslint_devkit_1.createRule)({name:"no-zip-slip",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-zip-slip.md",description:"Detects zip slip/archive extraction vulnerabilities",cwe:"CWE-22"},messages:{unsafeArchiveExtraction:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.SECURITY,issueName:"Unsafe Archive Extraction",cwe:"CWE-22",description:"Archive extraction without path validation",severity:"HIGH",fix:"Use safe extraction libraries or validate all paths",documentationLink:"https://snyk.io/research/zip-slip-vulnerability"}),pathTraversalInArchive:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.SECURITY,issueName:"Path Traversal in Archive",cwe:"CWE-22",description:"Archive contains path traversal sequences",severity:"CRITICAL",fix:"Reject archives with path traversal or sanitize paths",documentationLink:"https://cwe.mitre.org/data/definitions/22.html"}),unvalidatedArchivePath:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.SECURITY,issueName:"Unvalidated Archive Path",cwe:"CWE-22",description:"Archive entry path used without validation",severity:"HIGH",fix:"Validate paths before extraction",documentationLink:"https://snyk.io/research/zip-slip-vulnerability"}),dangerousArchiveDestination:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.SECURITY,issueName:"Dangerous Archive Destination",cwe:"CWE-22",description:"Archive extracted to sensitive location",severity:"MEDIUM",fix:"Extract to safe temporary directory",documentationLink:"https://cwe.mitre.org/data/definitions/22.html"})},schema:[{type:"object",properties:{archiveFunctions:{type:"array",items:{type:"string"},default:["extract","extractAll","extractAllTo","unzip","untar","extractArchive"]},pathValidationFunctions:{type:"array",items:{type:"string"},default:["validatePath","sanitizePath","checkPath","safePath"]},safeLibraries:{type:"array",items:{type:"string"},default:["yauzl","safe-archive-extract","tar-stream","unzipper"]},archiveModules:{type:"array",items:{type:"string"},default:DEFAULT_ARCHIVE_MODULES,description:"Module specifiers that mean this file works with archives"},reportWithoutArchiveContext:{type:"boolean",default:false,description:"Report entry-name and traversal shapes in files with no archive. Restores the pre-inversion behaviour."}},additionalProperties:false}]},defaultOptions:[{archiveFunctions:["extract","extractAll","extractAllTo","unzip","untar","extractArchive"],pathValidationFunctions:["validatePath","sanitizePath","checkPath","safePath"],safeLibraries:["yauzl","safe-archive-extract","tar-stream","unzipper"]}],create(context){const options=context.options[0]||{};const{archiveFunctions=["extract","extractAll","extractAllTo","unzip","untar","extractArchive"],pathValidationFunctions=["validatePath","sanitizePath","checkPath","safePath"],safeLibraries=["yauzl","safe-archive-extract","tar-stream","unzipper"],archiveModules=DEFAULT_ARCHIVE_MODULES,reportWithoutArchiveContext=false}=options;const filename=context.filename;const archiveModuleSet=new Set(archiveModules.map(name=>name.toLowerCase()));let hasArchiveContext=reportWithoutArchiveContext;const isArchiveModule=specifier=>typeof specifier==="string"&&archiveModuleSet.has(specifier.toLowerCase());const ARCHIVE_NAME=/zip|tarball|archive|gzip|gunzip|untar|tarstream|\btar\b/i;const namesArchive=node=>{if(node.type==="Identifier")return ARCHIVE_NAME.test(node.name);if(node.type==="MemberExpression"){return namesArchive(node.object)||node.property.type==="Identifier"&&ARCHIVE_NAME.test(node.property.name)}return false};const pending=[];const isArchiveExtraction=node=>{const callee=node.callee;if(callee.type==="MemberExpression"&&callee.property.type==="Identifier"&&archiveFunctions.includes(callee.property.name)){if(!AMBIGUOUS_EXTRACTORS.has(callee.property.name))return true;return namesArchive(callee.object)}if(callee.type==="Identifier"&&archiveFunctions.includes(callee.name)){return true}return false};const isPathValidated=pathNode=>{let current=pathNode;while(current){if(current.type==="CallExpression"&&current.callee.type==="Identifier"&&pathValidationFunctions.includes(current.callee.name)){return true}if(current.type==="CallExpression"&&current.callee.type==="MemberExpression"&&current.callee.object.type==="Identifier"&&current.callee.object.name==="path"&&current.callee.property.type==="Identifier"&&current.callee.property.name==="basename"){return true}if(current.type==="IfStatement"){const test=current.test;if(test.type==="CallExpression"&&test.callee.type==="MemberExpression"&&test.callee.property.type==="Identifier"&&test.callee.property.name==="startsWith"){return true}if(test.type==="UnaryExpression"&&test.operator==="!"&&test.argument.type==="CallExpression"&&test.argument.callee.type==="MemberExpression"&&test.argument.callee.property.type==="Identifier"&&test.argument.callee.property.name==="startsWith"){return true}if(test.type==="CallExpression"&&test.callee.type==="MemberExpression"&&test.callee.property.type==="Identifier"&&test.callee.property.name==="includes"){return true}}current=current.parent}return false};const isSafeLibrary=node=>{const callee=node.callee;if(callee.type==="MemberExpression"&&callee.object.type==="Identifier"&&safeLibraries.includes(callee.object.name)){return true}if(callee.type==="Identifier"){const name=callee.name.toLowerCase();if(name==="extract"||name==="unzipper"||safeLibraries.some(lib=>name.includes(lib.toLowerCase()))){return true}}return false};return{ImportDeclaration(node){if(isArchiveModule(node.source.value))hasArchiveContext=true},NewExpression(node){if(namesArchive(node.callee))hasArchiveContext=true},"Program:exit"(){for(const report of pending)report()},CallExpression(node){if(node.callee.type==="Identifier"&&node.callee.name==="require"&&node.arguments[0]?.type==="Literal"&&isArchiveModule(node.arguments[0].value)){hasArchiveContext=true}if(namesArchive(node.callee))hasArchiveContext=true;if(isArchiveExtraction(node)&&!isSafeLibrary(node)){const sourceCode=context.sourceCode;let hasSafeAnnotation=false;const allComments=sourceCode.getAllComments();for(const comment of allComments){if(comment.type==="Block"&&comment.value.includes("@safe")){hasSafeAnnotation=true;break}}if(hasSafeAnnotation){return}const args=node.arguments;let destArg;if(node.callee.type==="MemberExpression"&&node.callee.property.type==="Identifier"){const methodName=node.callee.property.name;if(["extractAllTo","unzip"].includes(methodName)){destArg=args[0]}else{destArg=args.length>=2?args[1]:void 0}}else{destArg=args.length>=2?args[1]:void 0}const destText=destArg&&destArg.type==="Literal"&&typeof destArg.value==="string"?destArg.value:"";const isDestDangerous=isDangerousDestination(destText);const isMethodCall=node.callee.type==="MemberExpression";if(isMethodCall){const isSafeRelativePath=destText.startsWith("./")||destText.startsWith("../");if(!isSafeRelativePath){context.report({node,messageId:"unsafeArchiveExtraction",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}if(isDestDangerous&&destArg){context.report({node:destArg,messageId:"dangerousArchiveDestination",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}}else{if(isDestDangerous){context.report({node,messageId:"dangerousArchiveDestination",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}else{context.report({node,messageId:"unsafeArchiveExtraction",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}}}const callee=node.callee;if(callee.type==="MemberExpression"&&callee.property.type==="Identifier"&&["join","resolve","relative","normalize"].includes(callee.property.name)){const args=node.arguments;for(const arg of args){if(arg.type==="MemberExpression"&&arg.property.type==="Identifier"&&["name","path","fileName","entryName","relativePath","filename","pathname"].includes(arg.property.name)){if(!isPathValidated(arg)){pending.push(()=>{if(!hasArchiveContext)return;context.report({node:arg,messageId:"unvalidatedArchivePath",data:{filePath:filename,line:String(node.loc?.start.line??0)}})})}}}}},Literal(node){if(typeof node.value!=="string"){return}const text=node.value;if(!(text.includes("/")||text.includes("\\"))||!containsPathTraversal(text)){return}let current=node;while(current){if(current.type==="CallExpression"&&isArchiveExtraction(current)){context.report({node,messageId:"pathTraversalInArchive",data:{filePath:filename,line:String(node.loc?.start.line??0)}});return}current=current.parent}},VariableDeclarator(node){if(!node.init||node.id.type!=="Identifier"){return}const varName=node.id.name.toLowerCase();if(varName.includes("entry")||varName.includes("file")||varName.includes("path")){if(node.init.type==="MemberExpression"&&node.init.property.type==="Identifier"&&["name","path"].includes(node.init.property.name)){}}}}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noZipSlip=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");const AMBIGUOUS_EXTRACTORS=new Set(["extract","extractAll","unzip","untar"]);const DEFAULT_ARCHIVE_ENTRY_FIELDS=["name","path","fileName","entryName","filename"];const DEFAULT_ARCHIVE_MODULES=["adm-zip","unzipper","yauzl","yazl","tar","tar-fs","tar-stream","extract-zip","node-stream-zip","jszip","archiver","decompress","unzip-stream","zip-stream","gunzip-maybe","7zip-min","node-7z"];const containsPathTraversal=pathText=>{return/\.\.\//.test(pathText)||/\.\.\\/.test(pathText)||pathText.startsWith("..")||/\/\.\./.test(pathText)};const isDangerousDestination=destText=>{if(destText.startsWith("/tmp")||destText.includes("os.tmpdir")||destText.includes("TMPDIR")){return false}return destText.includes("/var")||destText.includes("/usr")||destText.includes("/etc")||destText.includes("/root")||destText.includes("/home")||destText.includes("C:\\Windows")||destText.includes("C:\\Program Files")||destText.includes("C:\\Users")};exports.noZipSlip=(0,eslint_devkit_1.createRule)({name:"no-zip-slip",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-zip-slip.md",description:"Detects zip slip/archive extraction vulnerabilities",cwe:"CWE-22"},messages:{unsafeArchiveExtraction:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.SECURITY,issueName:"Unsafe Archive Extraction",cwe:"CWE-22",description:"Archive extraction without path validation",severity:"HIGH",fix:"Use safe extraction libraries or validate all paths",documentationLink:"https://snyk.io/research/zip-slip-vulnerability"}),pathTraversalInArchive:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.SECURITY,issueName:"Path Traversal in Archive",cwe:"CWE-22",description:"Archive contains path traversal sequences",severity:"CRITICAL",fix:"Reject archives with path traversal or sanitize paths",documentationLink:"https://cwe.mitre.org/data/definitions/22.html"}),unvalidatedArchivePath:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.SECURITY,issueName:"Unvalidated Archive Path",cwe:"CWE-22",description:"Archive entry path used without validation",severity:"HIGH",fix:"Validate paths before extraction",documentationLink:"https://snyk.io/research/zip-slip-vulnerability"}),dangerousArchiveDestination:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.SECURITY,issueName:"Dangerous Archive Destination",cwe:"CWE-22",description:"Archive extracted to sensitive location",severity:"MEDIUM",fix:"Extract to safe temporary directory",documentationLink:"https://cwe.mitre.org/data/definitions/22.html"})},schema:[{type:"object",properties:{archiveFunctions:{type:"array",items:{type:"string"},default:["extract","extractAll","extractAllTo","unzip","untar","extractArchive"]},pathValidationFunctions:{type:"array",items:{type:"string"},default:["validatePath","sanitizePath","checkPath","safePath"]},safeLibraries:{type:"array",items:{type:"string"},default:["yauzl","safe-archive-extract","tar-stream","unzipper"]},archiveEntryFields:{type:"array",items:{type:"string"},default:[...DEFAULT_ARCHIVE_ENTRY_FIELDS],description:"Property names an archive entry exposes its path on. Replaces the default."},archiveModules:{type:"array",items:{type:"string"},default:DEFAULT_ARCHIVE_MODULES,description:"Module specifiers that mean this file works with archives"},reportWithoutArchiveContext:{type:"boolean",default:false,description:"Report entry-name and traversal shapes in files with no archive. Restores the pre-inversion behaviour."}},additionalProperties:false}]},defaultOptions:[{archiveFunctions:["extract","extractAll","extractAllTo","unzip","untar","extractArchive"],pathValidationFunctions:["validatePath","sanitizePath","checkPath","safePath"],safeLibraries:["yauzl","safe-archive-extract","tar-stream","unzipper"]}],create(context){const options=context.options[0]||{};const archiveEntryFields=new Set(options.archiveEntryFields??DEFAULT_ARCHIVE_ENTRY_FIELDS);const{archiveFunctions=["extract","extractAll","extractAllTo","unzip","untar","extractArchive"],pathValidationFunctions=["validatePath","sanitizePath","checkPath","safePath"],safeLibraries=["yauzl","safe-archive-extract","tar-stream","unzipper"],archiveModules=DEFAULT_ARCHIVE_MODULES,reportWithoutArchiveContext=false}=options;const filename=context.filename;const archiveModuleSet=new Set(archiveModules.map(name=>name.toLowerCase()));let hasArchiveContext=reportWithoutArchiveContext;const isArchiveModule=specifier=>typeof specifier==="string"&&archiveModuleSet.has(specifier.toLowerCase());const ARCHIVE_NAME=/zip|tarball|archive|gzip|gunzip|untar|tarstream|\btar\b/i;const namesArchive=node=>{if(node.type==="Identifier")return ARCHIVE_NAME.test(node.name);if(node.type==="MemberExpression"){return namesArchive(node.object)||node.property.type==="Identifier"&&ARCHIVE_NAME.test(node.property.name)}return false};const pending=[];const isArchiveExtraction=node=>{const callee=node.callee;if(callee.type==="MemberExpression"&&callee.property.type==="Identifier"&&archiveFunctions.includes(callee.property.name)){if(!AMBIGUOUS_EXTRACTORS.has(callee.property.name))return true;return namesArchive(callee.object)}if(callee.type==="Identifier"&&archiveFunctions.includes(callee.name)){return true}return false};const isPathValidated=pathNode=>{let current=pathNode;while(current){if(current.type==="CallExpression"&&current.callee.type==="Identifier"&&pathValidationFunctions.includes(current.callee.name)){return true}if(current.type==="CallExpression"&&current.callee.type==="MemberExpression"&&current.callee.object.type==="Identifier"&&current.callee.object.name==="path"&&current.callee.property.type==="Identifier"&&current.callee.property.name==="basename"){return true}if(current.type==="IfStatement"){const test=current.test;if(test.type==="CallExpression"&&test.callee.type==="MemberExpression"&&test.callee.property.type==="Identifier"&&test.callee.property.name==="startsWith"){return true}if(test.type==="UnaryExpression"&&test.operator==="!"&&test.argument.type==="CallExpression"&&test.argument.callee.type==="MemberExpression"&&test.argument.callee.property.type==="Identifier"&&test.argument.callee.property.name==="startsWith"){return true}if(test.type==="CallExpression"&&test.callee.type==="MemberExpression"&&test.callee.property.type==="Identifier"&&test.callee.property.name==="includes"){return true}}current=current.parent}return false};const isSafeLibrary=node=>{const callee=node.callee;if(callee.type==="MemberExpression"&&callee.object.type==="Identifier"&&safeLibraries.includes(callee.object.name)){return true}if(callee.type==="Identifier"){const name=callee.name.toLowerCase();if(name==="extract"||name==="unzipper"||safeLibraries.some(lib=>name.includes(lib.toLowerCase()))){return true}}return false};return{ImportDeclaration(node){if(isArchiveModule(node.source.value))hasArchiveContext=true},NewExpression(node){if(namesArchive(node.callee))hasArchiveContext=true},"Program:exit"(){for(const report of pending)report()},CallExpression(node){if(node.callee.type==="Identifier"&&node.callee.name==="require"&&node.arguments[0]?.type==="Literal"&&isArchiveModule(node.arguments[0].value)){hasArchiveContext=true}if(namesArchive(node.callee))hasArchiveContext=true;if(isArchiveExtraction(node)&&!isSafeLibrary(node)){const sourceCode=context.sourceCode;let hasSafeAnnotation=false;const allComments=sourceCode.getAllComments();for(const comment of allComments){if(comment.type==="Block"&&comment.value.includes("@safe")){hasSafeAnnotation=true;break}}if(hasSafeAnnotation){return}const args=node.arguments;let destArg;if(node.callee.type==="MemberExpression"&&node.callee.property.type==="Identifier"){const methodName=node.callee.property.name;if(["extractAllTo","unzip"].includes(methodName)){destArg=args[0]}else{destArg=args.length>=2?args[1]:void 0}}else{destArg=args.length>=2?args[1]:void 0}const destText=(destArg===void 0?null:(0,eslint_devkit_1.staticString)(destArg))??"";const isDestDangerous=isDangerousDestination(destText);const isMethodCall=node.callee.type==="MemberExpression";if(isMethodCall){const isSafeRelativePath=destText.startsWith("./")||destText.startsWith("../");if(!isSafeRelativePath){context.report({node,messageId:"unsafeArchiveExtraction",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}if(isDestDangerous&&destArg){context.report({node:destArg,messageId:"dangerousArchiveDestination",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}}else{if(isDestDangerous){context.report({node,messageId:"dangerousArchiveDestination",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}else{context.report({node,messageId:"unsafeArchiveExtraction",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}}}const callee=node.callee;if(callee.type==="MemberExpression"&&callee.property.type==="Identifier"&&["join","resolve","relative","normalize"].includes(callee.property.name)){const args=node.arguments;for(const arg of args){if(arg.type==="MemberExpression"&&arg.property.type==="Identifier"&&archiveEntryFields.has(arg.property.name)){if(!isPathValidated(arg)){pending.push(()=>{if(!hasArchiveContext)return;context.report({node:arg,messageId:"unvalidatedArchivePath",data:{filePath:filename,line:String(node.loc?.start.line??0)}})})}}}}},Literal(node){if(typeof node.value!=="string"){return}const text=node.value;if(!(text.includes("/")||text.includes("\\"))||!containsPathTraversal(text)){return}let current=node;while(current){if(current.type==="CallExpression"&&isArchiveExtraction(current)){context.report({node,messageId:"pathTraversalInArchive",data:{filePath:filename,line:String(node.loc?.start.line??0)}});return}current=current.parent}}}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireSecureDeletion=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const const_value_1=require("../../utils/const-value");const SENSITIVE_PROPERTY_NAMES=["password","passwd","pwd","passphrase","secret","token","jwt","bearer","credential","api key","secret key","private key","signing key","encryption key","access key","session id","ssn","credit card","card number","cvv"];function isSensitiveName(name,phrases){const words=(0,eslint_devkit_1.identifierWords)(name);if(words.length===0)return false;return phrases.some(phrase=>{const needle=(0,eslint_devkit_1.identifierWords)(phrase);if(needle.length===0||needle.length>words.length)return false;const start=words.length-needle.length;return needle.every((word,offset)=>words[start+offset]===word)})}exports.requireSecureDeletion=(0,eslint_devkit_1.createRule)({name:"require-secure-deletion",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/require-secure-deletion.md",description:"Require secure data deletion patterns",cwe:"CWE-459",cvss:5.3},messages:{violationDetected:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Incomplete Secret Cleanup",cwe:"CWE-459",description:"`delete` on the sensitive property `{{property}}` unbinds it without scrubbing the value",severity:"MEDIUM",fix:"Overwrite the value before deleting it (obj.{{property}} = undefined, or zero-fill the Buffer), and make sure no copy of the object was spread, logged, or serialised first",documentationLink:"https://cwe.mitre.org/data/definitions/459.html"})},schema:[{type:"object",properties:{additionalSensitiveProperties:{type:"array",items:{type:"string"},default:[],description:'Extra sensitive property names, matched as whole words at the END of the name. "pin code", "pin_code" and "pinCode" all match a property called pinCode; "pincode" does not.'}},additionalProperties:false}]},defaultOptions:[{additionalSensitiveProperties:[]}],create(context,[options={}]){const{additionalSensitiveProperties=[]}=options;const phrases=[...SENSITIVE_PROPERTY_NAMES,...additionalSensitiveProperties];function deletedPropertyName(node){const argument=node.type===eslint_devkit_1.AST_NODE_TYPES.ChainExpression?node.expression:node;if(argument.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return void 0;const property=argument.property;if(!argument.computed&&property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return property.name;const resolved=(0,const_value_1.resolveConstantString)(context.sourceCode,property);return resolved?.value}function reportIfSensitive(node,property){if(!property)return;if(!isSensitiveName(property,phrases))return;context.report({node,messageId:"violationDetected",data:{property}})}return{UnaryExpression(node){if(node.operator!=="delete")return;reportIfSensitive(node,deletedPropertyName(node.argument))},CallExpression(node){const callee=node.callee;if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression||callee.computed||callee.object.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier||callee.object.name!=="Reflect"||callee.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier||callee.property.name!=="deleteProperty"){return}const key=node.arguments[1];if(!key)return;reportIfSensitive(node,(0,const_value_1.resolveConstantString)(context.sourceCode,key)?.value)}}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireSecureDeletion=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const const_value_1=require("../../utils/const-value");const SENSITIVE_PROPERTY_NAMES=["password","passwd","pwd","passphrase","secret","token","jwt","bearer","credential","api key","secret key","private key","signing key","encryption key","access key","session id","ssn","credit card","card number","cvv"];function isSensitiveName(name,phrases){const words=(0,eslint_devkit_1.identifierWords)(name);if(words.length===0)return false;return phrases.some(phrase=>{const needle=(0,eslint_devkit_1.identifierWords)(phrase);if(needle.length===0||needle.length>words.length)return false;const start=words.length-needle.length;return needle.every((word,offset)=>words[start+offset]===word)})}exports.requireSecureDeletion=(0,eslint_devkit_1.createRule)({name:"require-secure-deletion",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/require-secure-deletion.md",description:"Require secure data deletion patterns",cwe:"CWE-459",cvss:5.3},messages:{violationDetected:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Incomplete Secret Cleanup",cwe:"CWE-459",description:"`delete` on the sensitive property `{{property}}` unbinds it without scrubbing the value",severity:"MEDIUM",fix:"Overwrite the value before deleting it (obj.{{property}} = undefined, or zero-fill the Buffer), and make sure no copy of the object was spread, logged, or serialised first",documentationLink:"https://cwe.mitre.org/data/definitions/459.html"})},schema:[{type:"object",properties:{sensitiveProperties:{type:"array",items:{type:"string"},default:[...SENSITIVE_PROPERTY_NAMES],description:"Replace the built-in sensitive-property vocabulary. Takes precedence over additionalSensitiveProperties."},additionalSensitiveProperties:{type:"array",items:{type:"string"},default:[],description:'Extra sensitive property names, matched as whole words at the END of the name. "pin code", "pin_code" and "pinCode" all match a property called pinCode; "pincode" does not.'}},additionalProperties:false}]},defaultOptions:[{additionalSensitiveProperties:[],sensitiveProperties:[...SENSITIVE_PROPERTY_NAMES]}],create(context,[options={}]){const{additionalSensitiveProperties=[],sensitiveProperties=SENSITIVE_PROPERTY_NAMES}=options;const phrases=[...sensitiveProperties,...additionalSensitiveProperties];function deletedPropertyName(node){const argument=node.type===eslint_devkit_1.AST_NODE_TYPES.ChainExpression?node.expression:node;if(argument.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return void 0;const property=argument.property;if(!argument.computed&&property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return property.name;const resolved=(0,const_value_1.resolveConstantString)(context.sourceCode,property);return resolved?.value}function reportIfSensitive(node,property){if(!property)return;if(!isSensitiveName(property,phrases))return;context.report({node,messageId:"violationDetected",data:{property}})}return{UnaryExpression(node){if(node.operator!=="delete")return;reportIfSensitive(node,deletedPropertyName(node.argument))},CallExpression(node){const callee=node.callee;if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression||callee.computed||callee.object.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier||callee.object.name!=="Reflect"||callee.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier||callee.property.name!=="deleteProperty"){return}const key=node.arguments[1];if(!key)return;reportIfSensitive(node,(0,const_value_1.resolveConstantString)(context.sourceCode,key)?.value)}}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.storesACredential=storesACredential;exports.expressionNamesACredential=expressionNamesACredential;exports.isEncrypted=isEncrypted;exports.isEncryptedExpression=isEncryptedExpression;exports.isWebStorageWrite=isWebStorageWrite;exports.isEnvironmentWrite=isEnvironmentWrite;exports.isFileWrite=isFileWrite;const eslint_devkit_1=require("@interlace/eslint-devkit");const CREDENTIAL_WORDS=["password","passwd","secret","token","credential","apikey","api_key","accesskey","access_key","privatekey","private_key","jwt","sessionid","session_id","refreshtoken","refresh_token","clientsecret","client_secret","authtoken","auth_token"];function namesACredential(text){const normalized=text.toLowerCase();return CREDENTIAL_WORDS.some(word=>normalized.includes(word))}function nameOf(node){switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Identifier:return node.name;case eslint_devkit_1.AST_NODE_TYPES.Literal:return typeof node.value==="string"?node.value:"";case eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral:return node.quasis.map(q=>q.value.raw).join("");case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:if(node.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return node.property.name;return node.property.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof node.property.value==="string"?node.property.value:"";default:return""}}function storesACredential(node){return node.arguments.some(argument=>{if(argument.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement)return false;return namesACredential(nameOf(argument))})}function expressionNamesACredential(node){return namesACredential(nameOf(node))}const CRYPTO_MODULES=new Set(["crypto","crypto-js","node-forge","tweetnacl","libsodium","libsodium-wrappers","libsodium-wrappers-sumo","sodium-native","jose","node-jose","@aws-crypto/client-node","@google-cloud/kms"]);const ENCRYPTION_APIS=new Set(["publicEncrypt","privateEncrypt","createCipheriv","createCipher","encrypt","wrapKey","CompactEncrypt","FlattenedEncrypt","GeneralEncrypt","compactEncrypt","crypto_secretbox_easy","crypto_box_easy","crypto_box_seal","crypto_aead_chacha20poly1305_ietf_encrypt","crypto_aead_xchacha20poly1305_ietf_encrypt","crypto_secretstream_xchacha20poly1305_push","secretbox","box"]);const CIPHER_OUTPUT_METHODS=new Set(["update","final"]);function localFunctionOf(node,scope){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return null;let current=scope;let variable;while(current&&!variable){variable=current.set?.get(node.name);current=current.upper}if(!variable||variable.defs.length!==1)return null;const def=variable.defs[0];if(def.type==="FunctionName")return def.node;if(def.type!=="Variable")return null;const init=def.node.init;if(init?.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression||init?.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression){return init}return null}function returnedExpressions(fn){const body=fn.body;if(!body)return[];if(body.type!==eslint_devkit_1.AST_NODE_TYPES.BlockStatement)return[body];const found=[];const walk=statements=>{for(const statement of statements){if(statement.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement){if(statement.argument)found.push(statement.argument)}else if(statement.type===eslint_devkit_1.AST_NODE_TYPES.IfStatement){walk([statement.consequent]);if(statement.alternate)walk([statement.alternate])}else if(statement.type===eslint_devkit_1.AST_NODE_TYPES.BlockStatement){walk(statement.body)}else if(statement.type===eslint_devkit_1.AST_NODE_TYPES.TryStatement){walk(statement.block.body)}}};walk(body.body);return found}function provablyEncrypts(value,sourceCode,depth,seen){if(depth>8||seen.has(value))return false;seen.add(value);const recurse=node=>provablyEncrypts(node,sourceCode,depth+1,seen);switch(value.type){case eslint_devkit_1.AST_NODE_TYPES.AwaitExpression:return recurse(value.argument);case eslint_devkit_1.AST_NODE_TYPES.ArrayExpression:return value.elements.some(element=>element!==null&&recurse(element));case eslint_devkit_1.AST_NODE_TYPES.Identifier:{let current=sourceCode.getScope(value);let variable;while(current&&!variable){variable=current.set?.get(value.name);current=current.upper}if(!variable||variable.defs.length!==1)return false;const def=variable.defs[0];if(def.type!=="Variable"||!def.node.init)return false;return recurse(def.node.init)}case eslint_devkit_1.AST_NODE_TYPES.CallExpression:{const callee=value.callee;const scope=sourceCode.getScope(value);const binding=(0,eslint_devkit_1.resolveModuleBinding)(callee,scope);if(binding&&CRYPTO_MODULES.has(binding.module)&&binding.path.length>0&&ENCRYPTION_APIS.has(binding.path[binding.path.length-1])){return true}if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!callee.computed&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&CIPHER_OUTPUT_METHODS.has(callee.property.name)&&recurse(callee.object)){return true}const fn=localFunctionOf(callee,scope);if(fn&&returnedExpressions(fn).some(returned=>recurse(returned))){return true}return value.arguments.some(argument=>argument.type!==eslint_devkit_1.AST_NODE_TYPES.SpreadElement&&recurse(argument))}default:return false}}function isEncrypted(node,sourceCode){return isEncryptedExpression(node.arguments[1],sourceCode)}function isEncryptedExpression(value,sourceCode){if(value===void 0)return false;return provablyEncrypts(value,sourceCode,0,new Set)}const CLIENT_STORES=new Set(["localStorage","sessionStorage","AsyncStorage"]);function isWebStorageWrite(node){const callee=node.callee;if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return false;if(callee.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;if(callee.property.name!=="setItem")return false;const object=callee.object;if(object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return CLIENT_STORES.has(object.name)}return object.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&object.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&CLIENT_STORES.has(object.property.name)}function isEnvironmentWrite(node){const target=node.left;if(target.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return false;const object=target.object;return object.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!object.computed&&object.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&object.object.name==="process"&&object.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&object.property.name==="env"}function isFileWrite(node){const callee=node.callee;if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return false;if(callee.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;return["writeFile","writeFileSync","appendFile","appendFileSync"].includes(callee.property.name)}
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.storesACredential=storesACredential;exports.expressionNamesACredential=expressionNamesACredential;exports.isEncrypted=isEncrypted;exports.isEncryptedExpression=isEncryptedExpression;exports.isWebStorageWrite=isWebStorageWrite;exports.isEnvironmentWrite=isEnvironmentWrite;exports.isFileWrite=isFileWrite;const eslint_devkit_1=require("@interlace/eslint-devkit");const CREDENTIAL_WORDS=["password","passwd","secret","token","credential","apikey","api_key","accesskey","access_key","privatekey","private_key","jwt","sessionid","session_id","refreshtoken","refresh_token","clientsecret","client_secret","authtoken","auth_token"];const CONFIG_ABOUT_A_CREDENTIAL=new Set(["alg","algo","algorithm","cipher","digest","expiry","expiration","ttl","lifetime","maxage","rotation","type","kind","format","encoding","scheme","length","len","size","count","limit","name","label","id","prefix","suffix","path","file","url","uri","endpoint","host","header","issuer","audience","realm","enabled","disabled","required","strategy","provider"]);function segmentsOf(text){return text.replace(/([a-z0-9])([A-Z])/g,"$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean).map(part=>part.toLowerCase())}function namesACredential(text){const normalized=text.toLowerCase();if(!CREDENTIAL_WORDS.some(word=>normalized.includes(word)))return false;const segments=segmentsOf(text);const tail=segments[segments.length-1];if(tail!==void 0&&CONFIG_ABOUT_A_CREDENTIAL.has(tail))return false;return true}function nameOf(node){switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Identifier:return node.name;case eslint_devkit_1.AST_NODE_TYPES.Literal:return typeof node.value==="string"?node.value:"";case eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral:return node.quasis.map(q=>q.value.raw).join("");case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:if(node.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return node.property.name;return node.property.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof node.property.value==="string"?node.property.value:"";default:return""}}function storesACredential(node){return node.arguments.some(argument=>{if(argument.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement)return false;return namesACredential(nameOf(argument))})}function expressionNamesACredential(node){return namesACredential(nameOf(node))}const CRYPTO_MODULES=new Set(["crypto","crypto-js","node-forge","tweetnacl","libsodium","libsodium-wrappers","libsodium-wrappers-sumo","sodium-native","jose","node-jose","@aws-crypto/client-node","@google-cloud/kms"]);const ENCRYPTION_APIS=new Set(["publicEncrypt","privateEncrypt","createCipheriv","createCipher","encrypt","wrapKey","CompactEncrypt","FlattenedEncrypt","GeneralEncrypt","compactEncrypt","crypto_secretbox_easy","crypto_box_easy","crypto_box_seal","crypto_aead_chacha20poly1305_ietf_encrypt","crypto_aead_xchacha20poly1305_ietf_encrypt","crypto_secretstream_xchacha20poly1305_push","secretbox","box"]);const CIPHER_OUTPUT_METHODS=new Set(["update","final"]);function localFunctionOf(node,scope){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return null;let current=scope;let variable;while(current&&!variable){variable=current.set?.get(node.name);current=current.upper}if(!variable||variable.defs.length!==1)return null;const def=variable.defs[0];if(def.type==="FunctionName")return def.node;if(def.type!=="Variable")return null;const init=def.node.init;if(init?.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression||init?.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression){return init}return null}function returnedExpressions(fn){const body=fn.body;if(!body)return[];if(body.type!==eslint_devkit_1.AST_NODE_TYPES.BlockStatement)return[body];const found=[];const walk=statements=>{for(const statement of statements){if(statement.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement){if(statement.argument)found.push(statement.argument)}else if(statement.type===eslint_devkit_1.AST_NODE_TYPES.IfStatement){walk([statement.consequent]);if(statement.alternate)walk([statement.alternate])}else if(statement.type===eslint_devkit_1.AST_NODE_TYPES.BlockStatement){walk(statement.body)}else if(statement.type===eslint_devkit_1.AST_NODE_TYPES.TryStatement){walk(statement.block.body)}}};walk(body.body);return found}function provablyEncrypts(value,sourceCode,depth,seen){if(depth>8||seen.has(value))return false;seen.add(value);const recurse=node=>provablyEncrypts(node,sourceCode,depth+1,seen);switch(value.type){case eslint_devkit_1.AST_NODE_TYPES.AwaitExpression:return recurse(value.argument);case eslint_devkit_1.AST_NODE_TYPES.ArrayExpression:return value.elements.some(element=>element!==null&&recurse(element));case eslint_devkit_1.AST_NODE_TYPES.Identifier:{let current=sourceCode.getScope(value);let variable;while(current&&!variable){variable=current.set?.get(value.name);current=current.upper}if(!variable||variable.defs.length!==1)return false;const def=variable.defs[0];if(def.type!=="Variable"||!def.node.init)return false;return recurse(def.node.init)}case eslint_devkit_1.AST_NODE_TYPES.CallExpression:{const callee=value.callee;const scope=sourceCode.getScope(value);const binding=(0,eslint_devkit_1.resolveModuleBinding)(callee,scope);if(binding&&CRYPTO_MODULES.has(binding.module)&&binding.path.length>0&&ENCRYPTION_APIS.has(binding.path[binding.path.length-1])){return true}if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!callee.computed&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&CIPHER_OUTPUT_METHODS.has(callee.property.name)&&recurse(callee.object)){return true}const fn=localFunctionOf(callee,scope);if(fn&&returnedExpressions(fn).some(returned=>recurse(returned))){return true}return value.arguments.some(argument=>argument.type!==eslint_devkit_1.AST_NODE_TYPES.SpreadElement&&recurse(argument))}default:return false}}function isEncrypted(node,sourceCode){return isEncryptedExpression(node.arguments[1],sourceCode)}function isEncryptedExpression(value,sourceCode){if(value===void 0)return false;return provablyEncrypts(value,sourceCode,0,new Set)}const CLIENT_STORES=new Set(["localStorage","sessionStorage","AsyncStorage"]);function isWebStorageWrite(node){const callee=node.callee;if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return false;if(callee.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;if(callee.property.name!=="setItem")return false;const object=callee.object;if(object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return CLIENT_STORES.has(object.name)}return object.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&object.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&CLIENT_STORES.has(object.property.name)}function isEnvironmentWrite(node){const target=node.left;if(target.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return false;const object=target.object;return object.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!object.computed&&object.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&object.object.name==="process"&&object.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&object.property.name==="env"}function isFileWrite(node){const callee=node.callee;if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return false;if(callee.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;return["writeFile","writeFileSync","appendFile","appendFileSync"].includes(callee.property.name)}