Summary
The GET /api/libraries/:id/download endpoint validates that the requesting user has access to the library specified in the URL path, but fetches downloadable items solely by attacker-provided IDs without constraining them to that library. An authenticated user with download permission and access to any one library can exfiltrate the full file contents of items belonging to any other library, including libraries they are explicitly denied access to.
Severity
Medium (CVSS 3.1: 6.5)
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
- Attack Vector: Network — the endpoint is a standard HTTP API route
- Attack Complexity: Low — exploitation requires a single crafted request; no race conditions or special configuration
- Privileges Required: Low — requires an authenticated user with
canDownload permission and access to at least one library
- User Interaction: None
- Scope: Unchanged — the impact stays within the audiobookshelf application's authorization boundary
- Confidentiality Impact: High — full file contents (audiobooks, podcasts, ebooks) from any library can be exfiltrated
- Integrity Impact: None — the endpoint is read-only
- Availability Impact: None — no denial of service
Affected Component
server/controllers/LibraryController.js — downloadMultiple (lines 1422–1459)
CWE
- CWE-863: Incorrect Authorization
Description
Missing library scope in bulk download query
The downloadMultiple handler accepts a comma-separated list of library item IDs via the ids query parameter and fetches them from the database without constraining the query to the library specified in the URL path:
// server/controllers/LibraryController.js:1433-1440
const itemIds = req.query.ids.split(',')
const libraryItems = await Database.libraryItemModel.findAll({
attributes: ['id', 'libraryId', 'path', 'isFile'],
where: {
id: itemIds
// Missing: libraryId: req.library.id
}
})
The LibraryItem Sequelize model has no defaultScope, beforeFind hook, or any other data-layer mechanism that would automatically restrict queries by libraryId. The Database.libraryItemModel getter returns the raw Sequelize model without any wrapper or proxy.
Inconsistency with other endpoints in the same controller
The codebase is aware of the need for library scoping — nearly every other method in LibraryController that queries library items includes a libraryId filter. For example:
removeAllMetadataFiles (line 1338):
const libraryItemsWithMetadata = await Database.libraryItemModel.findAll({
attributes: ['id', 'libraryFiles'],
where: [
{
libraryId: req.library.id // Properly scoped
},
// ...
]
})
removeLibraryItemsWithIssues (line 656):
const libraryItemsWithIssues = await Database.libraryItemModel.findAll({
where: {
libraryId: req.library.id, // Properly scoped
// ...
}
})
Single-item download (LibraryItemController.middleware, line 1177):
if (!req.user.checkCanAccessLibraryItem(req.libraryItem)) {
return res.sendStatus(403) // Checks libraryId via checkCanAccessLibrary()
}
The downloadMultiple endpoint is the only bulk operation on library items that omits library scoping.
Execution chain
- Attacker authenticates and obtains a valid session/token (requires
canDownload permission and access to at least one library)
- Attacker sends
GET /api/libraries/<accessible-library-id>/download?ids=<target-item-id-from-other-library>
LibraryController.middleware runs: checkCanAccessLibrary(req.params.id) passes because the URL contains a library the attacker can access
downloadMultiple runs: req.user.canDownload passes
- The database query fetches items by ID only — the target item from the inaccessible library is returned
zipHelpers.zipDirectoriesPipe reads the target item's filesystem path and streams it as a ZIP to the attacker
- The attacker receives the full file contents of the item from the library they cannot access
Proof of Concept
# Alice has access to library 11111111-... but NOT to the library containing item 44444444-...
curl -s -H 'Authorization: Bearer $ALICE_TOKEN' \
'http://127.0.0.1:3891/audiobookshelf/api/libraries/11111111-1111-4111-8111-111111111111/download?ids=44444444-4444-4444-8444-444444444444' \
-o leak.zip
unzip -l leak.zip
# Shows files from the restricted library
unzip -p leak.zip secret.mp3
# Extracts the audio file from the restricted library
Impact
- Confidentiality breach: Any authenticated user with download permission can exfiltrate the full file contents (audiobooks, podcasts, ebooks, metadata) of items from libraries they are explicitly denied access to
- Access control bypass: The per-library permission model (
librariesAccessible) is rendered ineffective for bulk downloads
- Data exfiltration at scale: Multiple item IDs can be provided in a single request, enabling bulk exfiltration
Recommended Remediation
Option 1: Add libraryId constraint to the database query (preferred)
Add libraryId: req.library.id to the where clause so the query only returns items belonging to the authorized library:
// server/controllers/LibraryController.js — downloadMultiple
const libraryItems = await Database.libraryItemModel.findAll({
attributes: ['id', 'libraryId', 'path', 'isFile'],
where: {
id: itemIds,
libraryId: req.library.id // Scope to the authorized library
}
})
This is the lowest-layer fix and is consistent with how every other method in the controller scopes its queries.
Option 2: Validate each item's library membership after fetching
If there is a reason to keep the query broad, filter results after fetching and optionally log the discrepancy:
const libraryItems = await Database.libraryItemModel.findAll({
attributes: ['id', 'libraryId', 'path', 'isFile'],
where: {
id: itemIds
}
})
const authorizedItems = libraryItems.filter((li) => li.libraryId === req.library.id)
if (authorizedItems.length < libraryItems.length) {
Logger.warn(`[LibraryController] User "${req.user.username}" attempted to download items from unauthorized libraries`)
}
Option 1 is preferred as it prevents unauthorized data from ever being loaded from the database.
Credit
This vulnerability was discovered and reported by bugbunny.ai.
Summary
The
GET /api/libraries/:id/downloadendpoint validates that the requesting user has access to the library specified in the URL path, but fetches downloadable items solely by attacker-provided IDs without constraining them to that library. An authenticated user with download permission and access to any one library can exfiltrate the full file contents of items belonging to any other library, including libraries they are explicitly denied access to.Severity
Medium (CVSS 3.1: 6.5)
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:NcanDownloadpermission and access to at least one libraryAffected Component
server/controllers/LibraryController.js—downloadMultiple(lines 1422–1459)CWE
Description
Missing library scope in bulk download query
The
downloadMultiplehandler accepts a comma-separated list of library item IDs via theidsquery parameter and fetches them from the database without constraining the query to the library specified in the URL path:The
LibraryItemSequelize model has nodefaultScope,beforeFindhook, or any other data-layer mechanism that would automatically restrict queries bylibraryId. TheDatabase.libraryItemModelgetter returns the raw Sequelize model without any wrapper or proxy.Inconsistency with other endpoints in the same controller
The codebase is aware of the need for library scoping — nearly every other method in
LibraryControllerthat queries library items includes alibraryIdfilter. For example:removeAllMetadataFiles(line 1338):removeLibraryItemsWithIssues(line 656):Single-item download (
LibraryItemController.middleware, line 1177):The
downloadMultipleendpoint is the only bulk operation on library items that omits library scoping.Execution chain
canDownloadpermission and access to at least one library)GET /api/libraries/<accessible-library-id>/download?ids=<target-item-id-from-other-library>LibraryController.middlewareruns:checkCanAccessLibrary(req.params.id)passes because the URL contains a library the attacker can accessdownloadMultipleruns:req.user.canDownloadpasseszipHelpers.zipDirectoriesPipereads the target item's filesystem path and streams it as a ZIP to the attackerProof of Concept
Impact
librariesAccessible) is rendered ineffective for bulk downloadsRecommended Remediation
Option 1: Add libraryId constraint to the database query (preferred)
Add
libraryId: req.library.idto thewhereclause so the query only returns items belonging to the authorized library:This is the lowest-layer fix and is consistent with how every other method in the controller scopes its queries.
Option 2: Validate each item's library membership after fetching
If there is a reason to keep the query broad, filter results after fetching and optionally log the discrepancy:
Option 1 is preferred as it prevents unauthorized data from ever being loaded from the database.
Credit
This vulnerability was discovered and reported by bugbunny.ai.