CVE-2021-21421: Generation of Error Message Containing Sensitive Information (CWE-209)
value: node-etsy-client is a NodeJs Etsy ReST API Client. Applications that are using node-etsy-client and reporting client error to the end user will offer api key value too This is fixed in node-etsy-client v0.3.0 and later.
CWE description: The product generates an error message that includes sensitive information about its environment, users, or associated data.
CVSS v3 base score: 6.5
Published: 2021-04-01T22:15Z
Repository: https://github.com/creharmony/node-etsy-client
Language: YAML
Files changed: .github/workflows/main.yml, .github/workflows/minor.yml, .github/workflows/patch.yml, README.md, package.json, src/EtsyClient.js, tests/unauthenticated_client.test.js
Commit date: 2021-03-31 21:03:11 +0200
Commit message: Fix #17 do not report secret on error, add github action

Diff: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..2975627
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,66 @@
+# node-etsy-client continuous integration
+
+name: etsy_client_ci
+
+# Controls when the action will run.
+on:
+  # Triggers the workflow on pull request or push (only for the npmjs branch)
+  push:
+    branches: [ npmjs ]
+  pull_request:
+
+  # Allows you to run this workflow manually from the Actions tab
+  workflow_dispatch:
+
+# A workflow run is made up of one or more jobs that can run sequentially or in parallel
+jobs:
+  # This workflow contains a single job called "build"
+  build:
+    # The type of runner that the job will run on
+    runs-on: ubuntu-latest
+
+    env:
+      ETSY_SHOP: fakefakefakefake
+
+    strategy:
+      matrix:
+        # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
+        # node-version: [10.x, 12.x, 14.x, 15.x]
+        node-version: [12.x]
+
+    steps:
+    - name: Checkout code
+      uses: actions/checkout@v2
+
+    - name: Use Node.js ${{ matrix.node-version }}
+      uses: actions/setup-node@v1
+      with:
+        node-version: ${{ matrix.node-version }}
+
+    - name: Cache dependencies
+      uses: actions/cache@v2
+      with:
+        path: |
+          **/node_modules
+        key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
+
+    - name: Install dependencies
+      # - run: npm ci # need package.json.lock
+      run: npm install
+
+    - name: Run ci-tests with code coverage
+      run: npm run ci-test
+
+    - name: Report coverage to the PR
+      continue-on-error: true
+      uses: romeovs/lcov-reporter-action@v0.2.16
+      with:
+        github-token: ${{ secrets.GITHUB_TOKEN }}
+        lcov-file: ./coverage/lcov.info
+
+    - name: Publish NpmJS package
+      if: github.ref == 'refs/heads/npmjs'
+      run: |
+        echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_DEPLOY_TOKEN }}" > .npmrc
+        npm whoami # rely on .npmrc
+        npm publish
\ No newline at end of file
diff --git a/.github/workflows/minor.yml b/.github/workflows/minor.yml
new file mode 100644
index 0000000..33c4c4b
--- /dev/null
+++ b/.github/workflows/minor.yml
@@ -0,0 +1,34 @@
+# WIP - node-etsy-client minor from 'main' branch to 'npmjs' branch
+name: etsy_client_minor
+on:
+  # Allows you to run this workflow manually from the Actions tab
+  workflow_dispatch:
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        node-version: [12.x]
+
+    steps:
+      - name: Git checkout
+        uses: actions/checkout@v2
+        with:
+          ref: 'npmjs'
+          token: ${{ secrets.GH_ACTIONS_TOKEN }}
+
+      - name: Use Node.js ${{ matrix.node-version }}
+        uses: actions/setup-node@v1
+        with:
+          node-version: ${{ matrix.node-version }}
+
+      - name: Minor
+        run: |
+          git config --global user.email  'github-actions[bot]@users.noreply.github.com'
+          git config --global user.name 'github-actions[bot]'
+          git fetch --all
+          git checkout main
+          npm version minor
+          git branch -f npmjs
+          git push origin main npmjs --tags
\ No newline at end of file
diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml
new file mode 100644
index 0000000..54ce08b
--- /dev/null
+++ b/.github/workflows/patch.yml
@@ -0,0 +1,34 @@
+# WIP - node-etsy-client patch from 'main' branch to 'npmjs' branch
+name: etsy_client_patch
+on:
+  # Allows you to run this workflow manually from the Actions tab
+  workflow_dispatch:
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        node-version: [12.x]
+
+    steps:
+      - name: Git checkout
+        uses: actions/checkout@v2
+        with:
+          ref: 'npmjs'
+          token: ${{ secrets.GH_ACTIONS_TOKEN }}
+
+      - name: Use Node.js ${{ matrix.node-version }}
+        uses: actions/setup-node@v1
+        with:
+          node-version: ${{ matrix.node-version }}
+
+      - name: Patch
+        run: |
+          git config --global user.email  'github-actions[bot]@users.noreply.github.com'
+          git config --global user.name 'github-actions[bot]'
+          git fetch --all
+          git checkout main
+          npm version patch
+          git branch -f npmjs
+          git push origin main npmjs --tags
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml.disabled
similarity index 100%
rename from .travis.yml
rename to .travis.yml.disabled
diff --git a/README.md b/README.md
index c381eef..a031808 100644
--- a/README.md
+++ b/README.md
@@ -58,10 +58,13 @@ You're not a dev ? just submit an issue (bug, improvements, questions). Or else:
 * Then mocha tests
 ```
 git clone https://github.com/creharmony/node-etsy-client.git
 cd node-etsy-client
 npm install
+# play test without etsy endpoint
+npm run test
+# play test with etsy endpoint
 . ./env/initEnv.example.sh
 npm run test
 ```
 * you could also fork, feature branch, then submit a pull request.
 
