CVE-2023-51665: Server-Side Request Forgery (SSRF) (CWE-918)
value: Audiobookshelf is a self-hosted audiobook and podcast server. Prior to 2.7.0, Audiobookshelf is vulnerable to unauthenticated blind server-side request (SSRF) vulnerability in Auth.js. This vulnerability has been addressed in version 2.7.0. There are no known workarounds for this vulnerability.


CWE description: The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
CVSS v3 base score: 7.5
Published: 2023-12-27T18:15Z
Repository: https://github.com/advplyr/audiobookshelf
Language: JavaScript
Files changed: server/Auth.js
Commit date: 2023-12-17 10:41:39 -0600
Commit message: Update:/auth/openid/config API endpoint to require admin user and validate issuer URL

Diff: diff --git a/server/Auth.js b/server/Auth.js
index 0a282c9c..d2334de2 100644
--- a/server/Auth.js
+++ b/server/Auth.js
@@ -294,11 +294,11 @@ class Auth {
         // We will allow if it is in the whitelist, by saving it into this.openIdAuthSession and setting the redirect uri to /auth/openid/mobile-redirect
         //    where we will handle the redirect to it
         if (req.query.redirect_uri) {
           // Check if the redirect_uri is in the whitelist
           if (Database.serverSettings.authOpenIDMobileRedirectURIs.includes(req.query.redirect_uri) ||
-           (Database.serverSettings.authOpenIDMobileRedirectURIs.length === 1 && Database.serverSettings.authOpenIDMobileRedirectURIs[0] === '*')) {
+            (Database.serverSettings.authOpenIDMobileRedirectURIs.length === 1 && Database.serverSettings.authOpenIDMobileRedirectURIs[0] === '*')) {
             oidcStrategy._params.redirect_uri = new URL(`${protocol}://${req.get('host')}/auth/openid/mobile-redirect`).toString()
             mobile_redirect_uri = req.query.redirect_uri
           } else {
             Logger.debug(`[Auth] Invalid redirect_uri=${req.query.redirect_uri} - not in whitelist`)
             return res.status(400).send('Invalid redirect_uri')
@@ -379,11 +379,11 @@ class Auth {
     // It will redirect to an app-link like audiobookshelf://oauth
     router.get('/auth/openid/mobile-redirect', (req, res) => {
       try {
         // Extract the state parameter from the request
         const { state, code } = req.query
-    
+
         // Check if the state provided is in our list
         if (!state || !this.openIdAuthSession.has(state)) {
           Logger.error('[Auth] /auth/openid/mobile-redirect route: State parameter mismatch')
           return res.status(400).send('State parameter mismatch')
         }
@@ -467,21 +467,42 @@ class Auth {
     },
       // on a successfull login: read the cookies and react like the client requested (callback or json)
       this.handleLoginSuccessBasedOnCookie.bind(this))
 
     /**
-     * Used to auto-populate the openid URLs in config/authentication
+     * Helper route used to auto-populate the openid URLs in config/authentication
+     * Takes an issuer URL as a query param and requests the config data at "/.well-known/openid-configuration"
+     * 
+     * @example /auth/openid/config?issuer=http://192.168.1.66:9000/application/o/audiobookshelf/
      */
-    router.get('/auth/openid/config', async (req, res) => {
+    router.get('/auth/openid/config', this.isAuthenticated, async (req, res) => {
+      if (!req.user.isAdminOrUp) {
+        Logger.error(`[Auth] Non-admin user "${req.user.username}" attempted to get issuer config`)
+        return res.sendStatus(403)
+      }
+
       if (!req.query.issuer) {
         return res.status(400).send('Invalid request. Query param \'issuer\' is required')
       }
+
+      // Strip trailing slash
       let issuerUrl = req.query.issuer
       if (issuerUrl.endsWith('/')) issuerUrl = issuerUrl.slice(0, -1)
 
-      const configUrl = `${issuerUrl}/.well-known/openid-configuration`
-      axios.get(configUrl).then(({ data }) => {
+      // Append config pathname and validate URL
+      let configUrl = null
+      try {
+        configUrl = new URL(`${issuerUrl}/.well-known/openid-configuration`)
+        if (!configUrl.pathname.endsWith('/.well-known/openid-configuration')) {
+          throw new Error('Invalid pathname')
+        }
+      } catch (error) {
+        Logger.error(`[Auth] Failed to get openid configuration. Invalid URL "${configUrl}"`, error)
+        return res.status(400).send('Invalid request. Query param \'issuer\' is invalid')
+      }
+
+      axios.get(configUrl.toString()).then(({ data }) => {
         res.json({
           issuer: data.issuer,
           authorization_endpoint: data.authorization_endpoint,
           token_endpoint: data.token_endpoint,
           userinfo_endpoint: data.userinfo_endpoint,

Vulnerable code:            (Database.serverSettings.authOpenIDMobileRedirectURIs.length === 1 && Database.serverSettings.authOpenIDMobileRedirectURIs[0] === '*')) {
    
     * Used to auto-populate the openid URLs in config/authentication
    router.get('/auth/openid/config', async (req, res) => {
      const configUrl = `${issuerUrl}/.well-known/openid-configuration`
      axios.get(configUrl).then(({ data }) => {
Fixed code:             (Database.serverSettings.authOpenIDMobileRedirectURIs.length === 1 && Database.serverSettings.authOpenIDMobileRedirectURIs[0] === '*')) {

     * Helper route used to auto-populate the openid URLs in config/authentication
     * Takes an issuer URL as a query param and requests the config data at "/.well-known/openid-configuration"
     * 
     * @example /auth/openid/config?issuer=http://192.168.1.66:9000/application/o/audiobookshelf/
    router.get('/auth/openid/config', this.isAuthenticated, async (req, res) => {
      if (!req.user.isAdminOrUp) {
        Logger.error(`[Auth] Non-admin user "${req.user.username}" attempted to get issuer config`)
        return res.sendStatus(403)
      }


      // Strip trailing slash
      // Append config pathname and validate URL
      let configUrl = null
      try {
        configUrl = new URL(`${issuerUrl}/.well-known/openid-configuration`)
        if (!configUrl.pathname.endsWith('/.well-known/openid-configuration')) {
          throw new Error('Invalid pathname')
        }
      } catch (error) {
        Logger.error(`[Auth] Failed to get openid configuration. Invalid URL "${configUrl}"`, error)
        return res.status(400).send('Invalid request. Query param \'issuer\' is invalid')
      }

      axios.get(configUrl.toString()).then(({ data }) => {