Merge pull request #1271 from bilawalsidhu/codex/fix-share-style-keys

fix(share): reject inherited style keys during restore
This commit is contained in:
Said Turksever authored and GitHub committed 2026-10-22 23:30:16 +01:00
commit 62c319b575
7 files changed
+50 -8

No files matched your search

+1
View File
@@ -0,0 +1 @@
- Reject inherited style keys during restore (asturksever, #1271).
+4 -6
View File
@@ -9,12 +9,10 @@ broader suffix variants. Follow-ups retain boundary provenance, and cancelled
lookups preserve the previous analyst cohort and boundary source. Navigation lookups preserve the previous analyst cohort and boundary source. Navigation
search keeps a successful geocode, with no search or view-based region recovery. search keeps a successful geocode, with no search or view-based region recovery.
Global OpenSky and adsb.lol military snapshot refreshes are single-flight through Share links and structured views accept only own keys in their style registries.
body reading and cache publication. Each caller retains its own response and Inherited names such as `constructor` and `__proto__` fall back to the existing
viewport-specific regional fallback. OpenSky 5xx responses cancel their bodies, normal/null defaults without interrupting other restored fields. Programmatic
start a 15-second cooldown and serve dated last-good data with the upstream status visual-style calls ignore unknown or non-string values before mutating state.
in the stale reason. Cold failures use the existing regional fallback or a JSON
upstream error; no error HTML is relayed as JSON.
Shared distance checks use `src/geoDistance.js` with `{ lat, lon }` points and Shared distance checks use `src/geoDistance.js` with `{ lat, lon }` points and
the IUGG mean spherical Earth radius, 6371.0088 km. `greatCircleKm` is the IUGG mean spherical Earth radius, 6371.0088 km. `greatCircleKm` is
+17
View File
@@ -778,3 +778,20 @@ test('visual input listeners are revoked before asynchronous UI teardown', () =>
assert.match(VisualSettings.prototype.stop.toString(), /this\._styleParameters\?\.destroy\(\)/); assert.match(VisualSettings.prototype.stop.toString(), /this\._styleParameters\?\.destroy\(\)/);
assert.match(VisualSettings.prototype.stop.toString(), /this\._visualEffects\.stop\(\)/); assert.match(VisualSettings.prototype.stop.toString(), /this\._visualEffects\.stop\(\)/);
}); });
test('share restore rejects inherited styles while continuing later fields', () => {
for (const style of ['constructor', '__proto__', 'toString', 'hasOwnProperty', 'unknown']) {
const restored = makeManager(`#lat=10&lon=20&style=${style}&cr=1`).parseInitialHash();
assert.equal(restored.style, 'normal');
assert.equal(restored.celestialRing, true);
}
});
test('programmatic invalid styles are ignored before changing visual state', () => {
const target = { activeStyle: 'normal' };
for (const value of ['constructor', '__proto__', 'toString', 'hasOwnProperty', 'unknown', {}, null, () => {}]) {
assert.doesNotThrow(() => VisualSettings.prototype.setStyle.call(target, value));
assert.equal(target.activeStyle, 'normal');
}
});
+3 -1
View File
@@ -175,7 +175,9 @@ export class ShareLinkManager {
parseOr(params.get('dd'), 50), parseOr(params.get('dd'), 50),
50, 50,
); );
const style = URL_TO_STYLE[params.get('style')] || 'normal'; const style = Object.hasOwn(URL_TO_STYLE, params.get('style'))
? URL_TO_STYLE[params.get('style')]
: 'normal';
const decodedLayerState = decodeLayerStateParams(params); const decodedLayerState = decodeLayerStateParams(params);
const state = { const state = {
lat, lat,
+5
View File
@@ -1538,6 +1538,11 @@ export class VisualSettings {
preserveCyberRestore = false, preserveCyberRestore = false,
} = {}, } = {},
) { ) {
if (
typeof styleName !== 'string' ||
(styleName !== 'normal' && !Object.hasOwn(STYLES, styleName))
)
return;
const { setDetectionStyle } = this.services; const { setDetectionStyle } = this.services;
if (!restore) this.shareLinkManager?.claimRestoreLane?.('visual'); if (!restore) this.shareLinkManager?.claimRestoreLane?.('visual');
if (this.hud.getVariant() === 'cyber') { if (this.hud.getVariant() === 'cyber') {
+3 -1
View File
@@ -370,7 +370,9 @@ export function viewFromParams(params) {
pitch_deg: number('pitch'), pitch_deg: number('pitch'),
}, },
layers: state?.enabledLayerIds ?? [], layers: state?.enabledLayerIds ?? [],
style: STYLES_BY_URL_NAME[params.get('style')] ?? null, style: Object.hasOwn(STYLES_BY_URL_NAME, params.get('style'))
? STYLES_BY_URL_NAME[params.get('style')]
: null,
map: VIEW_MAPS.includes(map) ? map : null, map: VIEW_MAPS.includes(map) ? map : null,
follow, follow,
annotations: annotationsFromParams(params), annotations: annotationsFromParams(params),
+17
View File
@@ -126,3 +126,20 @@ test('annotations travel in the link, keeping only what the app draws', async ()
// Too long for a link: the view keeps them, the link leaves them out. // Too long for a link: the view keeps them, the link leaves them out.
assert.equal(viewToParams(many).has('an'), false); assert.equal(viewToParams(many).has('an'), false);
}); });
test('inherited and unknown style keys do not corrupt a restored view', () => {
for (const style of [
'constructor',
'__proto__',
'toString',
'hasOwnProperty',
'unknown',
]) {
const view = viewFromParams(
new URLSearchParams({ lat: '10', lon: '20', style, map: 'osm' }),
);
assert.equal(view.style, null);
assert.equal(view.map, 'osm');
assert.equal(view.camera.lat, 10);
}
});