CVE-2021-31590 (JavaScript, published 2021-07-19T20:15Z)
Description: 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.
Severity: HIGH (CVSS v3 base score: 8.8)
Weakness: NVD-CWE-Other \u2014 Other
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.
Repository: https://github.com/pwndoc/pwndoc
Files changed:
backend/src/models/user.js
backend/src/routes/user.js
frontend/src/boot/auth.js
frontend/src/boot/axios.js
frontend/src/pages/profile/profile.js
frontend/src/services/user.js
Commit message: Update Session management using refresh token

Added refresh token with lifetime of 7 days
Token has now a lifetime of 15min
Refresh token is updated on the frontend each time application is loaded or every 14min
Each refresh token is associated with a sessionId allowing to have multiple sessions on different devices


Vulnerable code:
            var token = jwt.sign(payload, auth.jwtSecret, {expiresIn: '24h'});
            var username = decoded.username
        var query = this.findOne({username: username})
                    newRefreshToken = jwt.sign({sessionId: sessionId, username: username}, auth.jwtRefreshSecret, {expiresIn: '7 days'})
                    newRefreshToken = jwt.sign({sessionId: sessionId, username: username, exp: expiration}, auth.jwtRefreshSecret)
                reject({fn: 'NotFound', message: 'User not found'})
UserSchema.statics.removeSession = function (username, sessionId) {
        var query = this.findOne({username: username})
        var token = ""
                var refreshToken = jwt.sign({sessionId: null, username: row.username}, auth.jwtRefreshSecret)
    app.get("/api/users/destroytoken", function(req, res) {
        User.removeSession(decoded.username, decoded.sessionId)
                    newUser.getToken()
                        res.cookie('token', msg.token, {secure: true, httpOnly: true})
export default ({ app, router, Vue }) => {
      User.checkToken()
      .then(() => {
      })
      .catch((error) => {
      })
export default ({ Vue }) => {
  // Redirect to login if response is 401 (Unauthenticated)
  axiosInstance.interceptors.response.use(response => {
  }, error => {
   if (error.response.status === 401) {
     if (window.location.pathname !== '/login')
      window.location = '/login'
   }
  return error
  })
  User.refreshToken()
                UserService.checkToken()
import Router from '../router'
    user: null,
                var countdown = this.user.exp*1000 - Date.now() - 60000 // Countdown to expiration less 1 minute
                setTimeout(() => {
                    this.refreshToken()
                }, countdown)
                console.log(error)
        Vue.prototype.$axios.get('users/refreshtoken')
        .then((response) => {
            var token = response.data.datas.token;
            this.user = jwtDecode(token);
            var countdown = this.user.exp*1000 - Date.now() - 60000 // Countdown to expiration less 1 minute
            setTimeout(() => {
                this.refreshToken()
            }, countdown)
        })
        .catch(err => {
            console.log(err)
            if (err.response && err.response.data.datas.includes('Expired'))
                Router.push('/login?tokenError=2')
            else
                Router.push('/login')
        Vue.prototype.$axios.get('users/destroytoken')
        .catch(err => Router.push('/login'))
    },

    checkToken() {
        return new Promise((resolve, reject) => {
            Vue.prototype.$axios.get(`users/checktoken`)
            .then(data => {
                var token = data.data.datas
                var decoded = jwtDecode(token);
                if (decoded) {
                    this.user = decoded;
                    resolve();
                }
                else
                    reject('InvalidToken');
                resolve()
            })
            .catch((error) => {
                reject(error);
            })
        return (this.user !== null);

Fixed code:
            var token = jwt.sign(payload, auth.jwtSecret, {expiresIn: '15 minutes'});
            var userId = decoded.userId
        var query = this.findById(userId)
                    newRefreshToken = jwt.sign({sessionId: sessionId, userId: userId}, auth.jwtRefreshSecret, {expiresIn: '7 days'})
                    newRefreshToken = jwt.sign({sessionId: sessionId, userId: userId, exp: expiration}, auth.jwtRefreshSecret)
                reject({fn: 'NotFound', message: 'Session not found'})
UserSchema.statics.removeSession = function (userId, sessionId) {
        var query = this.findById(userId)
                var refreshToken = jwt.sign({sessionId: null, userId: row._id}, auth.jwtRefreshSecret)
    app.delete("/api/users/refreshtoken", function(req, res) {
        User.removeSession(decoded.userId, decoded.sessionId)
                    newUser.getToken(req.headers['user-agent'])
                        res.cookie('token', `JWT ${msg.token}`, {secure: true, httpOnly: true})
                        res.cookie('refreshToken', msg.refreshToken, {secure: true, httpOnly: true, path: '/api/users/refreshtoken'})
export default async ({ urlPath, router, redirect }) => {
  // Launch refresh token countdown 840000=14min if not on login page
  setInterval(() => {
    User.refreshToken()
    .then()
    .catch(err => {
      if (!router.currentRoute.path.startsWith('/login'))
        if (err === 'Expired refreshToken')
          redirect('/login?tokenError=2')
        else
          redirect('/login')
    })
  }, 840000)

  // Call refreshToken when loading app and redirect to login if error
  try {
    await User.refreshToken()
  }
  catch(err) {
    if (!urlPath.startsWith('/login'))
      if (err === 'Expired refreshToken')
        redirect('/login?tokenError=2')
      else
        redirect('/login')
  }

      if (User.isAuth())
      else
import Router from '../router'
var refreshPending = false
var requestsQueue = []
// Redirect to login if response is 401 (Unauthenticated)
axiosInstance.interceptors.response.use(
  response => {
  }, 
  error => {
    const originalRequest = error.config

    // 401 after User.refreshToken function call
    if (error.response.status === 401 && originalRequest.url.endsWith('/users/refreshtoken')) {
      User.clear()
      return Promise.reject(error)
    }

    // 401 after login request
    if (error.response.status === 401 && originalRequest.url.endsWith('/users/token')) {
      return Promise.reject(error)
    }
    // All other 401 calls
    if (error.response.status === 401) {
      if (!refreshPending) {
        refreshPending = true
        axiosInstance.get('/users/refreshtoken')
        .then(() => {
          requestsQueue.forEach(e => e())
          requestsQueue = []
        })
        .catch(err => {
          Router.push('/login')
          return Promise.reject(error)
          
        })
        .finally(() => {
          refreshPending = false
        })
      }
      return new Promise((resolve) => {
        requestsQueue.push(() => resolve(axiosInstance.request(originalRequest)))
      })
    }
    return Promise.reject(error)
  }
)

export default ({ Vue }) => {
                UserService.refreshToken()
import User from '@/services/user';
import Router from '@/router'
    user: {
        username: "",
        role: "",
        firstname: "",
        lastname: "",
        roles: ""
    },
        return new Promise((resolve, reject) => {
            Vue.prototype.$axios.get('users/refreshtoken')
            .then((response) => {
                var token = response.data.datas.token;
                this.user = jwtDecode(token);
                resolve()
            })
            .catch(err => {
                if (err.response && err.response.data)
                    reject(err.response.data.datas)
                else 
                    reject('Invalid Token')
            })
        Vue.prototype.$axios.delete('users/refreshtoken')
            User.clear()
        .catch(err => {
            console.log(err)
        if (this.user && this.user.username)
            return true
        return false
    },

    // Reset user variable to default empty
    clear() {
        this.user = {
            username: "",
            role: "",
            firstname: "",
            lastname: "",
            roles: ""
        }