mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-12 03:41:40 +00:00
fix(panes): harden the persisted host map against unsafe pane ids
A pane id is plugin-controlled, and it becomes a KEY in the persisted
{ paneId: hostId } map. `__proto__` and friends are not ids, they are booby
traps:
- `map['__proto__'] = 'window'` on a plain object corrupts the map, and
can reach Object.prototype.
- `map[id]` on a polluted (or hand-edited) object can return a value straight
off the prototype chain for a pane that was never remembered at all — so a
pane could be "restored" to a host nobody ever put it in.
Three layers, because each is a one-liner:
- Reject `__proto__` / `constructor` / `prototype` as pane ids at
registration, so they never reach storage.
- Re-key whatever comes out of localStorage onto a null-prototype object, so
a corrupt or hand-edited value cannot smuggle a prototype in.
- Read with an own-property check.
Also from the same review:
- Removed `window.__fbPaneWindows`. It was exposed for pane-desktop.js back
when that file needed to reach the window handles; the rewrite dropped that
need and nothing has referenced it since. Dead global, and its comment
described a collaborator that no longer exists.
- Corrected the /pane cache comment. It claimed a stale page would leave the
window blank, which stopped being true when the readiness check gained a
`doc.body` fallback — it would still work, just without the pane window's
own layout. A comment that describes a failure mode the code no longer has
is worse than no comment.
Signed-off-by: topkoa <topkoa@gmail.com>
This commit is contained in:
@@ -1723,10 +1723,11 @@ def pane_host():
|
|||||||
# MOVES the real panel element into it (document.adoptNode) and copies the app's
|
# MOVES the real panel element into it (document.adoptNode) and copies the app's
|
||||||
# stylesheets across. See docs/plugin-panes.md.
|
# stylesheets across. See docs/plugin-panes.md.
|
||||||
#
|
#
|
||||||
# no-cache, matching the /static mount's contract (_RevalidatedStaticFiles). A
|
# no-cache, matching the /static mount's contract (_RevalidatedStaticFiles).
|
||||||
# stale copy of this page is especially nasty: the opener waits for an element
|
# The opener adopts the panel into this page's #fb-pane-root, so a stale copy
|
||||||
# inside it before adopting, so an old cached version means the pane window
|
# served from cache is a real hazard — it falls back to <body>, which works but
|
||||||
# simply sits there blank.
|
# loses the pane window's own layout, and a future change to the page would be
|
||||||
|
# invisible until the cache expired.
|
||||||
resp = FileResponse(str(STATIC_DIR / "panes" / "pane.html"))
|
resp = FileResponse(str(STATIC_DIR / "panes" / "pane.html"))
|
||||||
resp.headers["Cache-Control"] = "no-cache"
|
resp.headers["Cache-Control"] = "no-cache"
|
||||||
return resp
|
return resp
|
||||||
|
|||||||
@@ -45,26 +45,51 @@
|
|||||||
// Only which pane was open, and where. A pane's CONTENTS are the plugin's own
|
// Only which pane was open, and where. A pane's CONTENTS are the plugin's own
|
||||||
// DOM and the plugin's own state — none of our business.
|
// DOM and the plugin's own state — none of our business.
|
||||||
|
|
||||||
|
// A pane id is plugin-controlled and is used as a key in the persisted
|
||||||
|
// host map. `__proto__` and friends are not ids, they are booby traps: writing
|
||||||
|
// `map['__proto__'] = 'window'` on a plain object corrupts the map (and can
|
||||||
|
// reach Object.prototype), and reading `map[id]` can pick a value straight off
|
||||||
|
// the prototype chain for a pane that was never remembered at all.
|
||||||
|
//
|
||||||
|
// Rejected at registration, so the id never reaches storage — and the reads
|
||||||
|
// below are own-property checks anyway, because defence in depth is cheap here.
|
||||||
|
const UNSAFE_KEYS = ['__proto__', 'constructor', 'prototype'];
|
||||||
|
function _isUnsafeId(id) { return UNSAFE_KEYS.indexOf(id) >= 0; }
|
||||||
|
|
||||||
function _readJSON(key, fallback) {
|
function _readJSON(key, fallback) {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(key);
|
const raw = localStorage.getItem(key);
|
||||||
return raw ? JSON.parse(raw) : fallback;
|
if (!raw) return fallback;
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return fallback;
|
||||||
|
// Re-key onto a null-prototype object: whatever was in storage (hand
|
||||||
|
// edited, corrupt, polluted) can no longer smuggle in a prototype.
|
||||||
|
const safe = Object.create(null);
|
||||||
|
Object.keys(parsed).forEach((k) => { if (!_isUnsafeId(k)) safe[k] = parsed[k]; });
|
||||||
|
return safe;
|
||||||
} catch (e) { return fallback; } // private mode / corrupt value
|
} catch (e) { return fallback; } // private mode / corrupt value
|
||||||
}
|
}
|
||||||
function _writeJSON(key, value) {
|
function _writeJSON(key, value) {
|
||||||
try { localStorage.setItem(key, JSON.stringify(value)); } catch (e) { /* quota / private mode: non-fatal */ }
|
try { localStorage.setItem(key, JSON.stringify(value)); } catch (e) { /* quota / private mode: non-fatal */ }
|
||||||
}
|
}
|
||||||
function _rememberHost(id, hostId) {
|
function _rememberHost(id, hostId) {
|
||||||
const map = _readJSON(HOSTS_KEY, {});
|
if (_isUnsafeId(id)) return;
|
||||||
|
const map = _readJSON(HOSTS_KEY, Object.create(null));
|
||||||
if (hostId) map[id] = hostId; else delete map[id];
|
if (hostId) map[id] = hostId; else delete map[id];
|
||||||
_writeJSON(HOSTS_KEY, map);
|
_writeJSON(HOSTS_KEY, map);
|
||||||
}
|
}
|
||||||
|
function _rememberedHost(id) {
|
||||||
|
const map = _readJSON(HOSTS_KEY, Object.create(null));
|
||||||
|
return Object.prototype.hasOwnProperty.call(map, id) ? map[id] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Spec ─────────────────────────────────────────────────────────────────
|
// ── Spec ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function _normalize(spec) {
|
function _normalize(spec) {
|
||||||
if (!spec || typeof spec !== 'object') throw new TypeError('panes.register: spec must be an object');
|
if (!spec || typeof spec !== 'object') throw new TypeError('panes.register: spec must be an object');
|
||||||
if (!spec.id || typeof spec.id !== 'string') throw new TypeError('panes.register: spec.id is required');
|
if (!spec.id || typeof spec.id !== 'string') throw new TypeError('panes.register: spec.id is required');
|
||||||
|
// See UNSAFE_KEYS: a pane id becomes a key in the persisted host map.
|
||||||
|
if (_isUnsafeId(spec.id)) throw new TypeError('panes.register: unsafe pane id: ' + spec.id);
|
||||||
if (typeof spec.element !== 'function' && !(spec.element instanceof Element)) {
|
if (typeof spec.element !== 'function' && !(spec.element instanceof Element)) {
|
||||||
throw new TypeError('panes.register(' + spec.id + '): spec.element must be an Element, or a function returning one');
|
throw new TypeError('panes.register(' + spec.id + '): spec.element must be an Element, or a function returning one');
|
||||||
}
|
}
|
||||||
@@ -279,7 +304,7 @@
|
|||||||
// without a user gesture, so restoring a popped-out pane on page load
|
// without a user gesture, so restoring a popped-out pane on page load
|
||||||
// would only ever produce a "pop-up blocked" toast. Such a pane comes back
|
// would only ever produce a "pop-up blocked" toast. Such a pane comes back
|
||||||
// in the dock, and the chip pops it out again on the user's next click.
|
// in the dock, and the chip pops it out again on the user's next click.
|
||||||
let remembered = _readJSON(HOSTS_KEY, {})[s.id];
|
let remembered = _rememberedHost(s.id);
|
||||||
if (remembered) {
|
if (remembered) {
|
||||||
const h = hosts.get(remembered);
|
const h = hosts.get(remembered);
|
||||||
if (h && h.autoRestore === false) remembered = 'dock';
|
if (h && h.autoRestore === false) remembered = 'dock';
|
||||||
|
|||||||
@@ -278,9 +278,4 @@
|
|||||||
window.addEventListener('beforeunload', () => {
|
window.addEventListener('beforeunload', () => {
|
||||||
wins.forEach((w) => { if (!w.closed) { try { w.close(); } catch (e) { /* ignore */ } } });
|
wins.forEach((w) => { if (!w.closed) { try { w.close(); } catch (e) { /* ignore */ } } });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Exposed for pane-desktop.js, which upgrades this host in place rather than
|
|
||||||
// registering a competing one — the window still has to be opened HERE, by
|
|
||||||
// window.open(), or there would be no document to adopt into.
|
|
||||||
window.__fbPaneWindows = { FRAME_PREFIX, get: (id) => wins.get(id) || null };
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user