Skip to content

Harden layout persistence against poisoned localStorage - #869

Merged
bingenito merged 1 commit into
morganstanley:mainfrom
bingenito:security/core-layout-storage-hardening
Nov 3, 2026
Merged

bingenito merged 1 commit into
morganstanley:mainfrom
bingenito:security/core-layout-storage-hardening

Conversation

@bingenito

@bingenito bingenito commented Jul 31, 2026 •

Copy link
Copy Markdown
Member

Summary

loadLayout replays a layout JSON blob from localStorage on every future session, so a one-shot XSS that plants a malicious desktopJS-layouts entry keeps firing on reload. This closes that off in ContainerBase:

@codecov

codecov Bot commented Jul 31, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.70%. Comparing base (d29bfff) to head (860d8a4).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #869      +/-   ##
==========================================
+ Coverage   90.46%   90.70%   +0.24%     
==========================================
  Files          15       15              
  Lines        1122     1141      +19     
  Branches      234      228       -6     
==========================================
+ Hits         1015     1035      +20     
- Misses        102      106       +4     
+ Partials        5        0       -5     
Flag Coverage Δ
unittests 90.70% <100.00%> (+0.24%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
packages/desktopjs/src/container.ts 93.71% <100.00%> (+1.56%) ⬆️

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@bingenito bingenito changed the title Harden layout persistence against poisoned localStorage (F#13, F#14, F#16) Harden layout persistence against poisoned localStorage Jul 31, 2026
…F#16)

loadLayout replays a layout JSON blob from localStorage on every future
session, so a one-shot XSS that plants a malicious desktopJS-layouts
entry keeps firing on reload. Close that off:

- sanitizeUrl rejects non-http(s)/relative schemes (javascript:, data:,
  etc.) on persisted window urls before createWindow is called (F#14).
- Layout JSON is parsed with a reviver that strips __proto__/constructor/
  prototype keys, closing the Object.assign(window.options, ...)
  prototype-pollution gadget, and malformed/missing storage no longer
  throws out of getLayoutFromStorage/getLayouts (F#13).
- ContainerBase.storage is now set via an overridable constructor
  parameter instead of being hard-bound to window.localStorage (F#16).

F#03 (Object.assign smuggling arbitrary createWindow option keys) is
intentionally not addressed here: createWindow is a thin pass-through
shim in both remaining containers (window.open features string /
fin.desktop.Window options), so smuggled keys carry no more privilege
than a caller already has by calling createWindow/window.open directly.
The one option key that did escalate privilege, Electron's
webPreferences, is removed along with the rest of the Electron package.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@bingenito
bingenito force-pushed the security/core-layout-storage-hardening branch from d4ef278 to 860d8a4 Compare November 3, 2026 21:11
@bingenito
bingenito marked this pull request as ready for review November 3, 2026 21:17
@bingenito
bingenito requested a review from a team as a code owner November 3, 2026 21:17
Copilot AI balanced review requested due to automatic review settings November 3, 2026 21:17
@bingenito
bingenito marked this pull request as draft November 3, 2026 21:18
@bingenito
bingenito marked this pull request as ready for review November 3, 2026 21:19
@bingenito
bingenito merged commit 76c4efc into morganstanley:main Nov 3, 2026
5 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Obfuscated JavaScript URLs still allow script execution, and the protected constructor breaks existing subclass instantiation with TypeScript errors.

4 open issues
What changed in this PR

This PR adds safeguards to ContainerBase layout persistence to reduce the risk of replaying malicious data from localStorage.

Changes:

  • Centralizes stored JSON parsing, handles malformed data, and strips prototype-related keys.
  • Adds URL scheme filtering before restoring windows and guards missing group targets.
  • Adds tests for unsafe URLs, valid URLs, corrupt storage, and prototype-key stripping.
File Description
packages/​desktopjs/​tests/​unit/​container.spec.ts Adds layout-loading security and storage-handling tests.
packages/​desktopjs/​src/​container.ts Adds storage injection, defensive JSON parsing, URL filtering, and guarded group restoration.

Review effort: Balanced

: undefined;
public storage: Storage;

protected constructor(storage?: Storage) {
Comment on lines +229 to +233
const schemeMatch = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(url);
if (schemeMatch && !["http", "https"].includes(schemeMatch[1].toLowerCase())) {
this.log("warn", `Blocked layout window with disallowed url scheme '${schemeMatch[1]}'`);
return undefined;
}
}

try {
return JSON.parse(raw, (key, value) => ContainerBase.unsafeJsonKeys.has(key) ? undefined : value);
await container.loadLayout("Test");

expect(container.createWindow).toHaveBeenCalledWith("url", expect.objectContaining({ name: "1" }));
expect(({} as any).polluted).toBeUndefined();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants