CVE-2021-31590: Other (NVD-CWE-Other)
value: PwnDoc all versions until 0.4.0 (2021-08-23) has incorrect JSON Webtoken handling, leading to incorrect access control. With a valid JSON Webtoken that is used for authentication and authorization, a user can keep his admin privileges even if he is downgraded to the "user" privilege. Even after a user's account is deleted, the user can still access the administration panel (and add or delete users) and has complete access to the system.
CWE description: NVD is only using a subset of CWE for mapping instead of the entire CWE, and the weakness type is not covered by that subset.
CVSS v3 base score: 8.8
Published: 2021-07-19T20:15Z
Repository: https://github.com/pwndoc/pwndoc
Language: JavaScript
Files changed: .gitignore, backend/src/app.js, backend/src/config/config.json, backend/src/lib/auth.js, backend/src/lib/report-generator.js, backend/src/routes/settings.js, docker-compose.yml, docs/roles.md
Commit date: 2021-06-21 15:39:49 -0400
Commit message: Update JWT generation

JWT is now dynamic
config files have moved

Diff: diff --git a/.gitignore b/.gitignore
index efeb194..a2a04f0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,10 +7,14 @@ yarn-debug.log*
 yarn-error.log*
 mongo-data*
 .quasar
 report-templates
 custom-generator.js
+
+# Configuration files
+config.json
+report.json
 roles.json
 app-settings.json
 
 # Editor directories and files
 .idea
diff --git a/backend/src/app.js b/backend/src/app.js
index 67c8a37..1096338 100644
--- a/backend/src/app.js
+++ b/backend/src/app.js
@@ -8,11 +8,11 @@ var io = require('socket.io')(https);
 var bodyParser = require('body-parser');
 var cookieParser = require('cookie-parser')
 
 // Get configuration
 var env = process.env.NODE_ENV || 'dev';
-var config = require('./config.json')[env];
+var config = require('./config/config.json')[env];
 global.__basedir = __dirname;
 
 // Database connection
 var mongoose = require('mongoose');
 // Use native promises
diff --git a/backend/src/lib/app-settings.json b/backend/src/config/app-settings.json
similarity index 100%
rename from backend/src/lib/app-settings.json
rename to backend/src/config/app-settings.json
diff --git a/backend/src/config.json b/backend/src/config/config.json
similarity index 85%
rename from backend/src/config.json
rename to backend/src/config/config.json
index 8da5340..50f12b5 100644
--- a/backend/src/config.json
+++ b/backend/src/config/config.json
@@ -4,11 +4,12 @@
         "host": "",
         "database": {
             "name": "pwndoc",
             "server": "mongo-pwndoc-dev",
             "port": "27017"
-        }
+        },
+        "jwtSecret": "eb942782a7f0842c51b112d37ff2993de45307831eea267a3acc012bf7550711"
     },
     "prod": {
         "port": 4242,
         "host": "",
         "database": {
@@ -24,6 +25,6 @@
             "name": "pwndoc",
             "server": "mongo-pwndoc-test",
             "port": "27017"
         }
     }
-}
+}
\ No newline at end of file
diff --git a/backend/src/lib/report.json b/backend/src/config/report.json
similarity index 100%
rename from backend/src/lib/report.json
rename to backend/src/config/report.json
diff --git a/backend/src/lib/roles.json b/backend/src/config/roles.json
similarity index 100%
rename from backend/src/lib/roles.json
rename to backend/src/config/roles.json
diff --git a/backend/src/lib/auth.js b/backend/src/lib/auth.js
index b8aad07..eed919d 100644
--- a/backend/src/lib/auth.js
+++ b/backend/src/lib/auth.js
@@ -1,8 +1,18 @@
-// Secret for JWT tokens creation (make it dynamic for production)
-var jwtSecret = "ASy4FVjsXNLQl09LbieroWsjO5UXjvX5";
-exports.jwtSecret = jwtSecret;
+// Dynamic generation of JWT Secret if not exist (different for each environnment)
+var fs = require('fs')
+var env = process.env.NODE_ENV || 'dev'
+var config = require('../config/config.json')
+
+if (!config[env].jwtSecret) {
+    config[env].jwtSecret = require('crypto').randomBytes(32).toString('hex')
+    var configString = JSON.stringify(config, null, 4)
+    fs.writeFileSync(`${__basedir}/config/config.json`, configString)
+}
+
+var jwtSecret = config[env].jwtSecret
+exports.jwtSecret = jwtSecret
 
 /*  ROLES LOGIC
 
     role_name: {
         allows: [],
@@ -60,11 +70,11 @@ var builtInRoles = {
         allows: "*"
     }
 }
 
 try {
-    var customRoles = require('./roles.json')}
+    var customRoles = require('../config/roles.json')}
 catch(error) {
     var customRoles = []
 }
 var roles = {...customRoles, ...builtInRoles}
 
diff --git a/backend/src/lib/report-generator.js b/backend/src/lib/report-generator.js
index a760b9b..e9d7dc3 100644
--- a/backend/src/lib/report-generator.js
+++ b/backend/src/lib/report-generator.js
@@ -7,21 +7,21 @@ var sizeOf = require('image-size');
 var customGenerator = require('./custom-generator');
 var utils = require('./utils');
 var html2ooxml = require('./html2ooxml');
 var _ = require('lodash');
 var Image = require('mongoose').model('Image');
-var reportConfig = require('./report.json');
+var reportConfig = require('../config/report.json');
 
 // Generate document with docxtemplater
 async function generateDoc(audit) {
     var templatePath = `${__basedir}/../report-templates/${audit.template.name}.${audit.template.ext || 'docx'}`
     var preppedAudit = await prepAuditData(audit)
     var content = fs.readFileSync(templatePath, "binary");
 
     var zip = new JSZip(content);
 
-    var settings = JSON.parse(fs.readFileSync(`${__basedir}/lib/app-settings.json`));
+    var settings = JSON.parse(fs.readFileSync(`${__basedir}/config/app-settings.json`));
 
     var opts = {};
     // opts.centered = true;
     opts.getImage = function(tagValue, tagName) {
         if (tagValue !== "undefined") {
diff --git a/backend/src/routes/settings.js b/backend/src/routes/settings.js
index 2c531f5..e75095b 100644
--- a/backend/src/routes/settings.js
+++ b/backend/src/routes/settings.js
@@ -10,11 +10,11 @@ module.exports = function(app) {
     }
 
     // Get settings
     app.get("/api/settings", acl.hasPermission('settings:read'), function(req, res) {
         try {
-            var settings = JSON.parse(fs.readFileSync(`${__basedir}/lib/app-settings.json`))            
+            var settings = JSON.parse(fs.readFileSync(`${__basedir}/config/app-settings.json`))            
             Object.keys(defaultSettings).forEach(key => {
                 if (!(key in settings))
                     settings[key] = defaultSettings[key]
             })
             Response.Ok(res, settings)
@@ -32,11 +32,11 @@ module.exports = function(app) {
             else
                 settings[key] = defaultSettings[key]
         })
         var settingsString = JSON.stringify(settings, null, 4)
         try {
-            fs.writeFileSync(`${__basedir}/lib/app-settings.json`, settingsString)
+            fs.writeFileSync(`${__basedir}/config/app-settings.json`, settingsString)
             Response.Ok(res, settings)
         }
         catch(err) {
             Response.Internal(res, err)
         }
diff --git a/docker-compose.yml b/docker-compose.yml
index 41319f2..9c9428c 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -17,11 +17,11 @@ services:
     build: ./backend
     image: yeln4ts/pwndoc:backend
     container_name: pwndoc-backend
     volumes:
       - ./backend/report-templates:/app/report-templates
-      - ./backend/src/lib:/app/src/lib
+      - ./backend/src/config:/app/src/config
     depends_on:
       - mongodb
     restart: always
     ports:
       - 4242:4242
diff --git a/docs/roles.md b/docs/roles.md
index 63b6b48..411355a 100644
--- a/docs/roles.md
+++ b/docs/roles.md
@@ -53,11 +53,11 @@ This role has following permissions:
 
 This role has full permissions access
 
 ## Create additional Roles
 
-Custom roles can be defined in `backend/src/lib/roles.json`
+Custom roles can be defined in `backend/src/config/roles.json`
 The format is:
 
 ```
 role_name: {
   allows: [], // Array of allowed permissions to access or use '*' for all (admin)

Vulnerable code: var config = require('./config.json')[env];
        }
}
// Secret for JWT tokens creation (make it dynamic for production)
var jwtSecret = "ASy4FVjsXNLQl09LbieroWsjO5UXjvX5";
exports.jwtSecret = jwtSecret;
    var customRoles = require('./roles.json')}
var reportConfig = require('./report.json');
    var settings = JSON.parse(fs.readFileSync(`${__basedir}/lib/app-settings.json`));
            var settings = JSON.parse(fs.readFileSync(`${__basedir}/lib/app-settings.json`))            
            fs.writeFileSync(`${__basedir}/lib/app-settings.json`, settingsString)
      - ./backend/src/lib:/app/src/lib
Custom roles can be defined in `backend/src/lib/roles.json`
Fixed code: 
# Configuration files
config.json
report.json
var config = require('./config/config.json')[env];
        },
        "jwtSecret": "eb942782a7f0842c51b112d37ff2993de45307831eea267a3acc012bf7550711"
}
// Dynamic generation of JWT Secret if not exist (different for each environnment)
var fs = require('fs')
var env = process.env.NODE_ENV || 'dev'
var config = require('../config/config.json')

if (!config[env].jwtSecret) {
    config[env].jwtSecret = require('crypto').randomBytes(32).toString('hex')
    var configString = JSON.stringify(config, null, 4)
    fs.writeFileSync(`${__basedir}/config/config.json`, configString)
}

var jwtSecret = config[env].jwtSecret
exports.jwtSecret = jwtSecret
    var customRoles = require('../config/roles.json')}
var reportConfig = require('../config/report.json');
    var settings = JSON.parse(fs.readFileSync(`${__basedir}/config/app-settings.json`));
            var settings = JSON.parse(fs.readFileSync(`${__basedir}/config/app-settings.json`))            
            fs.writeFileSync(`${__basedir}/config/app-settings.json`, settingsString)
      - ./backend/src/config:/app/src/config
Custom roles can be defined in `backend/src/config/roles.json`