├── .eslint-doc-generatorrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── new-rule.yml │ └── rule-change.yml ├── release-please │ ├── config.json │ └── mainfest.json └── workflows │ ├── ci.yml │ ├── pr.yml │ └── release-please.yml ├── .gitignore ├── .markdownlint.json ├── .markdownlintignore ├── .prettierignore ├── .prettierrc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── avoid-command-injection-node.md ├── bypass-connect-csrf-protection-by-abusing.md ├── regular-expression-dos-and-node.md ├── rules │ ├── detect-bidi-characters.md │ ├── detect-buffer-noassert.md │ ├── detect-child-process.md │ ├── detect-disable-mustache-escape.md │ ├── detect-eval-with-expression.md │ ├── detect-new-buffer.md │ ├── detect-no-csrf-before-method-override.md │ ├── detect-non-literal-fs-filename.md │ ├── detect-non-literal-regexp.md │ ├── detect-non-literal-require.md │ ├── detect-object-injection.md │ ├── detect-possible-timing-attacks.md │ ├── detect-pseudoRandomBytes.md │ └── detect-unsafe-regex.md └── the-dangers-of-square-bracket-notation.md ├── eslint.config.js ├── index.js ├── package-lock.json ├── package.json ├── rules ├── detect-bidi-characters.js ├── detect-buffer-noassert.js ├── detect-child-process.js ├── detect-disable-mustache-escape.js ├── detect-eval-with-expression.js ├── detect-new-buffer.js ├── detect-no-csrf-before-method-override.js ├── detect-non-literal-fs-filename.js ├── detect-non-literal-regexp.js ├── detect-non-literal-require.js ├── detect-object-injection.js ├── detect-possible-timing-attacks.js ├── detect-pseudoRandomBytes.js └── detect-unsafe-regex.js ├── test ├── configs │ └── index.js ├── rules │ ├── detect-bidi-characters.js │ ├── detect-buffer-noassert.js │ ├── detect-child-process.js │ ├── detect-disable-mustache-escape.js │ ├── detect-eval-with-expression.js │ ├── detect-new-buffer.js │ ├── detect-no-csrf-before-method-override.js │ ├── detect-non-literal-fs-filename.js │ ├── detect-non-literal-regexp.js │ ├── detect-non-literal-require.js │ ├── detect-object-injection.js │ ├── detect-possible-timing-attacks.js │ ├── detect-pseudoRandomBytes.js │ └── detect-unsafe-regexp.js └── utils │ ├── import-utils.js │ └── is-static-expression.js └── utils ├── data └── fsFunctionData.json ├── find-variable.js ├── import-utils.js └── is-static-expression.js /.eslint-doc-generatorrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { format } = require('prettier'); 4 | const prettierRC = require('./.prettierrc.json'); 5 | 6 | /** @type {import('eslint-doc-generator').GenerateOptions} */ 7 | const config = { 8 | ignoreConfig: ['recommended-legacy'], 9 | postprocess: (doc) => format(doc, { ...prettierRC, parser: 'markdown' }), 10 | }; 11 | 12 | module.exports = config; 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F41E Report a problem" 2 | description: 'Report an issue with a rule' 3 | title: 'Bug: (fill in)' 4 | labels: 5 | - bug 6 | - 'repro:needed' 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: By opening an issue, you agree to abide by the [Open JS Foundation Code of Conduct](https://eslint.org/conduct). 11 | - type: input 12 | attributes: 13 | label: What version of eslint-plugin-security are you using? 14 | validations: 15 | required: true 16 | - type: textarea 17 | attributes: 18 | label: ESLint Environment 19 | description: | 20 | Please tell us about how you're running ESLint (Run `npx eslint --env-info`.) 21 | value: | 22 | Node version: 23 | npm version: 24 | Local ESLint version: 25 | Global ESLint version: 26 | Operating System: 27 | validations: 28 | required: true 29 | - type: dropdown 30 | attributes: 31 | label: What parser are you using? 32 | description: | 33 | Please keep in mind that some problems are parser-specific. 34 | options: 35 | - 'Default (Espree)' 36 | - '@typescript-eslint/parser' 37 | - '@babel/eslint-parser' 38 | - 'vue-eslint-parser' 39 | - '@angular-eslint/template-parser' 40 | - Other 41 | validations: 42 | required: true 43 | - type: textarea 44 | attributes: 45 | label: What did you do? 46 | description: | 47 | Please include a *minimal* reproduction case. If possible, include a link to a reproduction of the problem in the [ESLint demo](https://eslint.org/demo). Otherwise, include source code, configuration file(s), and any other information about how you're using ESLint. You can use Markdown in this field. 48 | value: | 49 |
50 | Configuration 51 | 52 | ``` 53 | 54 | ``` 55 |
56 | 57 | ```js 58 | 59 | ``` 60 | validations: 61 | required: true 62 | - type: textarea 63 | attributes: 64 | label: What did you expect to happen? 65 | description: | 66 | You can use Markdown in this field. 67 | validations: 68 | required: true 69 | - type: textarea 70 | attributes: 71 | label: What actually happened? 72 | description: | 73 | Please copy-paste the actual ESLint output. You can use Markdown in this field. 74 | validations: 75 | required: true 76 | - type: checkboxes 77 | attributes: 78 | label: Participation 79 | options: 80 | - label: I am willing to submit a pull request for this issue. 81 | required: false 82 | - type: textarea 83 | attributes: 84 | label: Additional comments 85 | description: Is there anything else that's important for the team to know? 86 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new-rule.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F680 Propose a new rule" 2 | description: 'Propose a new rule to be added to the plugin' 3 | title: 'New Rule: (fill in)' 4 | labels: 5 | - rule 6 | - feature 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: By opening an issue, you agree to abide by the [Open JS Foundation Code of Conduct](https://eslint.org/conduct). 11 | - type: input 12 | attributes: 13 | label: Rule details 14 | description: What should the new rule do? 15 | validations: 16 | required: true 17 | - type: input 18 | attributes: 19 | label: Related CVE 20 | description: We only accept new rules that have a published [CVE](https://www.redhat.com/en/topics/security/what-is-cve). 21 | validations: 22 | required: true 23 | - type: textarea 24 | attributes: 25 | label: Example code 26 | description: Please provide some example JavaScript code that this rule will warn about. This field will render as JavaScript. 27 | render: js 28 | validations: 29 | required: true 30 | - type: checkboxes 31 | attributes: 32 | label: Participation 33 | options: 34 | - label: I am willing to submit a pull request to implement this rule. 35 | required: false 36 | - type: textarea 37 | attributes: 38 | label: Additional comments 39 | description: Is there anything else that's important for the team to know? 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/rule-change.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F4DD Request a rule change" 2 | description: 'Request a change to an existing rule' 3 | title: 'Rule Change: (fill in)' 4 | labels: 5 | - enhancement 6 | - rule 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: By opening an issue, you agree to abide by the [Open JS Foundation Code of Conduct](https://eslint.org/conduct). 11 | - type: input 12 | attributes: 13 | label: What rule do you want to change? 14 | validations: 15 | required: true 16 | - type: dropdown 17 | attributes: 18 | label: What change to do you want to make? 19 | options: 20 | - Generate more warnings 21 | - Generate fewer warnings 22 | - Implement autofix 23 | - Implement suggestions 24 | validations: 25 | required: true 26 | - type: dropdown 27 | attributes: 28 | label: How do you think the change should be implemented? 29 | options: 30 | - A new option 31 | - A new default behavior 32 | - Other 33 | validations: 34 | required: true 35 | - type: textarea 36 | attributes: 37 | label: Example code 38 | description: Please provide some example code that this change will affect. This field will render as JavaScript. 39 | render: js 40 | validations: 41 | required: true 42 | - type: textarea 43 | attributes: 44 | label: What does the rule currently do for this code? 45 | validations: 46 | required: true 47 | - type: textarea 48 | attributes: 49 | label: What will the rule do after it's changed? 50 | validations: 51 | required: true 52 | - type: checkboxes 53 | attributes: 54 | label: Participation 55 | options: 56 | - label: I am willing to submit a pull request to implement this change. 57 | required: false 58 | - type: textarea 59 | attributes: 60 | label: Additional comments 61 | description: Is there anything else that's important for the team to know? 62 | -------------------------------------------------------------------------------- /.github/release-please/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", 3 | "release-type": "node", 4 | "prerelease": false, 5 | "include-component-in-tag": false, 6 | "bootstrap-sha": "aa33cf5a1dcd5794772d38e1c5ba5a421c8156b7", 7 | "changelog-sections": [ 8 | { "type": "feat", "section": "🌟 Features", "hidden": false }, 9 | { "type": "fix", "section": "🩹 Fixes", "hidden": false }, 10 | { "type": "docs", "section": "📚 Documentation", "hidden": false }, 11 | 12 | { "type": "chore", "section": "🧹 Chores", "hidden": false }, 13 | { "type": "perf", "section": "🧹 Chores", "hidden": false }, 14 | { "type": "refactor", "section": "🧹 Chores", "hidden": false }, 15 | { "type": "test", "section": "🧹 Chores", "hidden": false }, 16 | 17 | { "type": "build", "section": "🤖 Automation", "hidden": false }, 18 | { "type": "ci", "section": "🤖 Automation", "hidden": true } 19 | ], 20 | "packages": { ".": {} } 21 | } -------------------------------------------------------------------------------- /.github/release-please/mainfest.json: -------------------------------------------------------------------------------- 1 | {".":"3.0.1"} -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | 8 | jobs: 9 | lint: 10 | name: Lint 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | persist-credentials: false 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: lts/* 21 | 22 | - name: Install Packages 23 | run: npm install 24 | 25 | - name: Lint Files 26 | run: npm run lint 27 | 28 | test: 29 | name: Test 30 | strategy: 31 | matrix: 32 | os: [ubuntu-latest] 33 | node: [18.18.0, 18.x, 20.x, 22.x, 24.x] 34 | include: 35 | - os: windows-latest 36 | node: lts/* 37 | - os: macOS-latest 38 | node: lts/* 39 | runs-on: ${{ matrix.os }} 40 | permissions: 41 | contents: read 42 | steps: 43 | - uses: actions/checkout@v4 44 | with: 45 | persist-credentials: false 46 | 47 | - uses: actions/setup-node@v4 48 | with: 49 | node-version: ${{ matrix.node }} 50 | 51 | - name: Install Packages 52 | run: npm install 53 | 54 | - name: Test 55 | run: npm test 56 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request Titles 2 | on: pull_request 3 | 4 | jobs: 5 | conventional: 6 | name: Conventional PR 7 | runs-on: ubuntu-latest 8 | permissions: 9 | contents: read 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | persist-credentials: false 14 | - uses: actions/setup-node@v4 15 | - uses: beemojs/conventional-pr-action@v3 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | with: 19 | config-preset: conventional-changelog-conventionalcommits 20 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | name: release-please 6 | 7 | 8 | jobs: 9 | release-please: 10 | name: Create/Update Release Pull Request 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | pull-requests: write 15 | outputs: 16 | release_created: ${{ steps.release.outputs.release_created }} 17 | major: ${{ steps.release.outputs.major }} 18 | minor: ${{ steps.release.outputs.minor }} 19 | patch: ${{ steps.release.outputs.patch }} 20 | steps: 21 | - name: Release Please 22 | id: release 23 | uses: googleapis/release-please-action@v4 24 | with: 25 | config-file: .github/release-please/config.json 26 | manifest-file: .github/release-please/manifest.json 27 | 28 | publish-npm: 29 | name: Publish to NPM 30 | needs: release-please 31 | if: needs.release-please.outputs.release_created == 'true' 32 | runs-on: ubuntu-latest 33 | permissions: 34 | contents: read 35 | id-token: write 36 | steps: 37 | - name: Check out repo 38 | uses: actions/checkout@v4 39 | with: 40 | persist-credentials: false 41 | 42 | - name: Setup Node 43 | uses: actions/setup-node@v4 44 | with: 45 | check-latest: true 46 | node-version: lts/* 47 | registry-url: https://registry.npmjs.org 48 | 49 | - name: Publish to NPM 50 | env: 51 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 52 | run: npm publish --access public --provenance 53 | 54 | # Tweets out release announcement 55 | - run: 'npx @humanwhocodes/tweet "${{ github.event.repository.full_name }} v${{ needs.release-please.outputs.major }}.${{ needs.release-please.outputs.minor }}.${{ needs.release-please.outputs.patch }} has been released!\n\n${{ github.event.repository.html_url }}/releases/tag/v${{ needs.release-please.outputs.major }}.${{ needs.release-please.outputs.minor }}.${{ needs.release-please.outputs.patch }}"' 56 | env: 57 | TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }} 58 | TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }} 59 | TWITTER_ACCESS_TOKEN_KEY: ${{ secrets.TWITTER_ACCESS_TOKEN_KEY }} 60 | TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "line-length": false, 3 | "no-inline-html": { "allowed_elements": ["kbd"] } 4 | } 5 | -------------------------------------------------------------------------------- /.markdownlintignore: -------------------------------------------------------------------------------- 1 | CHANGELOG.md 2 | LICENSE 3 | node_modules 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /CHANGELOG.md 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 180 7 | } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### [3.0.1](https://www.github.com/eslint-community/eslint-plugin-security/compare/v3.0.0...v3.0.1) (2024-06-14) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * add name to recommended flat config ([#161](https://www.github.com/eslint-community/eslint-plugin-security/issues/161)) ([aa1c8c5](https://www.github.com/eslint-community/eslint-plugin-security/commit/aa1c8c57a2df4ce64a202808c5642a41b47d4519)) 9 | 10 | ### [3.0.1](https://www.github.com/eslint-community/eslint-plugin-security/compare/v3.0.0...v3.0.1) (2024-06-13) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * add name to recommended flat config ([#161](https://www.github.com/eslint-community/eslint-plugin-security/issues/161)) ([aa1c8c5](https://www.github.com/eslint-community/eslint-plugin-security/commit/aa1c8c57a2df4ce64a202808c5642a41b47d4519)) 16 | 17 | ## [3.0.0](https://www.github.com/eslint-community/eslint-plugin-security/compare/v2.1.1...v3.0.0) (2024-04-10) 18 | 19 | 20 | ### ⚠ BREAKING CHANGES 21 | 22 | * requires node ^18.18.0 || ^20.9.0 || >=21.1.0 (#146) 23 | 24 | ### Features 25 | 26 | * requires node ^18.18.0 || ^20.9.0 || >=21.1.0 ([#146](https://www.github.com/eslint-community/eslint-plugin-security/issues/146)) ([df1b606](https://www.github.com/eslint-community/eslint-plugin-security/commit/df1b6063c1224e1163dfdc37c96b64bb52d816bb)) 27 | 28 | 29 | ### Bug Fixes 30 | 31 | * Ensure everything works with ESLint v9 ([#145](https://www.github.com/eslint-community/eslint-plugin-security/issues/145)) ([ac50ab4](https://www.github.com/eslint-community/eslint-plugin-security/commit/ac50ab481ed63d7262513186136ca1429d3b8290)) 32 | 33 | ### [2.1.1](https://www.github.com/eslint-community/eslint-plugin-security/compare/v2.1.0...v2.1.1) (2024-02-14) 34 | 35 | 36 | ### Bug Fixes 37 | 38 | * Ensure empty eval() doesn't crash detect-eval-with-expression ([#139](https://www.github.com/eslint-community/eslint-plugin-security/issues/139)) ([8a7c7db](https://www.github.com/eslint-community/eslint-plugin-security/commit/8a7c7db1e2b49e2831d510b8dc1db235dee0edf0)) 39 | 40 | ## [2.1.0](https://www.github.com/eslint-community/eslint-plugin-security/compare/v2.0.0...v2.1.0) (2023-12-15) 41 | 42 | 43 | ### Features 44 | 45 | * add config recommended-legacy ([#132](https://www.github.com/eslint-community/eslint-plugin-security/issues/132)) ([13d3f2f](https://www.github.com/eslint-community/eslint-plugin-security/commit/13d3f2fc6ba327c894959db30462f3fda0272f0c)) 46 | 47 | ## [2.0.0](https://www.github.com/eslint-community/eslint-plugin-security/compare/v1.7.1...v2.0.0) (2023-10-17) 48 | 49 | 50 | ### ⚠ BREAKING CHANGES 51 | 52 | * switch the recommended config to flat (#118) 53 | 54 | ### Features 55 | 56 | * switch the recommended config to flat ([#118](https://www.github.com/eslint-community/eslint-plugin-security/issues/118)) ([e20a366](https://www.github.com/eslint-community/eslint-plugin-security/commit/e20a3664c2f638466286ae9a97515722fc98f97c)) 57 | 58 | ### [1.7.1](https://www.github.com/eslint-community/eslint-plugin-security/compare/v1.7.0...v1.7.1) (2023-02-02) 59 | 60 | 61 | ### Bug Fixes 62 | 63 | * false positives for static expressions in detect-non-literal-fs-filename, detect-child-process, detect-non-literal-regexp, and detect-non-literal-require ([#109](https://www.github.com/eslint-community/eslint-plugin-security/issues/109)) ([56102b5](https://www.github.com/eslint-community/eslint-plugin-security/commit/56102b50aed4bd632dd668770eb37de58788110b)) 64 | 65 | ## [1.7.0](https://www.github.com/eslint-community/eslint-plugin-security/compare/v1.6.0...v1.7.0) (2023-01-26) 66 | 67 | 68 | ### Features 69 | 70 | * improve detect-child-process rule ([#108](https://www.github.com/eslint-community/eslint-plugin-security/issues/108)) ([64ae529](https://www.github.com/eslint-community/eslint-plugin-security/commit/64ae52944a86f9d9daee769acd63ebbdfc5b6631)) 71 | 72 | ## [1.6.0](https://www.github.com/eslint-community/eslint-plugin-security/compare/v1.5.0...v1.6.0) (2023-01-11) 73 | 74 | ### Features 75 | 76 | * Add meta object documentation for all rules ([#79](https://www.github.com/eslint-community/eslint-plugin-security/issues/79)) ([fb1d9ef](https://www.github.com/eslint-community/eslint-plugin-security/commit/fb1d9ef56e0cf2705b9e413b483261df394c45e1)) 77 | * detect-bidi-characters rule ([#95](https://www.github.com/eslint-community/eslint-plugin-security/issues/95)) ([4294d29](https://www.github.com/eslint-community/eslint-plugin-security/commit/4294d29cca8af5c627de759919add6dd698644ba)) 78 | * **detect-non-literal-fs-filename:** change to track non-top-level `require()` as well ([#105](https://www.github.com/eslint-community/eslint-plugin-security/issues/105)) ([d3b1543](https://www.github.com/eslint-community/eslint-plugin-security/commit/d3b15435b45b9ac2ee5f0d3249f590e32369d7d2)) 79 | * extend detect non literal fs filename ([#92](https://www.github.com/eslint-community/eslint-plugin-security/issues/92)) ([08ba476](https://www.github.com/eslint-community/eslint-plugin-security/commit/08ba4764a83761f6f44cb28940923f1d25f88581)) 80 | * **non-literal-require:** support template literals ([#81](https://www.github.com/eslint-community/eslint-plugin-security/issues/81)) ([208019b](https://www.github.com/eslint-community/eslint-plugin-security/commit/208019bad4f70a142ab1f0ea7238c37cb70d1a5a)) 81 | 82 | ### Bug Fixes 83 | 84 | * Avoid crash when exec() is passed no arguments ([7f97815](https://www.github.com/eslint-community/eslint-plugin-security/commit/7f97815accf6bcd87de73c32a967946b1b3b0530)), closes [#82](https://www.github.com/eslint-community/eslint-plugin-security/issues/82) [#23](https://www.github.com/eslint-community/eslint-plugin-security/issues/23) 85 | * Avoid TypeError when exec stub is used with no arguments ([#97](https://www.github.com/eslint-community/eslint-plugin-security/issues/97)) ([9c18f16](https://www.github.com/eslint-community/eslint-plugin-security/commit/9c18f16187719b58cc5dfde9860344bad823db28)) 86 | * **detect-child-process:** false positive for destructuring with `exec` ([#102](https://www.github.com/eslint-community/eslint-plugin-security/issues/102)) ([657921a](https://www.github.com/eslint-community/eslint-plugin-security/commit/657921a93f6f73c0de6113e497b22e7cf079f520)) 87 | * **detect-child-process:** false positives for destructuring `spawn` ([#103](https://www.github.com/eslint-community/eslint-plugin-security/issues/103)) ([fdfe37d](https://www.github.com/eslint-community/eslint-plugin-security/commit/fdfe37d667367e5fd228c26573a1791c81a044d2)) 88 | * Incorrect method name in detect-buffer-noassert. ([313c0c6](https://www.github.com/eslint-community/eslint-plugin-security/commit/313c0c693f48aa85d0c9b65a46f6c620cd10f907)), closes [#63](https://www.github.com/eslint-community/eslint-plugin-security/issues/63) [#80](https://www.github.com/eslint-community/eslint-plugin-security/issues/80) 89 | 90 | ## 1.5.0 / 2022-04-14 91 | 92 | - Fix avoid crash when exec() is passed no arguments 93 | Closes [#82](https://github.com/eslint-community/eslint-plugin-security/pull/82) with ref as [#23](https://github.com/eslint-community/eslint-plugin-security/pull/23) 94 | - Fix incorrect method name in detect-buffer-noassert 95 | Closes [#63](https://github.com/eslint-community/eslint-plugin-security/pull/63) and [#80](https://github.com/eslint-community/eslint-plugin-security/pull/80) 96 | - Clean up source code formatting 97 | Fixes [#4](https://github.com/eslint-community/eslint-plugin-security/issues/4) and closes [#78](https://github.com/eslint-community/eslint-plugin-security/pull/78) 98 | - Add release script 99 | [Script](https://github.com/eslint-community/eslint-plugin-security/commit/0a6631ea448eb0031af7b351c85b3aa298c2e44c) 100 | - Add non-literal require TemplateLiteral support [#81](https://github.com/eslint-community/eslint-plugin-security/pull/81) 101 | - Add meta object documentation for all rules [#79](https://github.com/eslint-community/eslint-plugin-security/pull/79) 102 | - Added Git pre-commit hook to format JS files 103 | [Pre-commit hook](https://github.com/eslint-community/eslint-plugin-security/commit/e2ae2ee9ef214ca6d8f69fbcc438d230fda2bf97) 104 | - Added yarn installation method 105 | - Fix linting errors and step 106 | [Lint errors](https://github.com/eslint-community/eslint-plugin-security/commit/1258118c2d07722e9fb388a672b287bb43bc73b3), [Lint step](https://github.com/eslint-community/eslint-plugin-security/commit/84f3ed3ab88427753c7ac047d0bccbe557f28aa5) 107 | - Create workflows 108 | Check commit message on pull requests, Set up ci on main branch 109 | - Update test and lint commands to work cross-platform 110 | [Commit](https://github.com/eslint-community/eslint-plugin-security/commit/d3d8e7a27894aa3f83b560f530eb49750e9ee19a) 111 | - Merge pull request [#47](https://github.com/eslint-community/eslint-plugin-security/pull/47) from pdehaan/add-docs 112 | Add old liftsecurity blog posts to docs/ folder 113 | - Bumped up dependencies 114 | - Added `package-lock.json` 115 | - Fixed typos in README and documentation 116 | Replaced dead links in README 117 | 118 | ## 1.4.0 / 2017-06-12 119 | 120 | - 1.4.0 121 | - Stuff and things for 1.4.0 beep boop 🤖 122 | - Merge pull request [#14](https://github.com/eslint-community/eslint-plugin-security/issues/14) from travi/recommended-example 123 | Add recommended ruleset to the usage example 124 | - Merge pull request [#19](https://github.com/eslint-community/eslint-plugin-security/issues/19) from pdehaan/add-changelog 125 | Add basic CHANGELOG.md file 126 | - Merge pull request [#17](https://github.com/eslint-community/eslint-plugin-security/issues/17) from pdehaan/issue-16 127 | Remove filename from error output 128 | - Add basic CHANGELOG.md file 129 | - Remove filename from error output 130 | - Add recommended ruleset to the usage example 131 | for [#9](https://github.com/eslint-community/eslint-plugin-security/issues/9) 132 | - Merge pull request [#10](https://github.com/eslint-community/eslint-plugin-security/issues/10) from pdehaan/issue-9 133 | Add 'plugin:security/recommended' config to plugin 134 | - Merge pull request [#12](https://github.com/eslint-community/eslint-plugin-security/issues/12) from tupaschoal/patch-1 135 | Fix broken link for detect-object-injection 136 | - Fix broken link for detect-object-injection 137 | The current link leads to a 404 page, the new one is the proper page. 138 | - Add 'plugin:security/recommended' config to plugin 139 | 140 | ## 1.3.0 / 2017-02-09 141 | 142 | - 1.3.0 143 | - Merge branch 'scottnonnenberg-update-docs' 144 | - Fix merge conflicts because I can't figure out how to accept pr's in the right order 145 | - Merge pull request [#7](https://github.com/eslint-community/eslint-plugin-security/issues/7) from HamletDRC/patch-1 146 | README.md - documentation detect-new-buffer rule 147 | - Merge pull request [#8](https://github.com/eslint-community/eslint-plugin-security/issues/8) from HamletDRC/patch-2 148 | README.md - document detect-disable-mustache-escape rule 149 | - Merge pull request [#3](https://github.com/eslint-community/eslint-plugin-security/issues/3) from jesusprubio/master 150 | A bit of love 151 | - README.md - document detect-disable-mustache-escape rule 152 | - README.md - documentation detect-new-buffer rule 153 | - Merge pull request [#6](https://github.com/eslint-community/eslint-plugin-security/issues/6) from mathieumg/csrf-bug 154 | Fixed crash with `detect-no-csrf-before-method-override` rule 155 | - Fixed crash with `detect-no-csrf-before-method-override` rule. 156 | - Finishing last commit 157 | - Style guide applied to all the code involving the tests 158 | - Removing a repeated test and style changes 159 | - ESLint added to the workflow 160 | - Removed not needed variables 161 | - Fix to a problem with a rule detected implementing the tests 162 | - Test engine with tests for all the rules 163 | - Minor typos 164 | - A little bit of massage to readme intro 165 | - Add additional information to README for each rule 166 | 167 | ## 1.2.0 / 2016-01-21 168 | 169 | - 1.2.0 170 | - updated to check for new RegExp too 171 | 172 | ## 1.1.0 / 2016-01-06 173 | 174 | - 1.1.0 175 | - adding eslint rule to detect new buffer hotspot 176 | 177 | ## 1.0.0 / 2015-11-15 178 | 179 | - updated desc 180 | - rules disabled by default 181 | - update links 182 | - beep boop 183 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2015 &yet, LLC 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eslint-plugin-security 2 | 3 | [![NPM version](https://img.shields.io/npm/v/eslint-plugin-security.svg?style=flat)](https://npmjs.org/package/eslint-plugin-security) 4 | 5 | ESLint rules for Node Security 6 | 7 | This project will help identify potential security hotspots, but finds a lot of false positives which need triage by a human. 8 | 9 | ## Installation 10 | 11 | ```sh 12 | npm install --save-dev eslint-plugin-security 13 | ``` 14 | 15 | or 16 | 17 | ```sh 18 | yarn add --dev eslint-plugin-security 19 | ``` 20 | 21 | ## Usage 22 | 23 | ### Flat config (requires eslint >= v8.23.0) 24 | 25 | Add the following to your `eslint.config.js` file: 26 | 27 | ```js 28 | const pluginSecurity = require('eslint-plugin-security'); 29 | 30 | module.exports = [pluginSecurity.configs.recommended]; 31 | ``` 32 | 33 | ### eslintrc config (deprecated) 34 | 35 | Add the following to your `.eslintrc` file: 36 | 37 | ```js 38 | module.exports = { 39 | extends: ['plugin:security/recommended-legacy'], 40 | }; 41 | ``` 42 | 43 | ## Developer guide 44 | 45 | - Use [GitHub pull requests](https://help.github.com/articles/using-pull-requests). 46 | - Conventions: 47 | - We use our [custom ESLint setup](https://github.com/nodesecurity/eslint-config-nodesecurity). 48 | - Please implement a test for each new rule and use this command to be sure the new code respects the style guide and the tests keep passing: 49 | 50 | ```sh 51 | npm run-script cont-int 52 | ``` 53 | 54 | ## Tests 55 | 56 | ```sh 57 | npm test 58 | ``` 59 | 60 | ## Rules 61 | 62 | 63 | 64 | ⚠️ Configurations set to warn in.\ 65 | ✅ Set in the `recommended` configuration. 66 | 67 | | Name                                  | Description | ⚠️ | 68 | | :------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------- | :-- | 69 | | [detect-bidi-characters](docs/rules/detect-bidi-characters.md) | Detects trojan source attacks that employ unicode bidi attacks to inject malicious code. | ✅ | 70 | | [detect-buffer-noassert](docs/rules/detect-buffer-noassert.md) | Detects calls to "buffer" with "noAssert" flag set. | ✅ | 71 | | [detect-child-process](docs/rules/detect-child-process.md) | Detects instances of "child_process" & non-literal "exec()" calls. | ✅ | 72 | | [detect-disable-mustache-escape](docs/rules/detect-disable-mustache-escape.md) | Detects "object.escapeMarkup = false", which can be used with some template engines to disable escaping of HTML entities. | ✅ | 73 | | [detect-eval-with-expression](docs/rules/detect-eval-with-expression.md) | Detects "eval(variable)" which can allow an attacker to run arbitrary code inside your process. | ✅ | 74 | | [detect-new-buffer](docs/rules/detect-new-buffer.md) | Detects instances of new Buffer(argument) where argument is any non-literal value. | ✅ | 75 | | [detect-no-csrf-before-method-override](docs/rules/detect-no-csrf-before-method-override.md) | Detects Express "csrf" middleware setup before "method-override" middleware. | ✅ | 76 | | [detect-non-literal-fs-filename](docs/rules/detect-non-literal-fs-filename.md) | Detects variable in filename argument of "fs" calls, which might allow an attacker to access anything on your system. | ✅ | 77 | | [detect-non-literal-regexp](docs/rules/detect-non-literal-regexp.md) | Detects "RegExp(variable)", which might allow an attacker to DOS your server with a long-running regular expression. | ✅ | 78 | | [detect-non-literal-require](docs/rules/detect-non-literal-require.md) | Detects "require(variable)", which might allow an attacker to load and run arbitrary code, or access arbitrary files on disk. | ✅ | 79 | | [detect-object-injection](docs/rules/detect-object-injection.md) | Detects "variable[key]" as a left- or right-hand assignment operand. | ✅ | 80 | | [detect-possible-timing-attacks](docs/rules/detect-possible-timing-attacks.md) | Detects insecure comparisons (`==`, `!=`, `!==` and `===`), which check input sequentially. | ✅ | 81 | | [detect-pseudoRandomBytes](docs/rules/detect-pseudoRandomBytes.md) | Detects if "pseudoRandomBytes()" is in use, which might not give you the randomness you need and expect. | ✅ | 82 | | [detect-unsafe-regex](docs/rules/detect-unsafe-regex.md) | Detects potentially unsafe regular expressions, which may take a very long time to run, blocking the event loop. | ✅ | 83 | 84 | 85 | 86 | ## TypeScript support 87 | 88 | Type definitions for this package are managed by [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped). Use [@types/eslint-plugin-security](https://www.npmjs.com/package/@types/eslint-plugin-security) for type checking. 89 | 90 | ```sh 91 | npm install --save-dev @types/eslint-plugin-security 92 | 93 | # OR 94 | 95 | yarn add --dev @types/eslint-plugin-security 96 | ``` 97 | -------------------------------------------------------------------------------- /docs/avoid-command-injection-node.md: -------------------------------------------------------------------------------- 1 | # Avoiding Command Injection in Node.js 2 | 3 | In this post we are going to learn about the proper way to call a system command using node.js to avoid a common security flaw, command injection. 4 | 5 | A call that we often see used, due to it's simplicity is `child_process.exec`. It's got a simple pattern; pass in a command string and it calls you back with an error or the command results. 6 | 7 | Here is a very typical way you would call a system command with `child_process.exec.` 8 | 9 | ```js 10 | child_process.exec('ls', function (err, data) { 11 | console.log(data); 12 | }); 13 | ``` 14 | 15 | What happens though when you need to start getting user input for arguments into your command? The obvious solution is to take the user input and build your command out using string concatenation. But here's something I've learned over the years: When you use string concatenation to send data from one system to another you're probably going to have a bad day. 16 | 17 | ```js 18 | var path = 'user input'; 19 | child_process.exec('ls -l ' + path, function (err, data) { 20 | console.log(data); 21 | }); 22 | ``` 23 | 24 | ## Why is string concatenation a problem? 25 | 26 | Well, because under the hood, `child_process.exec` makes a call to execute /bin/sh rather than the target program. The command that was sent just gets passed along as a shell command in the newly spawned /bin/sh process. `child_process.exec` has a misleading name - it's a bash interpreter, not a program launcher. And that means that all shell metacharacters can have devastating effects if the command is including user input. 27 | 28 | ```sh 29 | [pid 25170] execve("/bin/sh", ["/bin/sh", "-c", "ls -l user input"], [/* 16 vars */] 30 | ``` 31 | 32 | For example, an attacker could use a ; to end the statement and start another one, they could use backticks or $() to run a subcommand. Lots of potential for abuse. 33 | 34 | ## So how do we do this the right way? 35 | 36 | Calls like `spawn` and `execFile` take additional command arguments as an array, are not executed under a shell environment, and do not manipulate the originally intended command to run. 37 | 38 | Let's modify our example to use `execFile` and `spawn` and see how the system calls differ, and why it isn't vulnerable to command injection. 39 | 40 | ### `child_process.execFile` 41 | 42 | ```js 43 | var child_process = require('child_process'); 44 | 45 | var path = '.'; 46 | child_process.execFile('/bin/ls', ['-l', path], function (err, result) { 47 | console.log(result); 48 | }); 49 | ``` 50 | 51 | System call that is run 52 | 53 | ```sh 54 | [pid 25565] execve("/bin/ls", ["/bin/ls", "-l", "."], [/* 16 vars */] 55 | ``` 56 | 57 | ### `child_process.spawn` 58 | 59 | Similar example using `spawn` instead. 60 | 61 | ```js 62 | var child_process = require('child_process'); 63 | 64 | var path = '.'; 65 | var ls = child_process.spawn('/bin/ls', ['-l', path]); 66 | ls.stdout.on('data', function (data) { 67 | console.log(data.toString()); 68 | }); 69 | ``` 70 | 71 | System call that is run 72 | 73 | ```sh 74 | [pid 26883] execve("/bin/ls", ["/bin/ls", "-l", "."], [/* 16 vars */ 75 | ``` 76 | 77 | When using `spawn` or `execFile`, our target program is the first argument to execve. This means that a user cannot run subcommands in the shell, because /bin/ls has no idea what to do with backticks or pipes or ;. It's /bin/bash that is going to be interpreting those commands. It's similar to using parameterized vs string-based SQL queries, if you are familiar with that. 78 | 79 | This does however come with a caveat: using `spawn` or `execFile` is not always a safe thing. For example, running /bin/find with `spawn` or `execFile` and passing user input in directly could still lead to complete system takeover. The find command has options that allow for arbitrary file read/write. 80 | 81 | So, here's the collective guidance for running system commands from node.js: 82 | 83 | - Avoid using `child_process.exec`, and never use it if the command contains any input that changes based on user input. 84 | - Try to avoid letting users pass in options to commands if possible. Typically values are okay when using spawn or execfile, but selecting options via a user controlled string is a bad idea. 85 | - If you must allow for user controlled options, look at the options for the command extensively, determine which options are safe, and whitelist only those options. 86 | -------------------------------------------------------------------------------- /docs/bypass-connect-csrf-protection-by-abusing.md: -------------------------------------------------------------------------------- 1 | # Bypass Connect CSRF protection by abusing methodOverride Middleware 2 | 3 | Since our platform isn't setup for advisories that are not specific to a particular module version, but rather a use / configuration of a certain module, we will announce this issue here and get it into the database at a later date. 4 | 5 | This issue was found and reported to us by [Luca Carettoni](http://twitter.com/_ikki) (who we consider one of the node security projects core advisers in many areas) 6 | 7 | ## Affected Component 8 | 9 | Connect, methodOverride middleware 10 | 11 | ### Description 12 | 13 | **Connect's "methodOverride" middleware allows an HTTP request to override the method of the request with the value of the "\_method" post key or with the header "x-http-method-override".** 14 | 15 | As the declaration order of middlewares determines the execution stack in Connect, it is possible to abuse this functionality in order to bypass the standard Connect's anti-CSRF protection. 16 | 17 | Considering the following code: 18 | 19 | ```js 20 | ... 21 | app.use(express.csrf()) 22 | ... 23 | app.use(express.methodOverride()) 24 | ``` 25 | 26 | Connect's CSRF middleware does not check csrf tokens in case of idempotent verbs (GET/HEAD/OPTIONS, see lib/middleware/csrf.js). As a result, it is possible to bypass this security control by sending a GET request with a POST MethodOverride header or key. 27 | 28 | ### Example 29 | 30 | ```sh 31 | GET / HTTP/1.1 32 | [..] 33 | _method=POST 34 | ``` 35 | 36 | ### Mitigation Factors 37 | 38 | Disable methodOverride or make sure that it takes precedence over other middleware declarations. 39 | 40 | Thanks to the same origin policy enforced by modern browsers in XMLHttpRequest and other mechanisms, the exploitability of this issue abusing "x-http-method-override" header is significantly reduced. 41 | 42 | There is also an [ESLint plugin](https://github.com/evilpacket/eslint-rules) that you can use to help identify this. 43 | -------------------------------------------------------------------------------- /docs/regular-expression-dos-and-node.md: -------------------------------------------------------------------------------- 1 | # Regular Expression DoS and Node.js 2 | 3 | Imagine you are trying to buy a ticket to your favorite JavaScript conference, and instead of getting the ticket page, you instead get `500 Internal Server Error`. For some reason the site is down. You can't do the thing that you want to do most and the conference is losing out on your purchase, all because the application is unavailable. 4 | 5 | Availability is not often treated as a security problem, which it is, and its impacts are immediate, and deeply felt. 6 | 7 | The attack surface for Node.js in regards to loss of availability is quite large, as we are dealing with a single event loop. If an attacker can control and block that event loop, then nothing else gets done. 8 | 9 | There are many ways to block the event loop, one way an attacker can do that is with [Regular Expression Denial of Service (ReDoS)](https://www.owasp.org/index.php/Regular_expression_Denial_of_Service_-_ReDoS). 10 | 11 | If user provided input finds its way into a regular expression, or a regular expression is designed with certain attributes, such as grouping with repetition, you can find yourself in a vulnerable position, as the regular expression match could take a long time to process. [OWASP](https://www.owasp.org/index.php/Regular_expression_Denial_of_Service_-_ReDoS) has a deeper explanation of why this occurs. 12 | 13 | Let's look at an vulnerable example. Below we are attempting the common task of validating an email address on the server. 14 | 15 | ```js 16 | validateEmailFormat: function( string ) { 17 | var emailExpression = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/; 18 | 19 | return emailExpression.test( string ); 20 | } 21 | ``` 22 | 23 | With the example above, we can use this test script to show how bad input can impact server responsiveness: 24 | 25 | ```js 26 | start = process.hrtime(); 27 | console.log(validateEmailFormat('baldwin@andyet.net')); 28 | console.log(process.hrtime(start)); 29 | 30 | start = process.hrtime(); 31 | console.log(validateEmailFormat('jjjjjjjjjjjjjjjjjjjjjjjjjjjj@ccccccccccccccccccccccccccccc.5555555555555555555555555555555555555555{')); 32 | console.log(process.hrtime(start)); 33 | 34 | start = process.hrtime(); 35 | console.log(validateEmailFormat('jjjjjjjjjjjjjjjjjjjjjjjjjjjj@ccccccccccccccccccccccccccccc.55555555555555555555555555555555555555555{')); 36 | console.log(process.hrtime(start)); 37 | 38 | start = process.hrtime(); 39 | console.log(validateEmailFormat('jjjjjjjjjjjjjjjjjjjjjjjjjjjj@ccccccccccccccccccccccccccccc.555555555555555555555555555555555555555555555555555555{')); 40 | console.log(process.hrtime(start)); 41 | ``` 42 | 43 | Here are the results of running that script: 44 | 45 | ```sh 46 | true 47 | [ 0, 9694442 ] <- Match on good data takes little time 48 | false 49 | [ 0, 49849962 ] <- Initial bad input baseline 50 | false 51 | [ 0, 55123953 ] <- Added 1 character to the input and you see minimal spike 52 | false 53 | [ 8, 487126563 ] <- Added 12 characters and you see it bumps up significantly 54 | ``` 55 | 56 | One way you can check regular expressions for badness in an automated way is by using a module from [substack](https://twitter.com/substack) called [safe-regex](https://www.npmjs.org/package/safe-regex). It's prone to false positives, however, it can be useful to point to potentially vulnerable regular expressions you would have otherwise missed in your code. 57 | 58 | Here is a rule for eslint that you can use to test your JavaScript regular expressions: 59 | 60 | ```js 61 | var safe = require('safe-regex'); 62 | module.exports = function (context) { 63 | 'use strict'; 64 | 65 | return { 66 | Literal: function (node) { 67 | var token = context.getTokens(node)[0], 68 | nodeType = token.type, 69 | nodeValue = token.value; 70 | 71 | if (nodeType === 'RegularExpression') { 72 | if (!safe(nodeValue)) { 73 | context.report(node, 'Possible Unsafe Regular Expression'); 74 | } 75 | } 76 | }, 77 | }; 78 | }; 79 | ``` 80 | 81 | Additionally, OWASP has a [list of regular expressions](https://www.owasp.org/index.php/OWASP_Validation_Regex_Repository) for common validations that might be useful to you. 82 | 83 | As part of our ongoing effort to increase the overall security of the Node.js ecosystem, we have conducted automated analysis of every module on npm. We did identify 56 unique vulnerable regular expressions and over 120 modules containing vulnerable regular expressions. Considering that there are now over 100k modules, the results were not alarming. We're working closely with the maintainers of each module to get the issues resolved, once that's done, advisories will be published to the [npm Security advisories](https://www.npmjs.com/advisories) site. 84 | -------------------------------------------------------------------------------- /docs/rules/detect-bidi-characters.md: -------------------------------------------------------------------------------- 1 | # Detects trojan source attacks that employ unicode bidi attacks to inject malicious code (`security/detect-bidi-characters`) 2 | 3 | ⚠️ This rule _warns_ in the ✅ `recommended` config. 4 | 5 | 6 | 7 | Detects cases of [trojan source attacks](https://trojansource.codes) that employ unicode bidi attacks to inject malicious code 8 | 9 | ## Why is Trojan Source important? 10 | 11 | The following publication on the topic of unicode characters attacks, dubbed [Trojan Source: Invisible Vulnerabilities](https://trojansource.codes/trojan-source.pdf), has caused a lot of concern from potential supply chain attacks where adversaries are able to inject malicious code into the source code of a project, slipping by unseen in the code review process. 12 | 13 | ### An example 14 | 15 | As an example, take the following code where `RLO`, `LRI`, `PDI`, `IRI` are placeholders to visualise the respective dangerous unicode characters: 16 | 17 | ```js 18 | #!/usr/bin/env node 19 | 20 | var accessLevel = 'user'; 21 | 22 | if (accessLevel != 'userRLO LRI// Check if adminPDI IRI') { 23 | console.log('You are an admin.'); 24 | } 25 | ``` 26 | 27 | The code above, will be rendered by a text editor as follows: 28 | 29 | ```js 30 | #!/usr/bin/env node 31 | 32 | var accessLevel = 'user'; 33 | 34 | if (accessLevel != 'user') { 35 | // Check if admin 36 | console.log('You are an admin.'); 37 | } 38 | ``` 39 | 40 | By looking at the rendered code above, a user reviewing this code might not notice the injected malicious unicode characters which are actually changing the semantic and the behaviour of the actual code. 41 | 42 | ### More information 43 | 44 | For more information on the topic, you're welcome to read on the official website [trojansource.codes](https://trojansource.codes/) and the following [source code repository](https://github.com/nickboucher/trojan-source/) which contains the source code of the publication. 45 | 46 | ### References 47 | 48 | - 49 | - 50 | - 51 | -------------------------------------------------------------------------------- /docs/rules/detect-buffer-noassert.md: -------------------------------------------------------------------------------- 1 | # Detects calls to "buffer" with "noAssert" flag set (`security/detect-buffer-noassert`) 2 | 3 | ⚠️ This rule _warns_ in the ✅ `recommended` config. 4 | 5 | 6 | 7 | Detect calls to [`buffer`](https://nodejs.org/api/buffer.html) with `noAssert` flag set. 8 | 9 | From the Node.js API docs: "Setting `noAssert` to true skips validation of the `offset`. This allows the `offset` to be beyond the end of the `Buffer`." 10 | -------------------------------------------------------------------------------- /docs/rules/detect-child-process.md: -------------------------------------------------------------------------------- 1 | # Detects instances of "child_process" & non-literal "exec()" calls (`security/detect-child-process`) 2 | 3 | ⚠️ This rule _warns_ in the ✅ `recommended` config. 4 | 5 | 6 | 7 | Detect instances of [`child_process`](https://nodejs.org/api/child_process.html) & non-literal [`exec()`](https://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback) 8 | 9 | More information: [Avoiding Command Injection in Node.js](../avoid-command-injection-node.md) 10 | -------------------------------------------------------------------------------- /docs/rules/detect-disable-mustache-escape.md: -------------------------------------------------------------------------------- 1 | # Detects "object.escapeMarkup = false", which can be used with some template engines to disable escaping of HTML entities (`security/detect-disable-mustache-escape`) 2 | 3 | ⚠️ This rule _warns_ in the ✅ `recommended` config. 4 | 5 | 6 | 7 | This can lead to Cross-Site Scripting (XSS) vulnerabilities. 8 | 9 | More information: [OWASP XSS]() 10 | -------------------------------------------------------------------------------- /docs/rules/detect-eval-with-expression.md: -------------------------------------------------------------------------------- 1 | # Detects "eval(variable)" which can allow an attacker to run arbitrary code inside your process (`security/detect-eval-with-expression`) 2 | 3 | ⚠️ This rule _warns_ in the ✅ `recommended` config. 4 | 5 | 6 | 7 | More information: [What are the security issues with eval in JavaScript?](http://security.stackexchange.com/questions/94017/what-are-the-security-issues-with-eval-in-javascript) 8 | -------------------------------------------------------------------------------- /docs/rules/detect-new-buffer.md: -------------------------------------------------------------------------------- 1 | # Detects instances of new Buffer(argument) where argument is any non-literal value (`security/detect-new-buffer`) 2 | 3 | ⚠️ This rule _warns_ in the ✅ `recommended` config. 4 | 5 | 6 | 7 | `new Buffer()` now emits a deprecation warning in Node.js. 8 | 9 | More information: [new Buffer(number) is unsafe](https://github.com/nodejs/node/issues/4660) 10 | -------------------------------------------------------------------------------- /docs/rules/detect-no-csrf-before-method-override.md: -------------------------------------------------------------------------------- 1 | # Detects Express "csrf" middleware setup before "method-override" middleware (`security/detect-no-csrf-before-method-override`) 2 | 3 | ⚠️ This rule _warns_ in the ✅ `recommended` config. 4 | 5 | 6 | 7 | This can allow `GET` requests (which are not checked by `csrf`) to turn into `POST` requests later. 8 | 9 | More information: [Bypass Connect CSRF protection by abusing methodOverride Middleware](../bypass-connect-csrf-protection-by-abusing.md) 10 | -------------------------------------------------------------------------------- /docs/rules/detect-non-literal-fs-filename.md: -------------------------------------------------------------------------------- 1 | # Detects variable in filename argument of "fs" calls, which might allow an attacker to access anything on your system (`security/detect-non-literal-fs-filename`) 2 | 3 | ⚠️ This rule _warns_ in the ✅ `recommended` config. 4 | 5 | 6 | 7 | More information: [OWASP Path Traversal](https://www.owasp.org/index.php/Path_Traversal) 8 | -------------------------------------------------------------------------------- /docs/rules/detect-non-literal-regexp.md: -------------------------------------------------------------------------------- 1 | # Detects "RegExp(variable)", which might allow an attacker to DOS your server with a long-running regular expression (`security/detect-non-literal-regexp`) 2 | 3 | ⚠️ This rule _warns_ in the ✅ `recommended` config. 4 | 5 | 6 | 7 | More information: [Regular Expression DoS and Node.js](../regular-expression-dos-and-node.md) 8 | -------------------------------------------------------------------------------- /docs/rules/detect-non-literal-require.md: -------------------------------------------------------------------------------- 1 | # Detects "require(variable)", which might allow an attacker to load and run arbitrary code, or access arbitrary files on disk (`security/detect-non-literal-require`) 2 | 3 | ⚠️ This rule _warns_ in the ✅ `recommended` config. 4 | 5 | 6 | 7 | More information: [Where does Node.js and require look for modules?](http://www.bennadel.com/blog/2169-where-does-node-js-and-require-look-for-modules.htm) 8 | -------------------------------------------------------------------------------- /docs/rules/detect-object-injection.md: -------------------------------------------------------------------------------- 1 | # Detects "variable[key]" as a left- or right-hand assignment operand (`security/detect-object-injection`) 2 | 3 | ⚠️ This rule _warns_ in the ✅ `recommended` config. 4 | 5 | 6 | 7 | JavaScript allows you to use expressions to access object properties in addition to using dot notation. So instead of writing this: 8 | 9 | ```js 10 | object.name = 'foo'; 11 | ``` 12 | 13 | You can write this: 14 | 15 | ```js 16 | object['name'] = 'foo'; 17 | ``` 18 | 19 | Square bracket notation allows any expression to be used in place of an identifier, so you can also do this: 20 | 21 | ```js 22 | const key = 'name'; 23 | object[key] = 'foo'; 24 | ``` 25 | 26 | By doing so, you've now obfuscated the property name from the reader, which makes it easy for a malicious actor to replace the value of `key` and change the behavior of the code. 27 | 28 | This rule flags any expression in the form of `object[expression]` no matter where it occurs. Examples of patterns this will be flagged are: 29 | 30 | ```js 31 | object[key] = value; 32 | 33 | value = object[key]; 34 | 35 | doSomething(object[key]); 36 | ``` 37 | 38 | More information: [The Dangers of Square Bracket Notation](../the-dangers-of-square-bracket-notation.md) 39 | -------------------------------------------------------------------------------- /docs/rules/detect-possible-timing-attacks.md: -------------------------------------------------------------------------------- 1 | # Detects insecure comparisons (`==`, `!=`, `!==` and `===`), which check input sequentially (`security/detect-possible-timing-attacks`) 2 | 3 | ⚠️ This rule _warns_ in the ✅ `recommended` config. 4 | 5 | 6 | -------------------------------------------------------------------------------- /docs/rules/detect-pseudoRandomBytes.md: -------------------------------------------------------------------------------- 1 | # Detects if "pseudoRandomBytes()" is in use, which might not give you the randomness you need and expect (`security/detect-pseudoRandomBytes`) 2 | 3 | ⚠️ This rule _warns_ in the ✅ `recommended` config. 4 | 5 | 6 | -------------------------------------------------------------------------------- /docs/rules/detect-unsafe-regex.md: -------------------------------------------------------------------------------- 1 | # Detects potentially unsafe regular expressions, which may take a very long time to run, blocking the event loop (`security/detect-unsafe-regex`) 2 | 3 | ⚠️ This rule _warns_ in the ✅ `recommended` config. 4 | 5 | 6 | 7 | More information: [Regular Expression DoS and Node.js](../regular-expression-dos-and-node.md) 8 | -------------------------------------------------------------------------------- /docs/the-dangers-of-square-bracket-notation.md: -------------------------------------------------------------------------------- 1 | # The Dangers of Square Bracket Notation 2 | 3 | We are going to be looking at some peculiar and potentially dangerous implications of JavaScript's square bracket notation in this post: where you shouldn't use this style of object access and why, as well how to use it safely when needed. 4 | 5 | Square bracket notation for objects in JavaScript provides a very convenient way to dynamically access a specific property or method based on the contents of a variable. The end result of this feature is something that is very similar to Ruby's Mass Assignment: Given an object, you are able to dynamically assign and retrieve properties of this object without specifying this property should be accessible. 6 | 7 | _Note: These examples are simple, and seemingly obvious - we will take a look at that later. For now, disregard the practicality of the examples and focus on the dangerous patterns that they reveal._ 8 | 9 | Let's take a look at why this could be a problem. 10 | 11 | ## Issue #1: Bracket object notation with user input grants access to every property available on the object 12 | 13 | ```js 14 | exampleClass[userInput[0]] = userInput[1]; 15 | ``` 16 | 17 | I won't spend much time here, as I believe this is fairly well known. If exampleClass contains a sensitive property, the above code will allow it to be edited. 18 | 19 | ## Issue #2: Bracket object notation with user input grants access to every property available on the object, **_including prototypes._** 20 | 21 | ```js 22 | userInput = ['constructor', '{}']; 23 | exampleClass[userInput[0]] = userInput[1]; 24 | ``` 25 | 26 | This looks pretty innocuous, even if it is an uncommon pattern. The problem here is that we can access or overwrite prototypes such as `constructor` or `__defineGetter__`, which may be used later on. The most likely outcome of this scenario would be an application crash, when a string is attempted to be called as a function. 27 | 28 | ## Issue #3: Bracket object notation with user input grants access to every property available on the object, including prototypes, **_which can lead to Remote Code Execution._** 29 | 30 | Now here's where things get really dangerous. It's also where example code gets really implausible - bear with me. 31 | 32 | ```js 33 | var user = function () { 34 | this.name = 'jon'; 35 | //An empty user constructor. 36 | }; 37 | 38 | function handler(userInput) { 39 | var anyVal = 'anyVal'; // This can be any attribute, and does not need to be user controlled. 40 | user[anyVal] = user[userInput[0]](userInput[1]); 41 | } 42 | ``` 43 | 44 | In the previous section, I mentioned that constructor can be accessed from square brackets. In this case, since we are dealing with a function, the constructor we get back is the `Function` Constructor, which compiles a string of code into a function. 45 | 46 | ## Exploitation 47 | 48 | In order to exploit the above code, we need a two stage exploit function. 49 | 50 | ```js 51 | function exploit(cmd) { 52 | var userInputStageOne = ['constructor', 'require("child_process").exec(arguments[0],console.log)']; 53 | var userInputStageTwo = ['anyVal', cmd]; 54 | 55 | handler(userInputStageOne); 56 | handler(userInputStageTwo); 57 | } 58 | ``` 59 | 60 | Let's break it down. 61 | 62 | The first time handler is run, it looks something like this: 63 | 64 | ```js 65 | userInput[0] = 'constructor'; 66 | userInput[1] = 'require("child_process").exec(arguments[0],console.log)'; 67 | 68 | user['anyVal'] = user['constructor'](userInput[1]); 69 | ``` 70 | 71 | Executing this code creates a function containing the payload, and assigns it to `user['anyVal']`: 72 | 73 | ```js 74 | user['anyVal'] = function () { 75 | require('child_process').exec(arguments[0], console.log); 76 | }; 77 | ``` 78 | 79 | And when handler is run a second time: 80 | 81 | ```js 82 | user.anyVal = user.anyVal('date'); 83 | ``` 84 | 85 | What we end up with is this: 86 | 87 | ![Exploiting date screenshot](https://cldup.com/lR_Xp0PwU9.png) 88 | 89 | Remote Code Execution. The biggest problem here is that there is very little indication in the code that this is what is going on. With something so serious, method calls tend to be very explicit - eval, child_process, etc. It's pretty difficult in node to accidentally introduce one of those into your application. Here though, without having either deep knowledge of JavaScript builtins or having done previous research, it is very easy to accidentally introduce this into your application. 90 | 91 | ## Isn't this so obscure that it doesn't matter a whole lot? 92 | 93 | Well, yes and no. Is this particular vector a widespread problem? No, because current JavaScript style guides don't advocate programming this way. Might it become a widespread problem in the future? Absolutely. This pattern is avoided because it isn't common, and therefore not learned and taken up as habit, not because it's a known insecure pattern. 94 | 95 | Yes, we are talking about some fairly extreme edge cases, but don't make the assumption that your code doesn't have problems because of that - I have seen this issue in production code with some regularity. And, for the majority of node developers, a large portion of application code was not written by them, but rather included through required modules which may contain peculiar flaws like this one. 96 | 97 | Edge cases are uncommon, but because they are uncommon the problems with them are not well known, and they frequently go un-noticed during code review. If the code works, these types of problems tend to disappear. If the code works, and the problems are buried in a module nested n-levels deep, it's likely it won't be found until it causes problems, and by then it's too late. A blind require is essentially running untrusted code in your application. Be aware of the code you're requiring. 98 | 99 | ## How do I fix it? 100 | 101 | The most direct fix here is going to be to **avoid the use of user input in property name fields**. This isn't reasonable in all circumstances, however, and there should be a way to safely use core language features. 102 | 103 | Another option is to create a allowlist of allowed property names, and filter each user input through a helper function to check before allowing it to be used. This is a great option in situations where you know specifically what property names to allow. 104 | 105 | In cases where you don't have a strictly defined data model ( which isn't ideal, but there are cases where it has to be so ) then using the same method as above, but with a denylist of disallowed properties instead is a valid choice. 106 | 107 | If you are using the `--harmony` flag or [io.js](https://iojs.org/), you also have the option of using [ECMAScript 6 direct proxies](http://wiki.ecmascript.org/doku.php?id=harmony:direct_proxies), which can stand in front of your real object ( private API ) and expose a limited subset of the object ( public API ). This is probably the best approach if you are using this pattern, as it is most consistent with typical object oriented programming paradigms. 108 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const jsPlugin = require('@eslint/js'); 4 | const prettierConfig = require('eslint-config-prettier'); 5 | const eslintPluginRecommendedConfig = require('eslint-plugin-eslint-plugin/configs/recommended'); 6 | 7 | const eslintPluginConfigs = [ 8 | eslintPluginRecommendedConfig, 9 | { 10 | rules: { 11 | 'eslint-plugin/prefer-message-ids': 'off', // TODO: enable 12 | 'eslint-plugin/require-meta-docs-description': ['error', { pattern: '^(Detects|Enforces|Requires|Disallows) .+\\.$' }], 13 | 'eslint-plugin/require-meta-docs-url': [ 14 | 'error', 15 | { 16 | pattern: 'https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/{{name}}.md', 17 | }, 18 | ], 19 | 'eslint-plugin/require-meta-schema': 'off', // TODO: enable 20 | 'eslint-plugin/require-meta-type': 'off', // TODO: enable 21 | }, 22 | }, 23 | ]; 24 | 25 | module.exports = [ 26 | jsPlugin.configs.recommended, 27 | prettierConfig, 28 | ...eslintPluginConfigs, 29 | { 30 | languageOptions: { 31 | sourceType: 'commonjs', 32 | }, 33 | }, 34 | { 35 | files: ['test/**/*.js'], 36 | languageOptions: { 37 | globals: { 38 | describe: 'readonly', 39 | it: 'readonly', 40 | }, 41 | }, 42 | }, 43 | ]; 44 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * eslint-plugin-security - ESLint plugin for Node Security 3 | */ 4 | 5 | 'use strict'; 6 | 7 | const pkg = require('./package.json'); 8 | 9 | const plugin = { 10 | meta: { 11 | name: pkg.name, 12 | version: pkg.version, 13 | }, 14 | rules: { 15 | 'detect-unsafe-regex': require('./rules/detect-unsafe-regex'), 16 | 'detect-non-literal-regexp': require('./rules/detect-non-literal-regexp'), 17 | 'detect-non-literal-require': require('./rules/detect-non-literal-require'), 18 | 'detect-non-literal-fs-filename': require('./rules/detect-non-literal-fs-filename'), 19 | 'detect-eval-with-expression': require('./rules/detect-eval-with-expression'), 20 | 'detect-pseudoRandomBytes': require('./rules/detect-pseudoRandomBytes'), 21 | 'detect-possible-timing-attacks': require('./rules/detect-possible-timing-attacks'), 22 | 'detect-no-csrf-before-method-override': require('./rules/detect-no-csrf-before-method-override'), 23 | 'detect-buffer-noassert': require('./rules/detect-buffer-noassert'), 24 | 'detect-child-process': require('./rules/detect-child-process'), 25 | 'detect-disable-mustache-escape': require('./rules/detect-disable-mustache-escape'), 26 | 'detect-object-injection': require('./rules/detect-object-injection'), 27 | 'detect-new-buffer': require('./rules/detect-new-buffer'), 28 | 'detect-bidi-characters': require('./rules/detect-bidi-characters'), 29 | }, 30 | rulesConfig: { 31 | 'detect-unsafe-regex': 0, 32 | 'detect-non-literal-regexp': 0, 33 | 'detect-non-literal-require': 0, 34 | 'detect-non-literal-fs-filename': 0, 35 | 'detect-eval-with-expression': 0, 36 | 'detect-pseudoRandomBytes': 0, 37 | 'detect-possible-timing-attacks': 0, 38 | 'detect-no-csrf-before-method-override': 0, 39 | 'detect-buffer-noassert': 0, 40 | 'detect-child-process': 0, 41 | 'detect-disable-mustache-escape': 0, 42 | 'detect-object-injection': 0, 43 | 'detect-new-buffer': 0, 44 | 'detect-bidi-characters': 0, 45 | }, 46 | configs: {}, // was assigned later so we can reference `plugin` 47 | }; 48 | 49 | const recommended = { 50 | name: 'security/recommended', 51 | plugins: { security: plugin }, 52 | rules: { 53 | 'security/detect-buffer-noassert': 'warn', 54 | 'security/detect-child-process': 'warn', 55 | 'security/detect-disable-mustache-escape': 'warn', 56 | 'security/detect-eval-with-expression': 'warn', 57 | 'security/detect-new-buffer': 'warn', 58 | 'security/detect-no-csrf-before-method-override': 'warn', 59 | 'security/detect-non-literal-fs-filename': 'warn', 60 | 'security/detect-non-literal-regexp': 'warn', 61 | 'security/detect-non-literal-require': 'warn', 62 | 'security/detect-object-injection': 'warn', 63 | 'security/detect-possible-timing-attacks': 'warn', 64 | 'security/detect-pseudoRandomBytes': 'warn', 65 | 'security/detect-unsafe-regex': 'warn', 66 | 'security/detect-bidi-characters': 'warn', 67 | }, 68 | }; 69 | 70 | const recommendedLegacy = { 71 | plugins: ['security'], 72 | rules: recommended.rules, 73 | }; 74 | 75 | Object.assign(plugin.configs, { 76 | recommended, 77 | 'recommended-legacy': recommendedLegacy 78 | }); 79 | 80 | module.exports = plugin; 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-security", 3 | "version": "3.0.1", 4 | "description": "Security rules for eslint", 5 | "main": "index.js", 6 | "type": "commonjs", 7 | "scripts": { 8 | "changelog": "changelog eslint-plugin-security all > CHANGELOG.md", 9 | "cont-int": "npm test && npm run lint", 10 | "format": "prettier --write .", 11 | "lint": "npm-run-all \"lint:*\"", 12 | "lint:docs": "markdownlint \"**/*.md\"", 13 | "lint:eslint-docs": "npm run update:eslint-docs -- --check", 14 | "lint:js": "eslint .", 15 | "lint:js:fix": "npm run lint:js -- --fix", 16 | "release": "npx semantic-release", 17 | "test": "mocha test/**", 18 | "update:eslint-docs": "eslint-doc-generator" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/eslint-community/eslint-plugin-security.git" 23 | }, 24 | "keywords": [ 25 | "eslint", 26 | "security", 27 | "nodesecurity" 28 | ], 29 | "author": "Node Security Project", 30 | "license": "Apache-2.0", 31 | "bugs": { 32 | "url": "https://github.com/eslint-community/eslint-plugin-security/issues" 33 | }, 34 | "homepage": "https://github.com/eslint-community/eslint-plugin-security#readme", 35 | "gitHooks": { 36 | "pre-commit": "lint-staged" 37 | }, 38 | "lint-staged": { 39 | "*.js": [ 40 | "prettier --write", 41 | "eslint --fix" 42 | ], 43 | "*.md": "prettier --write", 44 | "*.yml": "prettier --write" 45 | }, 46 | "dependencies": { 47 | "safe-regex": "^2.1.1" 48 | }, 49 | "devDependencies": { 50 | "@eslint/js": "^9.0.0", 51 | "changelog": "1.4.2", 52 | "eslint": "^9.0.0", 53 | "eslint-config-nodesecurity": "^1.3.1", 54 | "eslint-config-prettier": "^8.5.0", 55 | "eslint-doc-generator": "^1.7.0", 56 | "eslint-plugin-eslint-plugin": "^5.5.1", 57 | "lint-staged": "^12.3.7", 58 | "markdownlint-cli": "^0.32.2", 59 | "mocha": "^9.2.2", 60 | "npm-run-all": "^4.1.5", 61 | "prettier": "^2.6.2", 62 | "semantic-release": "^19.0.2", 63 | "yorkie": "^2.0.0" 64 | }, 65 | "engines": { 66 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 67 | }, 68 | "funding": "https://opencollective.com/eslint" 69 | } 70 | -------------------------------------------------------------------------------- /rules/detect-bidi-characters.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Detect trojan source attacks that employ unicode bidi attacks to inject malicious code 3 | * @author Luciamo Mammino 4 | * @author Simone Sanfratello 5 | * @author Liran Tal 6 | */ 7 | 8 | 'use strict'; 9 | 10 | const dangerousBidiCharsRegexp = /[\u061C\u200E\u200F\u202A\u202B\u202C\u202D\u202E\u2066\u2067\u2068\u2069]/gu; 11 | 12 | /** 13 | * Detects all the dangerous bidi characters in a given source text 14 | * 15 | * @param {object} options - Options 16 | * @param {string} options.sourceText - The source text to search for dangerous bidi characters 17 | * @param {number} options.firstLineOffset - The offset of the first line in the source text 18 | * @returns {Array<{line: number, column: number}>} - An array of reports, each report is an 19 | * object with the line and column of the dangerous character 20 | */ 21 | function detectBidiCharacters({ sourceText, firstLineOffset }) { 22 | const sourceTextToSearch = sourceText.toString(); 23 | 24 | const lines = sourceTextToSearch.split(/\r?\n/); 25 | 26 | return lines.reduce((reports, line, lineIndex) => { 27 | let match; 28 | let offset = lineIndex == 0 ? firstLineOffset : 0; 29 | 30 | while ((match = dangerousBidiCharsRegexp.exec(line)) !== null) { 31 | reports.push({ line: lineIndex, column: offset + match.index }); 32 | } 33 | 34 | return reports; 35 | }, []); 36 | } 37 | 38 | function report({ context, node, tokens, message, firstLineOffset }) { 39 | if (!tokens || !Array.isArray(tokens)) { 40 | return; 41 | } 42 | tokens.forEach((token) => { 43 | const reports = detectBidiCharacters({ sourceText: token.value, firstLineOffset: token.loc.start.column + firstLineOffset }); 44 | 45 | reports.forEach((report) => { 46 | context.report({ 47 | node: node, 48 | data: { 49 | text: token.value, 50 | }, 51 | loc: { 52 | start: { 53 | line: token.loc.start.line + report.line, 54 | column: report.column, 55 | }, 56 | end: { 57 | line: token.loc.start.line + report.line, 58 | column: report.column + 1, 59 | }, 60 | }, 61 | message, 62 | }); 63 | }); 64 | }); 65 | } 66 | 67 | //------------------------------------------------------------------------------ 68 | // Rule Definition 69 | //------------------------------------------------------------------------------ 70 | 71 | module.exports = { 72 | meta: { 73 | type: 'error', 74 | docs: { 75 | description: 'Detects trojan source attacks that employ unicode bidi attacks to inject malicious code.', 76 | category: 'Possible Security Vulnerability', 77 | recommended: true, 78 | url: 'https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-bidi-characters.md', 79 | }, 80 | }, 81 | create(context) { 82 | return { 83 | Program: function (node) { 84 | report({ 85 | context, 86 | node, 87 | tokens: node.tokens, 88 | firstLineOffset: 0, 89 | message: "Detected potential trojan source attack with unicode bidi introduced in this code: '{{text}}'.", 90 | }); 91 | report({ 92 | context, 93 | node, 94 | tokens: node.comments, 95 | firstLineOffset: 2, 96 | message: "Detected potential trojan source attack with unicode bidi introduced in this comment: '{{text}}'.", 97 | }); 98 | }, 99 | }; 100 | }, 101 | }; 102 | -------------------------------------------------------------------------------- /rules/detect-buffer-noassert.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tries to detect buffer read / write calls that use noAssert set to true 3 | * @author Adam Baldwin 4 | */ 5 | 6 | 'use strict'; 7 | 8 | //----------------------------------------------------------------------------- 9 | // Helpers 10 | //----------------------------------------------------------------------------- 11 | 12 | const read = [ 13 | 'readUInt8', 14 | 'readUInt16LE', 15 | 'readUInt16BE', 16 | 'readUInt32LE', 17 | 'readUInt32BE', 18 | 'readInt8', 19 | 'readInt16LE', 20 | 'readInt16BE', 21 | 'readInt32LE', 22 | 'readInt32BE', 23 | 'readFloatLE', 24 | 'readFloatBE', 25 | 'readDoubleLE', 26 | 'readDoubleBE', 27 | ]; 28 | 29 | const write = [ 30 | 'writeUInt8', 31 | 'writeUInt16LE', 32 | 'writeUInt16BE', 33 | 'writeUInt32LE', 34 | 'writeUInt32BE', 35 | 'writeInt8', 36 | 'writeInt16LE', 37 | 'writeInt16BE', 38 | 'writeInt32LE', 39 | 'writeInt32BE', 40 | 'writeFloatLE', 41 | 'writeFloatBE', 42 | 'writeDoubleLE', 43 | 'writeDoubleBE', 44 | ]; 45 | 46 | //------------------------------------------------------------------------------ 47 | // Rule Definition 48 | //------------------------------------------------------------------------------ 49 | 50 | module.exports = { 51 | meta: { 52 | type: 'error', 53 | docs: { 54 | description: 'Detects calls to "buffer" with "noAssert" flag set.', 55 | category: 'Possible Security Vulnerability', 56 | recommended: true, 57 | url: 'https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-buffer-noassert.md', 58 | }, 59 | __methodsToCheck: { 60 | read, 61 | write, 62 | }, 63 | }, 64 | create(context) { 65 | return { 66 | MemberExpression: function (node) { 67 | let index; 68 | if (read.indexOf(node.property.name) !== -1) { 69 | index = 1; 70 | } else if (write.indexOf(node.property.name) !== -1) { 71 | index = 2; 72 | } 73 | 74 | if (index && node.parent && node.parent.arguments && node.parent.arguments[index] && node.parent.arguments[index].value) { 75 | return context.report({ node: node, message: `Found Buffer.${node.property.name} with noAssert flag set true` }); 76 | } 77 | }, 78 | }; 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /rules/detect-child-process.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tries to detect instances of child_process 3 | * @author Adam Baldwin 4 | */ 5 | 6 | 'use strict'; 7 | 8 | const { getImportAccessPath } = require('../utils/import-utils'); 9 | const { isStaticExpression } = require('../utils/is-static-expression'); 10 | const childProcessPackageNames = ['child_process', 'node:child_process']; 11 | 12 | //------------------------------------------------------------------------------ 13 | // Rule Definition 14 | //------------------------------------------------------------------------------ 15 | 16 | module.exports = { 17 | meta: { 18 | type: 'error', 19 | docs: { 20 | description: 'Detects instances of "child_process" & non-literal "exec()" calls.', 21 | category: 'Possible Security Vulnerability', 22 | recommended: true, 23 | url: 'https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-child-process.md', 24 | }, 25 | }, 26 | create(context) { 27 | const sourceCode = context.sourceCode || context.getSourceCode(); 28 | return { 29 | CallExpression: function (node) { 30 | if (node.callee.name === 'require') { 31 | const args = node.arguments[0]; 32 | if ( 33 | args && 34 | args.type === 'Literal' && 35 | childProcessPackageNames.includes(args.value) && 36 | node.parent.type !== 'VariableDeclarator' && 37 | node.parent.type !== 'AssignmentExpression' && 38 | node.parent.type !== 'MemberExpression' 39 | ) { 40 | context.report({ node: node, message: 'Found require("' + args.value + '")' }); 41 | } 42 | return; 43 | } 44 | 45 | const scope = sourceCode.getScope ? sourceCode.getScope(node) : context.getScope(); 46 | 47 | // Reports non-literal `exec()` calls. 48 | if ( 49 | !node.arguments.length || 50 | isStaticExpression({ 51 | node: node.arguments[0], 52 | scope, 53 | }) 54 | ) { 55 | return; 56 | } 57 | const pathInfo = getImportAccessPath({ 58 | node: node.callee, 59 | scope, 60 | packageNames: childProcessPackageNames, 61 | }); 62 | const fnName = pathInfo && pathInfo.path.length === 1 && pathInfo.path[0]; 63 | if (fnName !== 'exec') { 64 | return; 65 | } 66 | context.report({ node: node, message: 'Found child_process.exec() with non Literal first argument' }); 67 | }, 68 | }; 69 | }, 70 | }; 71 | -------------------------------------------------------------------------------- /rules/detect-disable-mustache-escape.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | meta: { 5 | type: 'error', 6 | docs: { 7 | description: 'Detects "object.escapeMarkup = false", which can be used with some template engines to disable escaping of HTML entities.', 8 | category: 'Possible Security Vulnerability', 9 | recommended: true, 10 | url: 'https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-disable-mustache-escape.md', 11 | }, 12 | }, 13 | create(context) { 14 | return { 15 | AssignmentExpression: function (node) { 16 | if (node.operator === '=') { 17 | if (node.left.property) { 18 | if (node.left.property.name === 'escapeMarkup') { 19 | if (node.right.value === false) { 20 | context.report({ node: node, message: 'Markup escaping disabled.' }); 21 | } 22 | } 23 | } 24 | } 25 | }, 26 | }; 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /rules/detect-eval-with-expression.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Identifies eval with expression 3 | * @author Adam Baldwin 4 | */ 5 | 6 | 'use strict'; 7 | 8 | //------------------------------------------------------------------------------ 9 | // Rule Definition 10 | //------------------------------------------------------------------------------ 11 | 12 | module.exports = { 13 | meta: { 14 | type: 'error', 15 | docs: { 16 | description: 'Detects "eval(variable)" which can allow an attacker to run arbitrary code inside your process.', 17 | category: 'Possible Security Vulnerability', 18 | recommended: true, 19 | url: 'https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-eval-with-expression.md', 20 | }, 21 | }, 22 | create(context) { 23 | return { 24 | CallExpression(node) { 25 | if (node.callee.name === 'eval' && node.arguments.length && node.arguments[0].type !== 'Literal') { 26 | context.report({ node: node, message: `eval with argument of type ${node.arguments[0].type}` }); 27 | } 28 | }, 29 | }; 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /rules/detect-new-buffer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | meta: { 5 | type: 'error', 6 | docs: { 7 | description: 'Detects instances of new Buffer(argument) where argument is any non-literal value.', 8 | category: 'Possible Security Vulnerability', 9 | recommended: true, 10 | url: 'https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-new-buffer.md', 11 | }, 12 | }, 13 | create(context) { 14 | return { 15 | NewExpression: function (node) { 16 | if (node.callee.name === 'Buffer' && node.arguments[0] && node.arguments[0].type !== 'Literal') { 17 | return context.report({ node: node, message: 'Found new Buffer' }); 18 | } 19 | }, 20 | }; 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /rules/detect-no-csrf-before-method-override.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Check and see if CSRF middleware is before methodOverride 3 | * @author Adam Baldwin 4 | */ 5 | 6 | 'use strict'; 7 | 8 | //------------------------------------------------------------------------------ 9 | // Rule Definition 10 | //------------------------------------------------------------------------------ 11 | 12 | module.exports = { 13 | meta: { 14 | type: 'error', 15 | docs: { 16 | description: 'Detects Express "csrf" middleware setup before "method-override" middleware.', 17 | category: 'Possible Security Vulnerability', 18 | recommended: true, 19 | url: 'https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-no-csrf-before-method-override.md', 20 | }, 21 | }, 22 | create(context) { 23 | let csrf = false; 24 | 25 | return { 26 | CallExpression: function (node) { 27 | const token = context.getSourceCode().getTokens(node)[0]; 28 | const nodeValue = token.value; 29 | 30 | if (nodeValue === 'express') { 31 | if (!node.callee || !node.callee.property) { 32 | return; 33 | } 34 | 35 | if (node.callee.property.name === 'methodOverride' && csrf) { 36 | context.report({ node: node, message: 'express.csrf() middleware found before express.methodOverride()' }); 37 | } 38 | if (node.callee.property.name === 'csrf') { 39 | // Keep track of found CSRF 40 | csrf = true; 41 | } 42 | } 43 | }, 44 | }; 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /rules/detect-non-literal-fs-filename.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tries to detect calls to fs functions that take a non Literal value as the filename parameter 3 | * @author Adam Baldwin 4 | */ 5 | 6 | 'use strict'; 7 | 8 | const fsMetaData = require('../utils/data/fsFunctionData.json'); 9 | const funcNames = Object.keys(fsMetaData); 10 | const fsPackageNames = ['fs', 'node:fs', 'fs/promises', 'node:fs/promises', 'fs-extra']; 11 | 12 | const { getImportAccessPath } = require('../utils/import-utils'); 13 | const { isStaticExpression } = require('../utils/is-static-expression'); 14 | 15 | //------------------------------------------------------------------------------ 16 | // Rule Definition 17 | //------------------------------------------------------------------------------ 18 | 19 | module.exports = { 20 | meta: { 21 | type: 'error', 22 | docs: { 23 | description: 'Detects variable in filename argument of "fs" calls, which might allow an attacker to access anything on your system.', 24 | category: 'Possible Security Vulnerability', 25 | recommended: true, 26 | url: 'https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-non-literal-fs-filename.md', 27 | }, 28 | }, 29 | create(context) { 30 | const sourceCode = context.sourceCode || context.getSourceCode(); 31 | return { 32 | CallExpression(node) { 33 | // don't check require. If all arguments are Literals, it's surely safe! 34 | if ((node.callee.type === 'Identifier' && node.callee.name === 'require') || node.arguments.every((argument) => argument.type === 'Literal')) { 35 | return; 36 | } 37 | 38 | const scope = sourceCode.getScope ? sourceCode.getScope(node) : context.getScope(); 39 | const pathInfo = getImportAccessPath({ 40 | node: node.callee, 41 | scope, 42 | packageNames: fsPackageNames, 43 | }); 44 | if (!pathInfo) { 45 | return; 46 | } 47 | let fnName; 48 | if (pathInfo.path.length === 1) { 49 | // Check for: 50 | // | var something = require('fs').readFile; 51 | // | something(a); 52 | // , 53 | // | var something = require('fs'); 54 | // | something.readFile(c); 55 | // , 56 | // | var { readFile: something } = require('fs') 57 | // | readFile(filename); 58 | // , 59 | // | import { readFile as something } from 'fs'; 60 | // | something(filename); 61 | // , or 62 | // | import * as something from 'fs'; 63 | // | something.readFile(c); 64 | fnName = pathInfo.path[0]; 65 | } else if (pathInfo.path.length === 2) { 66 | // Check for: 67 | // | var something = require('fs').promises; 68 | // | something.readFile(filename) 69 | fnName = pathInfo.path[1]; 70 | } else { 71 | return; 72 | } 73 | if (!funcNames.includes(fnName)) { 74 | return false; 75 | } 76 | const packageName = pathInfo.packageName; 77 | 78 | const indices = []; 79 | for (const index of fsMetaData[fnName] || []) { 80 | if (index >= node.arguments.length) { 81 | continue; 82 | } 83 | const argument = node.arguments[index]; 84 | 85 | if (isStaticExpression({ node: argument, scope })) { 86 | continue; 87 | } 88 | indices.push(index); 89 | } 90 | if (indices.length) { 91 | context.report({ 92 | node, 93 | message: `Found ${fnName} from package "${packageName}" with non literal argument at index ${indices.join(',')}`, 94 | }); 95 | } 96 | }, 97 | }; 98 | }, 99 | }; 100 | -------------------------------------------------------------------------------- /rules/detect-non-literal-regexp.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tries to detect RegExp's created from non-literal strings. 3 | * @author Jon Lamendola 4 | */ 5 | 6 | 'use strict'; 7 | 8 | const { isStaticExpression } = require('../utils/is-static-expression'); 9 | 10 | //------------------------------------------------------------------------------ 11 | // Rule Definition 12 | //------------------------------------------------------------------------------ 13 | 14 | module.exports = { 15 | meta: { 16 | type: 'error', 17 | docs: { 18 | description: 'Detects "RegExp(variable)", which might allow an attacker to DOS your server with a long-running regular expression.', 19 | category: 'Possible Security Vulnerability', 20 | recommended: true, 21 | url: 'https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-non-literal-regexp.md', 22 | }, 23 | }, 24 | create(context) { 25 | const sourceCode = context.sourceCode || context.getSourceCode(); 26 | 27 | return { 28 | NewExpression(node) { 29 | if (node.callee.name === 'RegExp') { 30 | const args = node.arguments; 31 | const scope = sourceCode.getScope ? sourceCode.getScope(node) : context.getScope(); 32 | 33 | if ( 34 | args && 35 | args.length > 0 && 36 | !isStaticExpression({ 37 | node: args[0], 38 | scope, 39 | }) 40 | ) { 41 | return context.report({ node: node, message: 'Found non-literal argument to RegExp Constructor' }); 42 | } 43 | } 44 | }, 45 | }; 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /rules/detect-non-literal-require.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tries to detect calls to require with non-literal argument 3 | * @author Adam Baldwin 4 | */ 5 | 6 | 'use strict'; 7 | 8 | const { isStaticExpression } = require('../utils/is-static-expression'); 9 | 10 | //------------------------------------------------------------------------------ 11 | // Rule Definition 12 | //------------------------------------------------------------------------------ 13 | 14 | module.exports = { 15 | meta: { 16 | type: 'error', 17 | docs: { 18 | description: 'Detects "require(variable)", which might allow an attacker to load and run arbitrary code, or access arbitrary files on disk.', 19 | category: 'Possible Security Vulnerability', 20 | recommended: true, 21 | url: 'https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-non-literal-require.md', 22 | }, 23 | }, 24 | create(context) { 25 | const sourceCode = context.sourceCode || context.getSourceCode(); 26 | 27 | return { 28 | CallExpression(node) { 29 | if (node.callee.name === 'require') { 30 | const args = node.arguments; 31 | const scope = sourceCode.getScope ? sourceCode.getScope(node) : context.getScope(); 32 | 33 | if ( 34 | args && 35 | args.length > 0 && 36 | !isStaticExpression({ 37 | node: args[0], 38 | scope, 39 | }) 40 | ) { 41 | return context.report({ node: node, message: 'Found non-literal argument in require' }); 42 | } 43 | } 44 | }, 45 | }; 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /rules/detect-object-injection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tries to detect instances of var[var] 3 | * @author Jon Lamendola 4 | */ 5 | 6 | 'use strict'; 7 | 8 | //------------------------------------------------------------------------------ 9 | // Rule Definition 10 | //------------------------------------------------------------------------------ 11 | 12 | const getPath = (value, seen, keys) => { 13 | let index = seen.indexOf(value); 14 | const path = [keys[index]]; 15 | for (index--; index >= 0; index--) { 16 | if (seen[index][path[0]] === value) { 17 | value = seen[index]; 18 | path.unshift(keys[index]); 19 | } 20 | } 21 | return `~${path.join('.')}`; 22 | }; 23 | 24 | const getSerialize = (fn, decycle) => { 25 | const seen = []; 26 | const keys = []; 27 | decycle = 28 | decycle || 29 | function (key, value) { 30 | return `[Circular ${getPath(value, seen, keys)}]`; 31 | }; 32 | return function (key, value) { 33 | let ret = value; 34 | if (typeof value === 'object' && value) { 35 | if (seen.indexOf(value) !== -1) { 36 | ret = decycle(key, value); 37 | } else { 38 | seen.push(value); 39 | keys.push(key); 40 | } 41 | } 42 | if (fn) { 43 | ret = fn(key, ret); 44 | } 45 | return ret; 46 | }; 47 | }; 48 | 49 | const stringify = (obj, fn, spaces, decycle) => { 50 | return JSON.stringify(obj, getSerialize(fn, decycle), spaces); 51 | }; 52 | 53 | stringify.getSerialize = getSerialize; 54 | module.exports = { 55 | meta: { 56 | type: 'error', 57 | docs: { 58 | description: 'Detects "variable[key]" as a left- or right-hand assignment operand.', 59 | category: 'Possible Security Vulnerability', 60 | recommended: true, 61 | url: 'https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-object-injection.md', 62 | }, 63 | }, 64 | create(context) { 65 | return { 66 | MemberExpression: function (node) { 67 | if (node.computed === true) { 68 | if (node.property.type === 'Identifier') { 69 | if (node.parent.type === 'VariableDeclarator') { 70 | context.report({ node: node, message: 'Variable Assigned to Object Injection Sink' }); 71 | } else if (node.parent.type === 'CallExpression') { 72 | context.report({ node: node, message: 'Function Call Object Injection Sink' }); 73 | } else { 74 | context.report({ node: node, message: 'Generic Object Injection Sink' }); 75 | } 76 | } 77 | } 78 | }, 79 | }; 80 | }, 81 | }; 82 | -------------------------------------------------------------------------------- /rules/detect-possible-timing-attacks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Looks for potential hotspot string comparisons 3 | * @author Adam Baldwin / Jon Lamendola 4 | */ 5 | 6 | 'use strict'; 7 | 8 | //------------------------------------------------------------------------------ 9 | // Rule Definition 10 | //------------------------------------------------------------------------------ 11 | 12 | const keywords = `((${['password', 'secret', 'api', 'apiKey', 'token', 'auth', 'pass', 'hash'].join(')|(')}))`; 13 | 14 | const re = new RegExp(`^${keywords}$`, 'im'); 15 | 16 | const containsKeyword = (node) => { 17 | if (node.type === 'Identifier') { 18 | if (re.test(node.name)) { 19 | return true; 20 | } 21 | } 22 | return; 23 | }; 24 | 25 | module.exports = { 26 | meta: { 27 | type: 'error', 28 | docs: { 29 | description: 'Detects insecure comparisons (`==`, `!=`, `!==` and `===`), which check input sequentially.', 30 | category: 'Possible Security Vulnerability', 31 | recommended: true, 32 | url: 'https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-possible-timing-attacks.md', 33 | }, 34 | }, 35 | create(context) { 36 | return { 37 | IfStatement: function (node) { 38 | if (node.test && node.test.type === 'BinaryExpression') { 39 | if (node.test.operator === '==' || node.test.operator === '===' || node.test.operator === '!=' || node.test.operator === '!==') { 40 | if (node.test.left) { 41 | const left = containsKeyword(node.test.left); 42 | if (left) { 43 | return context.report({ node: node, message: `Potential timing attack, left side: ${left}` }); 44 | } 45 | } 46 | 47 | if (node.test.right) { 48 | const right = containsKeyword(node.test.right); 49 | if (right) { 50 | return context.report({ node: node, message: `Potential timing attack, right side: ${right}` }); 51 | } 52 | } 53 | } 54 | } 55 | }, 56 | }; 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /rules/detect-pseudoRandomBytes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tries to detect crypto.pseudoRandomBytes cause it's not cryptographical strong 3 | * @author Adam Baldwin 4 | */ 5 | 6 | 'use strict'; 7 | 8 | //------------------------------------------------------------------------------ 9 | // Rule Definition 10 | //------------------------------------------------------------------------------ 11 | 12 | module.exports = { 13 | meta: { 14 | type: 'error', 15 | docs: { 16 | description: 'Detects if "pseudoRandomBytes()" is in use, which might not give you the randomness you need and expect.', 17 | category: 'Possible Security Vulnerability', 18 | recommended: true, 19 | url: 'https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-pseudoRandomBytes.md', 20 | }, 21 | }, 22 | create(context) { 23 | return { 24 | MemberExpression: function (node) { 25 | if (node.property.name === 'pseudoRandomBytes') { 26 | return context.report({ node: node, message: 'Found crypto.pseudoRandomBytes which does not produce cryptographically strong numbers' }); 27 | } 28 | }, 29 | }; 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /rules/detect-unsafe-regex.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Check if the regex is evil or not using the safe-regex module 3 | * @author Adam Baldwin 4 | */ 5 | 6 | 'use strict'; 7 | 8 | //----------------------------------------------------------------------------- 9 | // Requirements 10 | //----------------------------------------------------------------------------- 11 | 12 | const safe = require('safe-regex'); 13 | 14 | //------------------------------------------------------------------------------ 15 | // Rule Definition 16 | //------------------------------------------------------------------------------ 17 | 18 | module.exports = { 19 | meta: { 20 | type: 'error', 21 | docs: { 22 | description: 'Detects potentially unsafe regular expressions, which may take a very long time to run, blocking the event loop.', 23 | category: 'Possible Security Vulnerability', 24 | recommended: true, 25 | url: 'https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-unsafe-regex.md', 26 | }, 27 | }, 28 | create(context) { 29 | return { 30 | Literal: function (node) { 31 | const token = context.getSourceCode().getTokens(node)[0]; 32 | const nodeType = token.type; 33 | const nodeValue = token.value; 34 | 35 | if (nodeType === 'RegularExpression') { 36 | if (!safe(nodeValue)) { 37 | context.report({ node: node, message: 'Unsafe Regular Expression' }); 38 | } 39 | } 40 | }, 41 | NewExpression: function (node) { 42 | if (node.callee.name === 'RegExp' && node.arguments && node.arguments.length > 0 && node.arguments[0].type === 'Literal') { 43 | if (!safe(node.arguments[0].value)) { 44 | context.report({ node: node, message: 'Unsafe Regular Expression (new RegExp)' }); 45 | } 46 | } 47 | }, 48 | }; 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /test/configs/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const plugin = require('../../index.js'); 3 | const assert = require('assert').strict; 4 | 5 | describe('export plugin object', () => { 6 | it('should export rules', () => { 7 | assert(plugin.rules); 8 | assert(typeof plugin.rules['detect-unsafe-regex'] === 'object'); 9 | }); 10 | 11 | it('should export configs', () => { 12 | assert(plugin.configs); 13 | assert(plugin.configs['recommended']); 14 | assert(plugin.configs['recommended-legacy']); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /test/rules/detect-bidi-characters.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const RuleTester = require('eslint').RuleTester; 4 | const tester = new RuleTester(); 5 | 6 | const ruleName = 'detect-bidi-characters'; 7 | const Rule = require(`../../rules/${ruleName}`); 8 | 9 | tester.run(ruleName, Rule, { 10 | valid: [ 11 | { 12 | code: ` 13 | var accessLevel = "user"; 14 | if (accessLevel != "user") { // Check if admin 15 | console.log("You are an admin."); 16 | } 17 | `, 18 | }, 19 | ], 20 | invalid: [ 21 | { 22 | code: ` 23 | var accessLevel = "user"; 24 | if (accessLevel != "user‮ ⁦// Check if admin⁩ ⁦") { 25 | console.log("You are an admin."); 26 | } 27 | `, 28 | errors: [ 29 | { message: /Detected potential trojan source attack with unicode bidi introduced in this code/i, line: 3, endLine: 3, column: 31, endColumn: 32 }, 30 | { message: /Detected potential trojan source attack with unicode bidi introduced in this code/i, line: 3, endLine: 3, column: 33, endColumn: 34 }, 31 | { message: /Detected potential trojan source attack with unicode bidi introduced in this code/i, line: 3, endLine: 3, column: 51, endColumn: 52 }, 32 | { message: /Detected potential trojan source attack with unicode bidi introduced in this code/i, line: 3, endLine: 3, column: 53, endColumn: 54 }, 33 | ], 34 | }, 35 | ], 36 | }); 37 | 38 | tester.run(`${ruleName} in comment-line`, Rule, { 39 | valid: [ 40 | { 41 | code: ` 42 | var isAdmin = false; 43 | /* begin admins only */ if (isAdmin) { 44 | console.log("You are an admin."); 45 | /* end admins only */ } 46 | `, 47 | }, 48 | ], 49 | invalid: [ 50 | { 51 | code: ` 52 | var isAdmin = false; 53 | /*‮ } ⁦if (isAdmin)⁩ ⁦ begin admins only */ 54 | console.log("You are an admin."); 55 | /* end admins only ‮ 56 | ⁦*/ 57 | /* end admins only ‮ 58 | { ⁦*/ 59 | `, 60 | errors: [ 61 | { message: /Detected potential trojan source attack with unicode bidi introduced in this comment/i, line: 3, endLine: 3, column: 9, endColumn: 10 }, 62 | { message: /Detected potential trojan source attack with unicode bidi introduced in this comment/i, line: 3, endLine: 3, column: 13, endColumn: 14 }, 63 | { message: /Detected potential trojan source attack with unicode bidi introduced in this comment/i, line: 3, endLine: 3, column: 26, endColumn: 27 }, 64 | { message: /Detected potential trojan source attack with unicode bidi introduced in this comment/i, line: 3, endLine: 3, column: 28, endColumn: 29 }, 65 | 66 | { message: /Detected potential trojan source attack with unicode bidi introduced in this comment/i, line: 5, endLine: 5, column: 26, endColumn: 27 }, 67 | { message: /Detected potential trojan source attack with unicode bidi introduced in this comment/i, line: 6, endLine: 6, column: 1, endColumn: 2 }, 68 | 69 | { message: /Detected potential trojan source attack with unicode bidi introduced in this comment/i, line: 7, endLine: 7, column: 26, endColumn: 27 }, 70 | { message: /Detected potential trojan source attack with unicode bidi introduced in this comment/i, line: 8, endLine: 8, column: 4, endColumn: 5 }, 71 | ], 72 | }, 73 | ], 74 | }); 75 | -------------------------------------------------------------------------------- /test/rules/detect-buffer-noassert.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const RuleTester = require('eslint').RuleTester; 4 | const tester = new RuleTester(); 5 | 6 | const ruleName = 'detect-buffer-noassert'; 7 | const rule = require(`../../rules/${ruleName}`); 8 | 9 | const allMethodNames = [...rule.meta.__methodsToCheck.read, ...rule.meta.__methodsToCheck.write]; 10 | 11 | tester.run(ruleName, rule, { 12 | valid: [...allMethodNames.map((methodName) => `a.${methodName}(0)`), ...allMethodNames.map((methodName) => `a.${methodName}(0, false)`)], 13 | invalid: [ 14 | ...rule.meta.__methodsToCheck.read.map((methodName) => ({ 15 | code: `a.${methodName}(0, true)`, 16 | errors: [{ message: `Found Buffer.${methodName} with noAssert flag set true` }], 17 | })), 18 | 19 | ...rule.meta.__methodsToCheck.write.map((methodName) => ({ 20 | code: `a.${methodName}(0, 0, true)`, 21 | errors: [{ message: `Found Buffer.${methodName} with noAssert flag set true` }], 22 | })), 23 | 24 | // hard-coded test to ensure #63 is fixed 25 | { 26 | code: 'a.readDoubleLE(0, true);', 27 | errors: [{ message: 'Found Buffer.readDoubleLE with noAssert flag set true' }], 28 | }, 29 | ], 30 | }); 31 | -------------------------------------------------------------------------------- /test/rules/detect-child-process.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const RuleTester = require('eslint').RuleTester; 4 | const tester = new RuleTester(); 5 | 6 | const ruleName = 'detect-child-process'; 7 | const rule = require(`../../rules/${ruleName}`); 8 | 9 | tester.run(ruleName, rule, { 10 | valid: [ 11 | "child_process.exec('ls')", 12 | ` 13 | var {} = require('child_process'); 14 | var result = /hello/.exec(str);`, 15 | ` 16 | var {} = require('node:child_process'); 17 | var result = /hello/.exec(str);`, 18 | ` 19 | import {} from 'child_process'; 20 | var result = /hello/.exec(str);`, 21 | ` 22 | import {} from 'node:child_process'; 23 | var result = /hello/.exec(str);`, 24 | "var { spawn } = require('child_process'); spawn(str);", 25 | "var { spawn } = require('node:child_process'); spawn(str);", 26 | "import { spawn } from 'child_process'; spawn(str);", 27 | "import { spawn } from 'node:child_process'; spawn(str);", 28 | ` 29 | var foo = require('child_process'); 30 | function fn () { 31 | var foo = /hello/; 32 | var result = foo.exec(str); 33 | }`, 34 | "var child = require('child_process'); child.spawn(str)", 35 | "var child = require('node:child_process'); child.spawn(str)", 36 | "import child from 'child_process'; child.spawn(str)", 37 | "import child from 'node:child_process'; child.spawn(str)", 38 | ` 39 | var foo = require('child_process'); 40 | function fn () { 41 | var result = foo.spawn(str); 42 | }`, 43 | "require('child_process').spawn(str)", 44 | ` 45 | function fn () { 46 | require('child_process').spawn(str) 47 | }`, 48 | ` 49 | var child_process = require('child_process'); 50 | var FOO = 'ls'; 51 | child_process.exec(FOO);`, 52 | ` 53 | import child_process from 'child_process'; 54 | const FOO = 'ls'; 55 | child_process.exec(FOO);`, 56 | ], 57 | invalid: [ 58 | { 59 | code: "require('child_process')", 60 | errors: [{ message: 'Found require("child_process")' }], 61 | }, 62 | { 63 | code: "require('node:child_process')", 64 | errors: [{ message: 'Found require("node:child_process")' }], 65 | }, 66 | { 67 | code: "var child = require('child_process'); child.exec(com)", 68 | errors: [{ message: 'Found child_process.exec() with non Literal first argument' }], 69 | }, 70 | { 71 | code: "var child = require('node:child_process'); child.exec(com)", 72 | errors: [{ message: 'Found child_process.exec() with non Literal first argument' }], 73 | }, 74 | { 75 | code: "import child from 'child_process'; child.exec(com)", 76 | errors: [{ message: 'Found child_process.exec() with non Literal first argument' }], 77 | }, 78 | { 79 | code: "import child from 'node:child_process'; child.exec(com)", 80 | errors: [{ message: 'Found child_process.exec() with non Literal first argument' }], 81 | }, 82 | { 83 | code: "var child = sinon.stub(require('child_process')); child.exec.returns({});", 84 | errors: [{ message: 'Found require("child_process")' }], 85 | }, 86 | { 87 | code: "var child = sinon.stub(require('node:child_process')); child.exec.returns({});", 88 | errors: [{ message: 'Found require("node:child_process")' }], 89 | }, 90 | { 91 | code: ` 92 | var foo = require('child_process'); 93 | function fn () { 94 | var result = foo.exec(str); 95 | }`, 96 | errors: [{ message: 'Found child_process.exec() with non Literal first argument', line: 4 }], 97 | }, 98 | { 99 | code: ` 100 | import foo from 'child_process'; 101 | function fn () { 102 | var result = foo.exec(str); 103 | }`, 104 | errors: [{ message: 'Found child_process.exec() with non Literal first argument', line: 4 }], 105 | }, 106 | { 107 | code: ` 108 | import foo from 'node:child_process'; 109 | function fn () { 110 | var result = foo.exec(str); 111 | }`, 112 | errors: [{ message: 'Found child_process.exec() with non Literal first argument', line: 4 }], 113 | }, 114 | { 115 | code: ` 116 | require('child_process').exec(str)`, 117 | errors: [{ message: 'Found child_process.exec() with non Literal first argument', line: 2 }], 118 | }, 119 | { 120 | code: ` 121 | function fn () { 122 | require('child_process').exec(str) 123 | }`, 124 | errors: [{ message: 'Found child_process.exec() with non Literal first argument', line: 3 }], 125 | }, 126 | { 127 | code: ` 128 | const {exec} = require('child_process'); 129 | exec(str)`, 130 | errors: [{ message: 'Found child_process.exec() with non Literal first argument', line: 3 }], 131 | }, 132 | { 133 | code: ` 134 | const {exec} = require('node:child_process'); 135 | exec(str)`, 136 | errors: [{ message: 'Found child_process.exec() with non Literal first argument', line: 3 }], 137 | }, 138 | ], 139 | }); 140 | -------------------------------------------------------------------------------- /test/rules/detect-disable-mustache-escape.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const RuleTester = require('eslint').RuleTester; 4 | const tester = new RuleTester(); 5 | 6 | const ruleName = 'detect-disable-mustache-escape'; 7 | 8 | tester.run(ruleName, require(`../../rules/${ruleName}`), { 9 | valid: [{ code: 'escapeMarkup = false' }], 10 | invalid: [ 11 | { 12 | code: 'a.escapeMarkup = false', 13 | errors: [{ message: 'Markup escaping disabled.' }], 14 | }, 15 | ], 16 | }); 17 | -------------------------------------------------------------------------------- /test/rules/detect-eval-with-expression.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const RuleTester = require('eslint').RuleTester; 4 | const tester = new RuleTester(); 5 | 6 | const ruleName = 'detect-eval-with-expression'; 7 | 8 | tester.run(ruleName, require(`../../rules/${ruleName}`), { 9 | valid: [{ code: "eval('alert()')" }, { code: 'eval("some nefarious code");' }, { code: 'eval()' }], 10 | invalid: [ 11 | { 12 | code: 'eval(a);', 13 | errors: [{ message: 'eval with argument of type Identifier' }], 14 | }, 15 | ], 16 | }); 17 | -------------------------------------------------------------------------------- /test/rules/detect-new-buffer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const RuleTester = require('eslint').RuleTester; 4 | const tester = new RuleTester(); 5 | 6 | const ruleName = 'detect-new-buffer'; 7 | const invalid = 'var a = new Buffer(c)'; 8 | 9 | tester.run(ruleName, require(`../../rules/${ruleName}`), { 10 | valid: [{ code: "var a = new Buffer('test')" }], 11 | invalid: [ 12 | { 13 | code: invalid, 14 | errors: [{ message: 'Found new Buffer' }], 15 | }, 16 | ], 17 | }); 18 | -------------------------------------------------------------------------------- /test/rules/detect-no-csrf-before-method-override.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const RuleTester = require('eslint').RuleTester; 4 | const tester = new RuleTester(); 5 | 6 | const ruleName = 'detect-no-csrf-before-method-override'; 7 | 8 | tester.run(ruleName, require(`../../rules/${ruleName}`), { 9 | valid: [{ code: 'express.methodOverride();express.csrf()' }], 10 | invalid: [ 11 | { 12 | code: 'express.csrf();express.methodOverride()', 13 | errors: [{ message: 'express.csrf() middleware found before express.methodOverride()' }], 14 | }, 15 | ], 16 | }); 17 | -------------------------------------------------------------------------------- /test/rules/detect-non-literal-fs-filename.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const RuleTester = require('eslint').RuleTester; 4 | const tester = new RuleTester(); 5 | 6 | const ruleName = 'detect-non-literal-fs-filename'; 7 | 8 | tester.run(ruleName, require(`../../rules/${ruleName}`), { 9 | valid: [ 10 | { 11 | code: `var fs = require('fs'); 12 | var a = fs.open('test')`, 13 | }, 14 | { 15 | code: `var something = require('some'); 16 | var a = something.readFile(c);`, 17 | }, 18 | { 19 | code: `var something = require('fs').readFile, readFile = require('foo').readFile; 20 | readFile(c);`, 21 | }, 22 | { 23 | code: ` 24 | import { promises as fsp } from 'fs'; 25 | import fs from 'fs'; 26 | import path from 'path'; 27 | 28 | const index = await fsp.readFile(path.resolve(__dirname, './index.html'), 'utf-8'); 29 | const key = fs.readFileSync(path.join(__dirname, './ssl.key')); 30 | await fsp.writeFile(path.resolve(__dirname, './sitemap.xml'), sitemap);`, 31 | languageOptions: { 32 | globals: { 33 | __dirname: 'readonly', 34 | }, 35 | }, 36 | }, 37 | { 38 | code: ` 39 | import fs from 'fs'; 40 | import path from 'path'; 41 | const dirname = path.dirname(__filename) 42 | const key = fs.readFileSync(path.resolve(dirname, './index.html'));`, 43 | languageOptions: { 44 | globals: { 45 | __filename: 'readonly', 46 | }, 47 | }, 48 | }, 49 | { 50 | code: ` 51 | import fs from 'fs'; 52 | const key = fs.readFileSync(\`\${process.cwd()}/path/to/foo.json\`);`, 53 | languageOptions: { 54 | globals: { 55 | process: 'readonly', 56 | }, 57 | }, 58 | }, 59 | ` 60 | import fs from 'fs'; 61 | import path from 'path'; 62 | import url from 'url'; 63 | const dirname = path.dirname(url.fileURLToPath(import.meta.url)); 64 | const html = fs.readFileSync(path.resolve(dirname, './index.html'), 'utf-8');`, 65 | { 66 | code: ` 67 | import fs from 'fs'; 68 | const pkg = fs.readFileSync(require.resolve('eslint/package.json'), 'utf-8');`, 69 | languageOptions: { 70 | globals: { 71 | require: 'readonly', 72 | }, 73 | }, 74 | }, 75 | ], 76 | invalid: [ 77 | /// requires 78 | { 79 | code: `var something = require('fs'); 80 | var a = something.open(c);`, 81 | errors: [{ message: 'Found open from package "fs" with non literal argument at index 0' }], 82 | }, 83 | { 84 | code: `var one = require('fs').readFile; 85 | one(filename);`, 86 | errors: [{ message: 'Found readFile from package "fs" with non literal argument at index 0' }], 87 | }, 88 | { 89 | code: `var one = require('node:fs').readFile; 90 | one(filename);`, 91 | errors: [{ message: 'Found readFile from package "node:fs" with non literal argument at index 0' }], 92 | }, 93 | { 94 | code: `var one = require('fs/promises').readFile; 95 | one(filename);`, 96 | errors: [{ message: 'Found readFile from package "fs/promises" with non literal argument at index 0' }], 97 | }, 98 | { 99 | code: `var something = require('fs/promises'); 100 | something.readFile(filename);`, 101 | errors: [{ message: 'Found readFile from package "fs/promises" with non literal argument at index 0' }], 102 | }, 103 | { 104 | code: `var something = require('node:fs/promises'); 105 | something.readFile(filename);`, 106 | errors: [{ message: 'Found readFile from package "node:fs/promises" with non literal argument at index 0' }], 107 | }, 108 | { 109 | code: `var something = require('fs-extra'); 110 | something.readFile(filename);`, 111 | errors: [{ message: 'Found readFile from package "fs-extra" with non literal argument at index 0' }], 112 | }, 113 | { 114 | code: `var { readFile: something } = require('fs'); 115 | something(filename)`, 116 | errors: [{ message: 'Found readFile from package "fs" with non literal argument at index 0' }], 117 | }, 118 | //// imports 119 | { 120 | code: `import { readFile as something } from 'fs'; 121 | something(filename);`, 122 | errors: [{ message: 'Found readFile from package "fs" with non literal argument at index 0' }], 123 | }, 124 | { 125 | code: `import { readFile as something } from 'node:fs'; 126 | something(filename);`, 127 | errors: [{ message: 'Found readFile from package "node:fs" with non literal argument at index 0' }], 128 | }, 129 | { 130 | code: `import { readFile as something } from 'fs-extra'; 131 | something(filename);`, 132 | errors: [{ message: 'Found readFile from package "fs-extra" with non literal argument at index 0' }], 133 | }, 134 | { 135 | code: `import { readFile as something } from 'fs/promises' 136 | something(filename)`, 137 | errors: [{ message: 'Found readFile from package "fs/promises" with non literal argument at index 0' }], 138 | }, 139 | { 140 | code: `import { readFile as something } from 'node:fs/promises' 141 | something(filename)`, 142 | errors: [{ message: 'Found readFile from package "node:fs/promises" with non literal argument at index 0' }], 143 | }, 144 | { 145 | code: `import * as something from 'fs'; 146 | something.readFile(filename);`, 147 | errors: [{ message: 'Found readFile from package "fs" with non literal argument at index 0' }], 148 | }, 149 | { 150 | code: `import * as something from 'node:fs'; 151 | something.readFile(filename);`, 152 | errors: [{ message: 'Found readFile from package "node:fs" with non literal argument at index 0' }], 153 | }, 154 | /// promises 155 | { 156 | code: `var something = require('fs').promises; 157 | something.readFile(filename)`, 158 | errors: [{ message: 'Found readFile from package "fs" with non literal argument at index 0' }], 159 | }, 160 | { 161 | code: `var something = require('node:fs').promises; 162 | something.readFile(filename)`, 163 | errors: [{ message: 'Found readFile from package "node:fs" with non literal argument at index 0' }], 164 | }, 165 | { 166 | code: `var something = require('fs'); 167 | something.promises.readFile(filename)`, 168 | errors: [{ message: 'Found readFile from package "fs" with non literal argument at index 0' }], 169 | }, 170 | { 171 | code: `var something = require('node:fs'); 172 | something.promises.readFile(filename)`, 173 | errors: [{ message: 'Found readFile from package "node:fs" with non literal argument at index 0' }], 174 | }, 175 | { 176 | code: "var fs = require('fs');\nfs.readFile(`template with ${filename}`);", 177 | errors: [{ message: 'Found readFile from package "fs" with non literal argument at index 0' }], 178 | }, 179 | // inline 180 | { 181 | code: "function foo () {\nvar fs = require('fs');\nfs.readFile(filename);\n}", 182 | errors: [{ message: 'Found readFile from package "fs" with non literal argument at index 0' }], 183 | }, 184 | { 185 | code: "function foo () {\nvar { readFile: something } = require('fs');\nsomething(filename);\n}", 186 | errors: [{ message: 'Found readFile from package "fs" with non literal argument at index 0' }], 187 | }, 188 | { 189 | code: "var fs = require('fs');\nfunction foo () {\nvar { readFile: something } = fs.promises;\nsomething(filename);\n}", 190 | errors: [{ message: 'Found readFile from package "fs" with non literal argument at index 0' }], 191 | }, 192 | { 193 | code: ` 194 | import fs from 'fs'; 195 | import path from 'path'; 196 | const key = fs.readFileSync(path.resolve(__dirname, foo));`, 197 | languageOptions: { 198 | globals: { 199 | __filename: 'readonly', 200 | }, 201 | }, 202 | errors: [{ message: 'Found readFileSync from package "fs" with non literal argument at index 0' }], 203 | }, 204 | ], 205 | }); 206 | -------------------------------------------------------------------------------- /test/rules/detect-non-literal-regexp.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const RuleTester = require('eslint').RuleTester; 4 | const tester = new RuleTester(); 5 | 6 | const ruleName = 'detect-non-literal-regexp'; 7 | const invalid = "var a = new RegExp(c, 'i')"; 8 | 9 | tester.run(ruleName, require(`../../rules/${ruleName}`), { 10 | valid: [ 11 | { code: "var a = new RegExp('ab+c', 'i')" }, 12 | { 13 | code: ` 14 | var source = 'ab+c' 15 | var a = new RegExp(source, 'i')`, 16 | }, 17 | ], 18 | invalid: [ 19 | { 20 | code: invalid, 21 | errors: [{ message: 'Found non-literal argument to RegExp Constructor' }], 22 | }, 23 | ], 24 | }); 25 | -------------------------------------------------------------------------------- /test/rules/detect-non-literal-require.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const RuleTester = require('eslint').RuleTester; 4 | 5 | const tester = new RuleTester({ languageOptions: { sourceType: 'commonjs' } }); 6 | 7 | const ruleName = 'detect-non-literal-require'; 8 | 9 | tester.run(ruleName, require(`../../rules/${ruleName}`), { 10 | valid: [ 11 | { code: "var a = require('b')" }, 12 | { code: 'var a = require(`b`)' }, 13 | { 14 | code: ` 15 | const d = 'debounce' 16 | var a = require(\`lodash/\${d}\`)`, 17 | }, 18 | { 19 | code: "const utils = require(__dirname + '/utils');", 20 | languageOptions: { 21 | globals: { 22 | __dirname: 'readonly', 23 | }, 24 | }, 25 | }, 26 | ], 27 | invalid: [ 28 | { 29 | code: 'var a = require(c)', 30 | errors: [{ message: 'Found non-literal argument in require' }], 31 | }, 32 | { 33 | code: 'var a = require(`${c}`)', 34 | errors: [{ message: 'Found non-literal argument in require' }], 35 | }, 36 | ], 37 | }); 38 | -------------------------------------------------------------------------------- /test/rules/detect-object-injection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const RuleTester = require('eslint').RuleTester; 4 | const tester = new RuleTester(); 5 | 6 | const ruleName = 'detect-object-injection'; 7 | 8 | const Rule = require(`../../rules/${ruleName}`); 9 | 10 | const valid = 'var a = {};'; 11 | // const invalidVariable = "TODO"; 12 | // const invalidFunction = "TODO"; 13 | const invalidGeneric = 'var a = {}; a[b] = 4'; 14 | 15 | // TODO 16 | // tester.run(`${ruleName} (Variable Assigned to)`, Rule, { 17 | // valid: [{ code: valid }], 18 | // invalid: [ 19 | // { 20 | // code: invalidVariable, 21 | // errors: [{ message: 'Variable Assigned to Object Injection Sink' }] 22 | // } 23 | // ] 24 | // }); 25 | // 26 | // 27 | // tester.run(`${ruleName} (Function)`, Rule, { 28 | // valid: [{ code: valid }], 29 | // invalid: [ 30 | // { 31 | // code: invalidFunction, 32 | // errors: [{ message: `Variable Assigned to Object Injection Sink: : 1\n\t${invalidFunction}\n\n` }] 33 | // } 34 | // ] 35 | // }); 36 | 37 | tester.run(`${ruleName} (Generic)`, Rule, { 38 | valid: [{ code: valid }], 39 | invalid: [ 40 | { 41 | code: invalidGeneric, 42 | errors: [{ message: 'Generic Object Injection Sink' }], 43 | }, 44 | ], 45 | }); 46 | -------------------------------------------------------------------------------- /test/rules/detect-possible-timing-attacks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const RuleTester = require('eslint').RuleTester; 4 | const tester = new RuleTester(); 5 | 6 | const ruleName = 'detect-possible-timing-attacks'; 7 | const Rule = require(`../../rules/${ruleName}`); 8 | 9 | const valid = 'if (age === 5) {}'; 10 | const invalidLeft = "if (password === 'mypass') {}"; 11 | const invalidRigth = "if ('mypass' === password) {}"; 12 | 13 | // We only check with one string "password" and operator "===" 14 | // to KISS. 15 | 16 | tester.run(`${ruleName} (left side)`, Rule, { 17 | valid: [{ code: valid }], 18 | invalid: [ 19 | { 20 | code: invalidLeft, 21 | errors: [{ message: 'Potential timing attack, left side: true' }], 22 | }, 23 | ], 24 | }); 25 | 26 | tester.run(`${ruleName} (right side)`, Rule, { 27 | valid: [{ code: valid }], 28 | invalid: [ 29 | { 30 | code: invalidRigth, 31 | errors: [{ message: 'Potential timing attack, right side: true' }], 32 | }, 33 | ], 34 | }); 35 | -------------------------------------------------------------------------------- /test/rules/detect-pseudoRandomBytes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const RuleTester = require('eslint').RuleTester; 4 | const tester = new RuleTester(); 5 | 6 | const ruleName = 'detect-pseudoRandomBytes'; 7 | const invalid = 'crypto.pseudoRandomBytes'; 8 | 9 | tester.run(ruleName, require(`../../rules/${ruleName}`), { 10 | valid: [{ code: 'crypto.randomBytes' }], 11 | invalid: [ 12 | { 13 | code: invalid, 14 | errors: [{ message: 'Found crypto.pseudoRandomBytes which does not produce cryptographically strong numbers' }], 15 | }, 16 | ], 17 | }); 18 | -------------------------------------------------------------------------------- /test/rules/detect-unsafe-regexp.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const RuleTester = require('eslint').RuleTester; 4 | const tester = new RuleTester(); 5 | 6 | const ruleName = 'detect-unsafe-regex'; 7 | const Rule = require(`../../rules/${ruleName}`); 8 | 9 | tester.run(ruleName, Rule, { 10 | valid: [{ code: '/^d+1337d+$/' }], 11 | invalid: [ 12 | { 13 | code: '/(x+x+)+y/', 14 | errors: [{ message: 'Unsafe Regular Expression' }], 15 | }, 16 | ], 17 | }); 18 | 19 | tester.run(`${ruleName} (new RegExp)`, Rule, { 20 | valid: [{ code: "new RegExp('^d+1337d+$')" }], 21 | invalid: [ 22 | { 23 | code: "new RegExp('x+x+)+y')", 24 | errors: [{ message: 'Unsafe Regular Expression (new RegExp)' }], 25 | }, 26 | ], 27 | }); 28 | -------------------------------------------------------------------------------- /test/utils/import-utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { getImportAccessPath } = require('../../utils/import-utils'); 4 | const { deepStrictEqual } = require('assert'); 5 | 6 | const Linter = require('eslint').Linter; 7 | 8 | function getGetImportAccessPathResult(code) { 9 | const linter = new Linter(); 10 | const result = []; 11 | const testRule = { 12 | create(context) { 13 | const sourceCode = context.sourceCode || context.getSourceCode(); 14 | return { 15 | 'Identifier[name = target]'(node) { 16 | let expr = node; 17 | if (node.parent.type === 'MemberExpression' && node.parent.property === node) { 18 | expr = node.parent; 19 | } 20 | const scope = sourceCode.getScope ? sourceCode.getScope(node) : context.getScope(); 21 | 22 | const info = getImportAccessPath({ 23 | node: expr, 24 | scope, 25 | packageNames: ['target', 'target-foo', 'target-bar'], 26 | }); 27 | if (!info) return; 28 | result.push({ 29 | path: info.path, 30 | packageName: info.packageName, 31 | ...(info.defaultImport ? { defaultImport: info.defaultImport } : {}), 32 | }); 33 | }, 34 | }; 35 | }, 36 | }; 37 | 38 | const linterResult = linter.verify(code, { 39 | plugins: { 40 | test: { 41 | rules: { 42 | 'test-rule': testRule, 43 | }, 44 | }, 45 | }, 46 | rules: { 47 | 'test/test-rule': 'error', 48 | }, 49 | }); 50 | deepStrictEqual(linterResult, []); 51 | 52 | return result; 53 | } 54 | 55 | describe('getImportAccessPath', () => { 56 | describe('The result of getImportAccessPath should be as expected.', () => { 57 | for (const { code, result } of [ 58 | { 59 | code: `var something = require('target'); 60 | something.target(c);`, 61 | result: [ 62 | { 63 | path: ['target'], 64 | packageName: 'target', 65 | }, 66 | ], 67 | }, 68 | { 69 | code: `var target = require('target'); 70 | target(c); 71 | var { foo } = require('target-foo'); 72 | foo.target(c); 73 | foo.bar.target(c); 74 | var { a: bar } = require('target-bar'); 75 | bar.target(c); 76 | var baz = require('target-baz'); 77 | baz.target(c); 78 | var qux = qux.foo.target;`, 79 | result: [ 80 | { 81 | path: [], 82 | packageName: 'target', 83 | }, 84 | { 85 | path: [], 86 | packageName: 'target', 87 | }, 88 | { 89 | path: ['foo', 'target'], 90 | packageName: 'target-foo', 91 | }, 92 | { 93 | path: ['foo', 'bar', 'target'], 94 | packageName: 'target-foo', 95 | }, 96 | { 97 | path: ['a', 'target'], 98 | packageName: 'target-bar', 99 | }, 100 | ], 101 | }, 102 | { 103 | code: `require('target').target; 104 | function fn () { 105 | var { foo } = require('target-foo'); 106 | foo.target(c); 107 | }`, 108 | result: [ 109 | { 110 | path: ['target'], 111 | packageName: 'target', 112 | }, 113 | { 114 | path: ['foo', 'target'], 115 | packageName: 'target-foo', 116 | }, 117 | ], 118 | }, 119 | { 120 | code: `import { foo } from 'target-foo'; 121 | foo.target(c); 122 | foo.bar.target(c); 123 | import { a as bar } from 'target-bar'; 124 | bar.target(c); 125 | import baz from 'target-baz'; 126 | baz.target(c);`, 127 | result: [ 128 | { 129 | path: ['foo', 'target'], 130 | packageName: 'target-foo', 131 | }, 132 | { 133 | path: ['foo', 'bar', 'target'], 134 | packageName: 'target-foo', 135 | }, 136 | { 137 | path: ['a', 'target'], 138 | packageName: 'target-bar', 139 | }, 140 | ], 141 | }, 142 | { 143 | code: `import foo from 'target-foo'; 144 | foo.target(c); 145 | import * as bar from 'target-bar'; 146 | bar.target(c);`, 147 | result: [ 148 | { 149 | path: ['target'], 150 | defaultImport: true, 151 | packageName: 'target-foo', 152 | }, 153 | { 154 | path: ['target'], 155 | packageName: 'target-bar', 156 | }, 157 | ], 158 | }, 159 | { 160 | code: `import foo from 'target-foo'; 161 | function fn () { 162 | foo.target(c); 163 | }`, 164 | result: [ 165 | { 166 | path: ['target'], 167 | defaultImport: true, 168 | packageName: 'target-foo', 169 | }, 170 | ], 171 | }, 172 | ]) { 173 | it(code, () => { 174 | deepStrictEqual(getGetImportAccessPathResult(code), result); 175 | }); 176 | } 177 | }); 178 | }); 179 | -------------------------------------------------------------------------------- /test/utils/is-static-expression.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { isStaticExpression } = require('../../utils/is-static-expression'); 4 | const { deepStrictEqual } = require('assert'); 5 | 6 | const Linter = require('eslint').Linter; 7 | 8 | /** 9 | * Get the return value using `isStaticExpression()`. 10 | * Give `isStaticExpression()` the argument given to `target()` in the code as an expression. 11 | */ 12 | function getIsStaticExpressionResult(code) { 13 | const linter = new Linter(); 14 | const result = []; 15 | const testRule = { 16 | create(context) { 17 | const sourceCode = context.sourceCode || context.getSourceCode(); 18 | 19 | return { 20 | 'CallExpression[callee.name = target]'(node) { 21 | const scope = sourceCode.getScope ? sourceCode.getScope(node) : context.getScope(); 22 | 23 | result.push( 24 | ...node.arguments.map((expr) => 25 | isStaticExpression({ 26 | node: expr, 27 | scope, 28 | }) 29 | ) 30 | ); 31 | }, 32 | }; 33 | }, 34 | }; 35 | 36 | const linterResult = linter.verify(code, { 37 | plugins: { 38 | test: { 39 | rules: { 40 | 'test-rule': testRule, 41 | }, 42 | }, 43 | }, 44 | languageOptions: { 45 | sourceType: 'module', 46 | globals: { 47 | __dirname: 'readonly', 48 | __filename: 'readonly', 49 | require: 'readonly', 50 | }, 51 | }, 52 | rules: { 53 | 'test/test-rule': 'error', 54 | }, 55 | }); 56 | deepStrictEqual(linterResult, []); 57 | 58 | return result; 59 | } 60 | 61 | describe('isStaticExpression', () => { 62 | describe('The result of isStaticExpression should be as expected.', () => { 63 | for (const { code, result } of [ 64 | { 65 | code: `target('foo');`, 66 | result: [true], 67 | }, 68 | { 69 | code: `target(a);`, 70 | result: [false], 71 | }, 72 | { 73 | code: ` 74 | const a = 'i' 75 | target(a);`, 76 | result: [true], 77 | }, 78 | { 79 | code: ` 80 | const a = b 81 | target(a);`, 82 | result: [false], 83 | }, 84 | { 85 | code: ` 86 | const a = a 87 | target(a);`, 88 | result: [false], 89 | }, 90 | { 91 | code: ` 92 | var a = 'foo' 93 | var a = 'bar' 94 | target(a);`, 95 | result: [false], 96 | }, 97 | { 98 | code: ` 99 | var a = 'foo' 100 | a = 'bar' 101 | var b = 'bar' 102 | target(a); 103 | target(b);`, 104 | result: [false, true], 105 | }, 106 | { 107 | code: `target(\`foo\`);`, 108 | result: [true], 109 | }, 110 | { 111 | code: ` 112 | target(\`foo\${a}\`);`, 113 | result: [false], 114 | }, 115 | { 116 | code: ` 117 | const a = 'i' 118 | target(\`foo\${a}\`);`, 119 | result: [true], 120 | }, 121 | { 122 | code: ` 123 | const a = 'i' 124 | target('foo' + 'bar'); 125 | target(a + 'foo'); 126 | target('foo' + a + 'bar'); 127 | `, 128 | result: [true, true, true], 129 | }, 130 | { 131 | code: ` 132 | const a = 'i' 133 | target(b + 'bar'); 134 | target('foo' + a + b); 135 | `, 136 | result: [false, false], 137 | }, 138 | { 139 | code: ` 140 | target(__dirname, __filename); 141 | `, 142 | result: [true, true], 143 | }, 144 | { 145 | code: ` 146 | function fn(__dirname) { 147 | target(__dirname, __filename); 148 | } 149 | `, 150 | result: [false, true], 151 | }, 152 | { 153 | code: ` 154 | const __filename = a 155 | target(__dirname, __filename); 156 | `, 157 | result: [true, false], 158 | }, 159 | { 160 | code: ` 161 | import path from 'path'; 162 | target(path.resolve(__dirname, './index.html')); 163 | target(path.join(__dirname, './ssl.key')); 164 | target(path.resolve(__dirname, './sitemap.xml')); 165 | `, 166 | result: [true, true, true], 167 | }, 168 | { 169 | code: ` 170 | import { posix as path } from 'path'; 171 | target(path.resolve(__dirname, './index.html')); 172 | `, 173 | result: [true], 174 | }, 175 | { 176 | code: ` 177 | const path = require('path'); 178 | target(path.resolve(__dirname, './index.html')); 179 | `, 180 | result: [true], 181 | }, 182 | { 183 | code: ` 184 | import path from 'unknown'; 185 | target(path.resolve(__dirname, './index.html')); 186 | `, 187 | result: [false], 188 | }, 189 | { 190 | code: ` 191 | import path from 'path'; 192 | target(path.unknown(__dirname, './index.html')); 193 | `, 194 | result: [false], 195 | }, 196 | { 197 | code: ` 198 | import path from 'path'; 199 | target(path.resolve.unknown(__dirname, './index.html')); 200 | `, 201 | result: [false], 202 | }, 203 | { 204 | code: ` 205 | import path from 'path'; 206 | const FOO = 'static' 207 | target(path.resolve(__dirname, foo)); 208 | target(path.resolve(__dirname, FOO)); 209 | `, 210 | result: [false, true], 211 | }, 212 | { 213 | code: ` 214 | import path from 'path'; 215 | const FOO = 'static' 216 | target(__dirname + path.sep + foo); 217 | target(__dirname + path.sep + FOO); 218 | `, 219 | result: [false, true], 220 | }, 221 | { 222 | code: ` 223 | target(require.resolve('static')); 224 | target(require.resolve(foo)); 225 | `, 226 | result: [true, false], 227 | }, 228 | { 229 | code: ` 230 | target(require); 231 | target(require('static')); 232 | `, 233 | result: [false, false], 234 | }, 235 | { 236 | code: ` 237 | import url from "node:url"; 238 | import path from "node:path"; 239 | 240 | const filename = url.fileURLToPath(import.meta.url); 241 | const dirname = path.dirname(url.fileURLToPath(import.meta.url)); 242 | 243 | target(filename); 244 | target(dirname); 245 | `, 246 | result: [true, true], 247 | }, 248 | { 249 | code: ` 250 | import url from "node:url"; 251 | target(import.meta.url); 252 | target(url.unknown(import.meta.url)); 253 | `, 254 | result: [true, false], 255 | }, 256 | ]) { 257 | it(code, () => { 258 | deepStrictEqual(getIsStaticExpressionResult(code), result); 259 | }); 260 | } 261 | }); 262 | }); 263 | -------------------------------------------------------------------------------- /utils/data/fsFunctionData.json: -------------------------------------------------------------------------------- 1 | { 2 | "appendFile": [0], 3 | "appendFileSync": [0], 4 | "chmod": [0], 5 | "chmodSync": [0], 6 | "chown": [0], 7 | "chownSync": [0], 8 | "createReadStream": [0], 9 | "createWriteStream": [0], 10 | "exists": [0], 11 | "existsSync": [0], 12 | "lchmod": [0], 13 | "lchmodSync": [0], 14 | "lchown": [0], 15 | "lchownSync": [0], 16 | "link": [0, 1], 17 | "linkSync": [0, 1], 18 | "lstat": [0], 19 | "lstatSync": [0], 20 | "mkdir": [0], 21 | "mkdirSync": [0], 22 | "open": [0], 23 | "openSync": [0], 24 | "readdir": [0], 25 | "readdirSync": [0], 26 | "readFile": [0], 27 | "readFileSync": [0], 28 | "readlink": [0], 29 | "readlinkSync": [0], 30 | "realpath": [0], 31 | "realpathSync": [0], 32 | "rename": [0, 1], 33 | "renameSync": [0, 1], 34 | "rmdir": [0], 35 | "rmdirSync": [0], 36 | "stat": [0], 37 | "statSync": [0], 38 | "symlink": [0, 1], 39 | "symlinkSync": [0, 1], 40 | "truncate": [0], 41 | "truncateSync": [0], 42 | "unlink": [0], 43 | "unlinkSync": [0], 44 | "unwatchFile": [0], 45 | "utimes": [0], 46 | "utimesSync": [0], 47 | "watch": [0], 48 | "watchFile": [0], 49 | "writeFile": [0], 50 | "writeFileSync": [0] 51 | } 52 | -------------------------------------------------------------------------------- /utils/find-variable.js: -------------------------------------------------------------------------------- 1 | module.exports.findVariable = findVariable; 2 | 3 | /** 4 | * Find the variable of a given name. 5 | * @param {import("eslint").Scope.Scope} scope the scope to start finding 6 | * @param {string} name the variable name to find. 7 | * @returns {import("eslint").Scope.Variable | null} 8 | */ 9 | function findVariable(scope, name) { 10 | while (scope != null) { 11 | const variable = scope.set.get(name); 12 | if (variable != null) { 13 | return variable; 14 | } 15 | scope = scope.upper; 16 | } 17 | return null; 18 | } 19 | -------------------------------------------------------------------------------- /utils/import-utils.js: -------------------------------------------------------------------------------- 1 | const { findVariable } = require('./find-variable'); 2 | 3 | module.exports.getImportAccessPath = getImportAccessPath; 4 | 5 | /** 6 | * @typedef {Object} ImportAccessPathInfo 7 | * @property {string[]} path 8 | * @property {boolean} [defaultImport] 9 | * @property {string} packageName 10 | * @property {import("estree").SimpleCallExpression | import("estree").ImportDeclaration} node 11 | */ 12 | /** 13 | * Returns the access path information from a require or import 14 | * 15 | * @param {Object} params 16 | * @param {import("estree").Expression} params.node The node to check. 17 | * @param {import("eslint").Scope.Scope} params.scope The scope of the given node. 18 | * @param {string[]} params.packageNames The interesting packages the method is imported from 19 | * @returns {ImportAccessPathInfo | null} 20 | */ 21 | function getImportAccessPath({ node, scope, packageNames }) { 22 | const tracked = new Set(); 23 | return getImportAccessPathInternal(node); 24 | 25 | /** 26 | * @param {import("estree").Expression} node 27 | * @returns {ImportAccessPathInfo | null} 28 | */ 29 | function getImportAccessPathInternal(node) { 30 | if (tracked.has(node)) { 31 | // Guard infinite loops. 32 | return null; 33 | } 34 | tracked.add(node); 35 | 36 | if (node.type === 'Identifier') { 37 | // Track variables. 38 | const variable = findVariable(scope, node.name); 39 | if (!variable) { 40 | return null; 41 | } 42 | // Check variables defined in `var foo = ...`. 43 | const declDef = variable.defs.find( 44 | /** @returns {def is import("eslint").Scope.Definition & {type: 'Variable'}} */ 45 | (def) => def.type === 'Variable' && def.node.type === 'VariableDeclarator' && def.node.init 46 | ); 47 | if (declDef) { 48 | let propName = null; 49 | if (declDef.node.id.type === 'ObjectPattern') { 50 | const property = declDef.node.id.properties.find((property) => property.type === 'Property' && property.value.type === 'Identifier' && property.value.name === node.name); 51 | if (property && !property.computed) { 52 | propName = property.key.name; 53 | } 54 | } else if (declDef.node.id.type !== 'Identifier') { 55 | // Unknown access path 56 | return null; 57 | } 58 | const nesting = getImportAccessPathInternal(declDef.node.init); 59 | if (!nesting) { 60 | return null; 61 | } 62 | /** 63 | * Detects: 64 | * | var something = require('package-name'); 65 | * | something(c); 66 | * , or 67 | * | var { propName: something } = require('package-name'); 68 | * | something(c); 69 | */ 70 | return { 71 | path: propName ? [...nesting.path, propName] : nesting.path, 72 | defaultImport: nesting.defaultImport, 73 | packageName: nesting.packageName, 74 | node: nesting.node, 75 | }; 76 | } 77 | // Check variables defined in `import foo from ...`. 78 | const importDef = variable.defs.find( 79 | /** @returns {def is import("eslint").Scope.Definition & {type: 'ImportBinding'}} */ 80 | (def) => 81 | def.type === 'ImportBinding' && 82 | (def.node.type === 'ImportDefaultSpecifier' || def.node.type === 'ImportNamespaceSpecifier' || def.node.type === 'ImportSpecifier') && 83 | isImportDeclaration(def.node.parent) 84 | ); 85 | if (importDef) { 86 | let propName = null; 87 | let defaultImport; 88 | if (importDef.node.type === 'ImportSpecifier') { 89 | propName = importDef.node.imported.name; 90 | } else if (importDef.node.type === 'ImportDefaultSpecifier') { 91 | defaultImport = true; 92 | } else if (importDef.node.type !== 'ImportNamespaceSpecifier') { 93 | // Unknown access path 94 | return null; 95 | } 96 | /** 97 | * Detects: 98 | * | import { propName as something } from 'package-name'; 99 | * | something(c); 100 | * , 101 | * | import * as something from 'package-name'; 102 | * | something(c); 103 | * , or 104 | * | import something from 'package-name'; 105 | * | something(c); 106 | */ 107 | return { 108 | path: propName ? [propName] : [], 109 | defaultImport: defaultImport, 110 | packageName: importDef.node.parent.source.value, 111 | node: importDef.node.parent, 112 | }; 113 | } 114 | return null; 115 | } else if (node.type === 'MemberExpression') { 116 | if (node.computed) { 117 | return null; 118 | } 119 | const nesting = getImportAccessPathInternal(node.object); 120 | if (!nesting) { 121 | return null; 122 | } 123 | /** 124 | * Detects: 125 | * | var something = require('package-name'); 126 | * | something.propName(c); 127 | * , 128 | * | var { something } = require('package-name'); 129 | * | something.propName(c); 130 | * , 131 | * | import something from 'package-name'; 132 | * | something.propName(c); 133 | * , 134 | * | import * as something from 'package-name'; 135 | * | something.propName(c); 136 | * , or 137 | * | import { something } from 'package-name'; 138 | * | something.propName(c); 139 | */ 140 | return { 141 | path: [...nesting.path, node.property.name], 142 | defaultImport: nesting.defaultImport, 143 | packageName: nesting.packageName, 144 | node: nesting.node, 145 | }; 146 | } else if (isRequireBasedImport(node)) { 147 | /** 148 | * Detects: 149 | * | require('package-name'); 150 | * , 151 | * | require('package-name').propName(c); 152 | * , or 153 | * | require('package-name')(c); 154 | */ 155 | return { 156 | path: [], 157 | packageName: node.arguments[0].value, 158 | node, 159 | }; 160 | } 161 | return null; 162 | } 163 | 164 | /** 165 | * Checks whether the given expression node is a require based import, or not 166 | * @param {import("estree").Expression} expression 167 | */ 168 | function isRequireBasedImport(expression) { 169 | return ( 170 | expression && 171 | expression.type === 'CallExpression' && 172 | expression.callee.name === 'require' && 173 | expression.arguments.length && 174 | expression.arguments[0].type === 'Literal' && 175 | packageNames.includes(expression.arguments[0].value) 176 | ); 177 | } 178 | 179 | /** 180 | * Checks whether the given node is a import, or not 181 | * @param {import("estree").Node} node 182 | */ 183 | function isImportDeclaration(node) { 184 | return node && node.type === 'ImportDeclaration' && packageNames.includes(node.source.value); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /utils/is-static-expression.js: -------------------------------------------------------------------------------- 1 | const { findVariable } = require('./find-variable'); 2 | const { getImportAccessPath } = require('./import-utils'); 3 | 4 | module.exports.isStaticExpression = isStaticExpression; 5 | 6 | const PATH_PACKAGE_NAMES = ['path', 'node:path', 'path/posix', 'node:path/posix']; 7 | const URL_PACKAGE_NAMES = ['url', 'node:url']; 8 | const PATH_CONSTRUCTION_METHOD_NAMES = new Set(['basename', 'dirname', 'extname', 'join', 'normalize', 'relative', 'resolve', 'toNamespacedPath']); 9 | const PATH_STATIC_MEMBER_NAMES = new Set(['delimiter', 'sep']); 10 | 11 | /** 12 | * @type {WeakMap} 13 | */ 14 | const cache = new WeakMap(); 15 | 16 | /** 17 | * Checks whether the given expression node is a static or not. 18 | * 19 | * @param {Object} params 20 | * @param {import("estree").Expression} params.node The node to check. 21 | * @param {import("eslint").Scope.Scope} params.scope The scope of the given node. 22 | * @returns {boolean} if true, the given expression node is a static. 23 | */ 24 | function isStaticExpression({ node, scope }) { 25 | const tracked = new Set(); 26 | return isStatic(node); 27 | 28 | /** 29 | * @param {import("estree").Expression} node 30 | * @returns {boolean} 31 | */ 32 | function isStatic(node) { 33 | let result = cache.get(node); 34 | if (result == null) { 35 | result = isStaticWithoutCache(node); 36 | cache.set(node, result); 37 | } 38 | return result; 39 | } 40 | /** 41 | * @param {import("estree").Expression} node 42 | * @returns {boolean} 43 | */ 44 | function isStaticWithoutCache(node) { 45 | if (tracked.has(node)) { 46 | // Guard infinite loops. 47 | return false; 48 | } 49 | tracked.add(node); 50 | if (node.type === 'Literal') { 51 | return true; 52 | } 53 | if (node.type === 'TemplateLiteral') { 54 | // A node is static if all interpolations are static. 55 | return node.expressions.every(isStatic); 56 | } 57 | if (node.type === 'BinaryExpression') { 58 | // An expression is static if both operands are static. 59 | return isStatic(node.left) && isStatic(node.right); 60 | } 61 | if (node.type === 'Identifier') { 62 | const variable = findVariable(scope, node.name); 63 | if (variable) { 64 | if (variable.defs.length === 0) { 65 | if (node.name === '__dirname' || node.name === '__filename') { 66 | // It is a global variable that can be used in CJS of Node.js. 67 | return true; 68 | } 69 | } else if (variable.defs.length === 1) { 70 | const def = variable.defs[0]; 71 | if ( 72 | def.type === 'Variable' && 73 | // It has an initial value. 74 | def.node.init && 75 | // It does not write new values. 76 | variable.references.every((ref) => ref.isReadOnly() || ref.identifier === def.name) 77 | ) { 78 | // A variable is static if its initial value is static. 79 | return isStatic(def.node.init); 80 | } 81 | } 82 | } else { 83 | return false; 84 | } 85 | } 86 | return isStaticPath(node) || isStaticFileURLToPath(node) || isStaticImportMetaUrl(node) || isStaticRequireResolve(node) || isStaticCwd(node); 87 | } 88 | 89 | /** 90 | * Checks whether the given expression is a static path construction. 91 | * 92 | * @param {import("estree").Expression} node The node to check. 93 | * @returns {boolean} if true, the given expression is a static path construction. 94 | */ 95 | function isStaticPath(node) { 96 | const pathInfo = getImportAccessPath({ 97 | node: node.type === 'CallExpression' ? node.callee : node, 98 | scope, 99 | packageNames: PATH_PACKAGE_NAMES, 100 | }); 101 | if (!pathInfo) { 102 | return false; 103 | } 104 | /** @type {string | undefined} */ 105 | let name; 106 | if (pathInfo.path.length === 1) { 107 | // e.g. import path from 'path' 108 | name = pathInfo.path[0]; 109 | } else if (pathInfo.path.length === 2 && pathInfo.path[0] === 'posix') { 110 | // e.g. import { posix as path } from 'path' 111 | name = pathInfo.path[1]; 112 | } 113 | if (name == null) { 114 | return false; 115 | } 116 | 117 | if (node.type === 'CallExpression') { 118 | if (!PATH_CONSTRUCTION_METHOD_NAMES.has(name)) { 119 | return false; 120 | } 121 | return Boolean(node.arguments.length) && node.arguments.every(isStatic); 122 | } 123 | 124 | return PATH_STATIC_MEMBER_NAMES.has(name); 125 | } 126 | 127 | /** 128 | * Checks whether the given expression is a static `url.fileURLToPath()`. 129 | * 130 | * @param {import("estree").Expression} node The node to check. 131 | * @returns {boolean} if true, the given expression is a static `url.fileURLToPath()`. 132 | */ 133 | function isStaticFileURLToPath(node) { 134 | if (node.type !== 'CallExpression') { 135 | return false; 136 | } 137 | const pathInfo = getImportAccessPath({ 138 | node: node.callee, 139 | scope, 140 | packageNames: URL_PACKAGE_NAMES, 141 | }); 142 | if (!pathInfo || pathInfo.path.length !== 1) { 143 | return false; 144 | } 145 | let name = pathInfo.path[0]; 146 | if (name !== 'fileURLToPath') { 147 | return false; 148 | } 149 | return Boolean(node.arguments.length) && node.arguments.every(isStatic); 150 | } 151 | 152 | /** 153 | * Checks whether the given expression is an `import.meta.url`. 154 | * 155 | * @param {import("estree").Expression} node The node to check. 156 | * @returns {boolean} if true, the given expression is an `import.meta.url`. 157 | */ 158 | function isStaticImportMetaUrl(node) { 159 | return ( 160 | node.type === 'MemberExpression' && 161 | !node.computed && 162 | node.property.type === 'Identifier' && 163 | node.property.name === 'url' && 164 | node.object.type === 'MetaProperty' && 165 | node.object.meta.name === 'import' && 166 | node.object.property.name === 'meta' 167 | ); 168 | } 169 | 170 | /** 171 | * Checks whether the given expression is a static `require.resolve()`. 172 | * 173 | * @param {import("estree").Expression} node The node to check. 174 | * @returns {boolean} if true, the given expression is a static `require.resolve()`. 175 | */ 176 | function isStaticRequireResolve(node) { 177 | if ( 178 | node.type !== 'CallExpression' || 179 | node.callee.type !== 'MemberExpression' || 180 | node.callee.computed || 181 | node.callee.property.type !== 'Identifier' || 182 | node.callee.property.name !== 'resolve' || 183 | node.callee.object.type !== 'Identifier' || 184 | node.callee.object.name !== 'require' 185 | ) { 186 | return false; 187 | } 188 | const variable = findVariable(scope, node.callee.object.name); 189 | if (!variable || variable.defs.length !== 0) { 190 | return false; 191 | } 192 | return Boolean(node.arguments.length) && node.arguments.every(isStatic); 193 | } 194 | 195 | /** 196 | * Checks whether the given expression is a static `process.cwd()`. 197 | * 198 | * @param {import("estree").Expression} node The node to check. 199 | * @returns {boolean} if true, the given expression is a static `process.cwd()`. 200 | */ 201 | function isStaticCwd(node) { 202 | if ( 203 | node.type !== 'CallExpression' || 204 | node.callee.type !== 'MemberExpression' || 205 | node.callee.computed || 206 | node.callee.property.type !== 'Identifier' || 207 | node.callee.property.name !== 'cwd' || 208 | node.callee.object.type !== 'Identifier' || 209 | node.callee.object.name !== 'process' 210 | ) { 211 | return false; 212 | } 213 | const variable = findVariable(scope, node.callee.object.name); 214 | if (!variable || variable.defs.length !== 0) { 215 | return false; 216 | } 217 | return true; 218 | } 219 | } 220 | --------------------------------------------------------------------------------