CVE case study

CVE-2026-61448: Malformed Content-Type Led to Stored XSS in Parse Server

Parse Server accepted malformed media types for unknown extensions, letting some storage adapters serve uploads that browsers could sniff as HTML.

Weakness
CWE-434
Affected
Parse Server 8.6.83 and earlier; 9.0.0 through 9.10.0-alpha.1
Remediation state
Upgrade to 8.6.84 or 9.10.0-alpha.2 and later
Advisory published
25 Jun 2026

Official vectorCVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:P/VC:N/VI:L/VA:N/SC:L/SI:L/SA:N

Why it matters

Parse Server’s default upload policy blocks extensions associated with browser-active content. When the filename extension was unknown, however, the server preserved the client’s media type. A malformed value could pass through validation and reach a cloud storage adapter unchanged.

Browsers do not reliably treat an invalid media type as inert. If the response lacks a valid type and anti-sniffing protection, the browser may inspect the body and render it as HTML. That converts a file-upload validation gap into stored cross-site scripting when another user opens the file URL.

How I found it

I traced Content-Type through Parse Server’s validator, storage adapter, and browser response path.

The validation extracted a subtype-like token and compared it with an extension blocklist. It did not first require the original Content-Type to be a valid type/subtype media type. I enumerated boundary shapes rather than adding more dangerous extensions: no slash, an empty subtype, and extra separators. Values such as image and image/ either produced an allowed token or a falsy token that skipped the check.

For an unrecognized extension, the server could pass the malformed value to a storage adapter. Object stores that preserved it left the browser with an invalid media type and a content-sniffing path. GridFS was the control: it recomputed the type and sent nosniff, so it was not affected.

Root cause

The validator extracted a subtype without first validating the full type/subtype value. The extension check and browser interpreted the same input differently.

GridFS was unaffected because it recomputed the type and sent X-Content-Type-Options: nosniff.

Source-to-sink trace

  1. 01
    Parser edgeFilesRouter::createHandler()

    Malformed media types can yield an allowed subtype token or an empty value that skips the blocklist condition.

  2. 02
    Representation gapFilesController::createFile()

    When the filename extension is unknown, the original client media type can remain unchanged.

  3. 03
    Persistence conditionS3/GCS/Azure-style adapters

    Some adapters store and later serve the supplied metadata rather than recalculating it.

  4. 04
    Browser sinkMIME parsing and sniffing

    An invalid response type may be sniffed from the body and rendered as HTML in the upload origin.

Safe proof of concept

Prerequisites

  • Node.js 20 or later for the offline harness.
  • A browser for the optional localhost rendering check.

Step-by-step reproduction

  1. Run the offline script below. It models the affected gate’s subtype extraction and default dangerous-extension blocklist decision.
  2. Compare the malformed cases with text/html. The recognized dangerous subtype is rejected, while the malformed representations pass.
  3. Save and run the localhost server below. Open /affected; on a browser that follows the disclosed sniffing path, the inert heading is rendered despite the malformed declared type.
  4. Open /control. This path combines a well-formed recomputed type with X-Content-Type-Options: nosniff, mirroring the important properties of the unaffected GridFS control; the body should be treated as text rather than active HTML.
  5. Upgrade Parse Server and rerun the vendor malformed-media-type regression tests; fixed releases reject the invalid type before an adapter can persist it.

Offline validator decision table

const blocked = /^(html?|xht|svg(?:z|\+xml)?|xml|xslt?(?:\+xml)?)$/i;

function affectedDecision(contentType) {
  let token;
  if (contentType.includes('/')) {
    token = contentType.split('/')[1]?.split(';')[0]?.replace(/\s+/g, '');
  } else {
    token = contentType.split(';')[0]?.replace(/\s+/g, '');
  }
  return token && blocked.test(token) ? 'REJECT' : 'PASS_TO_ADAPTER';
}

for (const value of ['image', 'image/', 'image//svg+xml', 'text/html']) {
  console.log(JSON.stringify(value), affectedDecision(value));
}

Expected offline result

"image" PASS_TO_ADAPTER
"image/" PASS_TO_ADAPTER
"image//svg+xml" PASS_TO_ADAPTER
"text/html" REJECT

Inert localhost browser-boundary server

const http = require('node:http');
const body = '<!doctype html><h1 id="proof">Local MIME proof</h1>';

http.createServer((req, res) => {
  if (req.url === '/affected') {
    res.setHeader('Content-Type', 'image/');
  } else {
    res.setHeader('Content-Type', 'text/plain; charset=utf-8');
    res.setHeader('X-Content-Type-Options', 'nosniff');
  }
  res.end(body);
}).listen(8085, '127.0.0.1', () => {
  console.log('affected: http://127.0.0.1:8085/affected');
  console.log('control:  http://127.0.0.1:8085/control');
});

Expected evidence

  • Malformed values pass the affected decision while an explicitly dangerous, well-formed subtype is rejected.
  • The localhost affected path carries the invalid media type to the browser unchanged; compatible browser behavior may render the inert heading.
  • The control combines a valid text type with nosniff, so the same body remains text rather than becoming an active HTML document.
Negative control

The default GridFS path is the product-level control because it combines MIME recomputation with X-Content-Type-Options: nosniff. The localhost /control route deliberately uses both properties; this article does not claim that the header alone is a universal navigation defense.

Fixed-version re-test

On 8.6.84, 9.10.0-alpha.2, or later, malformed media types must be rejected as invalid before reaching the adapter. Keep the strict extension allow-list, separate upload origin, and nosniff header as independent defenses.

Impact

Exploitation requires upload permission, a compatible adapter, an unknown extension, and a victim who opens the file. The script then runs in the upload origin.

Serving uploads from the primary application origin increases the consequence because browser credentials and same-origin access may be available to the rendered content.

Fix and retest

Upgrade to Parse Server 8.6.84, 9.10.0-alpha.2, or a later supported version. The fixes reject malformed media types before they reach storage and add regression coverage on both maintained branches.

Also restrict file extensions, serve uploads from a separate origin, and send X-Content-Type-Options: nosniff.

Engineering lesson

Validate filename, media type, storage metadata, response headers, and upload origin together.

References

Further reading

Evidence connected to this article.

Back to article start