diff --git a/package.json b/package.json
index 2ba2c5e..19abfb0 100644
--- a/package.json
+++ b/package.json
@@ -3,11 +3,13 @@
   "version": "0.2.0",
   "description": "node-etsy-client",
   "main": "./src/EtsyClient.js",
   "types": "./src/EtsyClient.d.ts",
   "scripts": {
-    "test": "mocha tests/*.test.js"
+    "test": "mocha tests/*.test.js",
+    "cover": "nyc --reporter text --reporter cobertura --reporter html --reporter=lcov --lines 66 mocha --exit --unhandled-rejections=strict tests/*.test.js",
+    "ci-test": "echo linux ci-test&& nyc --reporter text --reporter cobertura --reporter html --reporter=lcov --lines 66 mocha --exit --unhandled-rejections=strict tests/*.test.js --timeout 5000"
   },
   "private": false,
   "author": "Boly38 <boly380@gmail.com>",
   "license": "MIT",
   "repository": {
@@ -33,10 +35,11 @@
   },
   "dependencies": {
     "chai": "^4.2.0",
     "mocha": "^8.2.1",
     "node-fetch": "^2.6.1",
+    "nyc": "^15.1.0",
     "query-string": "^6.13.7",
     "winston": "^3.3.3"
   },
   "jshintConfig": {
     "esversion": 6
diff --git a/src/EtsyClient.js b/src/EtsyClient.js
index 45cc897..d1c6147 100644
--- a/src/EtsyClient.js
+++ b/src/EtsyClient.js
@@ -91,14 +91,32 @@ class EtsyClient {
      this._assumeField('endpoint', endpoint);
      return new Promise((resolve, reject) => {
          const getQueryString = queryString.stringify(this.getOptions(options));
          fetch(`${this.apiUrl}${endpoint}?${getQueryString}`)
            .then(response => EtsyClient._response(response, resolve, reject))
-           .catch(reject);
+           .catch((fetchError) => {
+             var secureError = {};
+             this.secureErrorAttribute(secureError, fetchError, "message");
+             this.secureErrorAttribute(secureError, fetchError, "reason");
+             this.secureErrorAttribute(secureError, fetchError, "type");
+             this.secureErrorAttribute(secureError, fetchError, "errno");
+             this.secureErrorAttribute(secureError, fetchError, "code");
+             reject(secureError);
+           });
      });
   }
 
+  secureErrorAttribute(secureError, sourceError, attribute) {
+    if (!Object.keys(sourceError).includes(attribute)) {
+      return;
+    }
+    secureError[attribute] = this.secureAttributeValue(sourceError[attribute]);
+  }
+
+  secureAttributeValue(value) {
+    return (value === null || value === undefined) ? null : value.replace(new RegExp(this.apiKey,'g'), "**hidden**");
+  }
 
   getOptions(options) {
     var merged = options ? options : {};
     merged['api_key'] = this.apiKey;
     if (this.lang != null) {
diff --git a/tests/unauthenticated_client.test.js b/tests/unauthenticated_client.test.js
index 53ebfa7..2ccec88 100644
--- a/tests/unauthenticated_client.test.js
+++ b/tests/unauthenticated_client.test.js
@@ -1,15 +1,28 @@
 const assert = require('assert').strict;
 const expect = require('chai').expect
 const EtsyClient = require('../src/EtsyClient.js');
 
+const FAKE_API_KEY = "ultraSecretRightHere";
+
 if (!process.env.ETSY_API_KEY) {
 
   describe("Test Unauthenticated EtsyClient", function() {
 
     it("should not work without api key", async function() {
         expect(function () { new EtsyClient() } ).to.throw('apiKey is required');
     });
 
+    it("should not report api key in error case", async function() {
+      const client = new EtsyClient({
+        apiKey:FAKE_API_KEY,
+        apiUrl:"https://IAmNotEtsyEndpoint.com"
+      });
+      const shops = await client.findAllShops()
+        .catch((getShopsError) => {
+          expect(""+getShopsError).to.not.include(FAKE_API_KEY);
+        })
+    });
+
   });
 
 }
\ No newline at end of file

Vulnerable code:     "test": "mocha tests/*.test.js"
           .catch(reject);
Fixed code: # node-etsy-client continuous integration

name: etsy_client_ci

# Controls when the action will run.
on:
  # Triggers the workflow on pull request or push (only for the npmjs branch)
  push:
    branches: [ npmjs ]
  pull_request:

  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
  # This workflow contains a single job called "build"
  build:
    # The type of runner that the job will run on
    runs-on: ubuntu-latest

    env:
      ETSY_SHOP: fakefakefakefake

    strategy:
      matrix:
        # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
        # node-version: [10.x, 12.x, 14.x, 15.x]
        node-version: [12.x]

    steps:
    - name: Checkout code
      uses: actions/checkout@v2

    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}

    - name: Cache dependencies
      uses: actions/cache@v2
      with:
        path: |
          **/node_modules
        key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}

    - name: Install dependencies
      # - run: npm ci # need package.json.lock
      run: npm install

    - name: Run ci-tests with code coverage
      run: npm run ci-test

    - name: Report coverage to the PR
      continue-on-error: true
      uses: romeovs/lcov-reporter-action@v0.2.16
      with:
        github-token: ${{ secrets.GITHUB_TOKEN }}
        lcov-file: ./coverage/lcov.info

    - name: Publish NpmJS package
      if: github.ref == 'refs/heads/npmjs'
      run: |
        echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_DEPLOY_TOKEN }}" > .npmrc
        npm whoami # rely on .npmrc
        npm publish
