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
- Run Ghost v6.19.0 with Portal enabled (default)
- Visit any page with a malformed preview hash, e.g.:
https://your-ghost-site.com/#preview?button={INVALID
- The Portal widget does not render
For the hashchange crash path:
- Load a Ghost page normally (Portal renders in bottom-right)
- Change the URL hash to
#preview?button={INVALID
- 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
- Run Ghost v6.19.0 with Portal enabled (default)
- Visit any page with a malformed preview hash, e.g.:
https://your-ghost-site.com/#preview?button={INVALID
- The Portal widget does not render
For the hashchange crash path:
- Load a Ghost page normally (Portal renders in bottom-right)
- Change the URL hash to
#preview?button={INVALID
- 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
Issue Summary
Description
The
fetchQueryStrData()function inapps/portal/src/app.jscallsJSON.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, andtransistorPortalSettings.Steps to Reproduce
For the hashchange crash path:
#preview?button={INVALIDupdateStateForPreviewLinks()(line 745) has no try/catch, so the exception propagates uncaught into ReactRoot Cause
fetchQueryStrData()(lines ~367–412) passes user-supplied URL parameter values directly toJSON.parse()with no validation or error handling:On the initial load path, the exception is caught by
initSetup()'s try/catch, which setsinitStatus: 'failed'and causesrender()to returnnull. On the hashchange path (updateStateForPreviewLinks), there is no try/catch at all.Suggested Fix
A simple safe-parse wrapper resolves all 9 instances:
Replace each
JSON.parse(value)call infetchQueryStrData()withsafeJSONParse(value). Additionally, wrappingupdateStateForPreviewLinks()in a try/catch would prevent the uncaught exception on the hashchange path.Environment
apps/portal/src/app.jsSteps to Reproduce
Steps to Reproduce
For the hashchange crash path:
#preview?button={INVALIDupdateStateForPreviewLinks()(line 745) has no try/catch, so the exception propagates uncaught into ReactGhost 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