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
- 01Parser edge
FilesRouter::createHandler()Malformed media types can yield an allowed subtype token or an empty value that skips the blocklist condition.
- 02Representation gap
FilesController::createFile()When the filename extension is unknown, the original client media type can remain unchanged.
- 03Persistence condition
S3/GCS/Azure-style adaptersSome adapters store and later serve the supplied metadata rather than recalculating it.
- 04Browser sink
MIME parsing and sniffingAn 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
- Run the offline script below. It models the affected gate’s subtype extraction and default dangerous-extension blocklist decision.
- Compare the malformed cases with
text/html. The recognized dangerous subtype is rejected, while the malformed representations pass. - 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. - Open
/control. This path combines a well-formed recomputed type withX-Content-Type-Options: nosniff, mirroring the important properties of the unaffected GridFS control; the body should be treated as text rather than active HTML. - 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" REJECTInert 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.
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.
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.