# WIP - node-etsy-client minor from 'main' branch to 'npmjs' branch
name: etsy_client_minor
on:
  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [12.x]

    steps:
      - name: Git checkout
        uses: actions/checkout@v2
        with:
          ref: 'npmjs'
          token: ${{ secrets.GH_ACTIONS_TOKEN }}

      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}

      - name: Minor
        run: |
          git config --global user.email  'github-actions[bot]@users.noreply.github.com'
          git config --global user.name 'github-actions[bot]'
          git fetch --all
          git checkout main
          npm version minor
          git branch -f npmjs
          git push origin main npmjs --tags
# WIP - node-etsy-client patch from 'main' branch to 'npmjs' branch
name: etsy_client_patch
on:
  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [12.x]

    steps:
      - name: Git checkout
        uses: actions/checkout@v2
        with:
          ref: 'npmjs'
          token: ${{ secrets.GH_ACTIONS_TOKEN }}

      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}

      - name: Patch
        run: |
          git config --global user.email  'github-actions[bot]@users.noreply.github.com'
          git config --global user.name 'github-actions[bot]'
          git fetch --all
          git checkout main
          npm version patch
          git branch -f npmjs
          git push origin main npmjs --tags
# play test without etsy endpoint
npm run test
# play test with etsy endpoint
    "test": "mocha tests/*.test.js",
    "cover": "nyc --reporter text --reporter cobertura --reporter html --reporter=lcov --lines 66 mocha --exit --unhandled-rejections=strict tests/*.test.js",
    "ci-test": "echo linux ci-test&& nyc --reporter text --reporter cobertura --reporter html --reporter=lcov --lines 66 mocha --exit --unhandled-rejections=strict tests/*.test.js --timeout 5000"
    "nyc": "^15.1.0",
           .catch((fetchError) => {
             var secureError = {};
             this.secureErrorAttribute(secureError, fetchError, "message");
             this.secureErrorAttribute(secureError, fetchError, "reason");
             this.secureErrorAttribute(secureError, fetchError, "type");
             this.secureErrorAttribute(secureError, fetchError, "errno");
             this.secureErrorAttribute(secureError, fetchError, "code");
             reject(secureError);
           });
  secureErrorAttribute(secureError, sourceError, attribute) {
    if (!Object.keys(sourceError).includes(attribute)) {
      return;
    }
    secureError[attribute] = this.secureAttributeValue(sourceError[attribute]);
  }

  secureAttributeValue(value) {
    return (value === null || value === undefined) ? null : value.replace(new RegExp(this.apiKey,'g'), "**hidden**");
  }
const FAKE_API_KEY = "ultraSecretRightHere";

    it("should not report api key in error case", async function() {
      const client = new EtsyClient({
        apiKey:FAKE_API_KEY,
        apiUrl:"https://IAmNotEtsyEndpoint.com"
      });
      const shops = await client.findAllShops()
        .catch((getShopsError) => {
          expect(""+getShopsError).to.not.include(FAKE_API_KEY);
        })
    });