Skip to content

Unhandled JSON.parse() exceptions in Portal's fetchQueryStrData() crash widget on malformed preview URLs #26399

@jackhax

Description

@jackhax

Issue Summary

Description

The fetchQueryStrData() function in apps/portal/src/app.js calls JSON.parse(value) on several URL hash parameters without try/catch protection. If a preview URL contains malformed JSON in any of these parameters, the Portal widget either fails to initialize or crashes entirely.

This affects 9 parameters: button, name, isFree, isMonthly, isYearly, portalProducts, disableBackground, signupCheckboxRequired, and transistorPortalSettings.

Steps to Reproduce

  1. Run Ghost v6.19.0 with Portal enabled (default)
  2. Visit any page with a malformed preview hash, e.g.:
   https://your-ghost-site.com/#preview?button={INVALID
  1. The Portal widget does not render

For the hashchange crash path:

  1. Load a Ghost page normally (Portal renders in bottom-right)
  2. Change the URL hash to #preview?button={INVALID
  3. The Portal widget crashes and disappears — updateStateForPreviewLinks() (line 745) has no try/catch, so the exception propagates uncaught into React

Root Cause

fetchQueryStrData() (lines ~367–412) passes user-supplied URL parameter values directly to JSON.parse() with no validation or error handling:

if (key === 'button') {
    data.site.portal_button = JSON.parse(value);
} else if (key === 'name') {
    data.site.portal_name = JSON.parse(value);
}
// ... 7 more parameters

On the initial load path, the exception is caught by initSetup()'s try/catch, which sets initStatus: 'failed' and causes render() to return null. On the hashchange path (updateStateForPreviewLinks), there is no try/catch at all.

Suggested Fix

A simple safe-parse wrapper resolves all 9 instances:

function safeJSONParse(value, fallback = null) {
    try {
        return JSON.parse(value);
    } catch (e) {
        console.warn('[Portal] Invalid JSON in URL parameter:', e.message);
        return fallback;
    }
}

Replace each JSON.parse(value) call in fetchQueryStrData() with safeJSONParse(value). Additionally, wrapping updateStateForPreviewLinks() in a try/catch would prevent the uncaught exception on the hashchange path.

Environment

  • Ghost version: 6.19.0
  • Portal version: 2.36.1
  • File: apps/portal/src/app.js

Steps to Reproduce

Steps to Reproduce

  1. Run Ghost v6.19.0 with Portal enabled (default)
  2. Visit any page with a malformed preview hash, e.g.:
   https://your-ghost-site.com/#preview?button={INVALID
  1. The Portal widget does not render

For the hashchange crash path:

  1. Load a Ghost page normally (Portal renders in bottom-right)
  2. Change the URL hash to #preview?button={INVALID
  3. The Portal widget crashes and disappears — updateStateForPreviewLinks() (line 745) has no try/catch, so the exception propagates uncaught into React

Ghost Version

6.19.0

Node.js Version

18.20.8

How did you install Ghost?

Git clone

Database type

MySQL 5.7

Browser & OS version

No response

Relevant log / error output

Code of Conduct

  • I agree to be friendly and polite to people in this repository

Metadata

Metadata

Assignees

No one assigned

    Labels

    community[triage] Community features and bugs

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions