From cb96a5cf39a2c02296f5a5227475a1bbb8fe8ace Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Sun, 28 Jun 2026 17:51:42 -0400 Subject: [PATCH] fix(extensions): trust only bootstrapped extension ids (#5094) --- api/extensions.py | 13 +++++- static/extension_settings.js | 25 +++++++++- tests/test_extension_hooks.py | 8 +++- tests/test_extension_settings_runtime.py | 58 ++++++++++++++---------- 4 files changed, 75 insertions(+), 29 deletions(-) diff --git a/api/extensions.py b/api/extensions.py index e822d1d4..04bec03e 100644 --- a/api/extensions.py +++ b/api/extensions.py @@ -1115,7 +1115,18 @@ def get_extension_config() -> Dict[str, Any]: _EXTENSION_STYLESHEET_URLS_ENV, manifest_stylesheets or None ), } - runtime_entries = _extension_runtime_entries(manifest, disabled_ids) if manifest is not None else [] + runtime_entries: List[Dict[str, object]] = [] + if manifest is not None: + extension_state = _manifest_extension_state(manifest, disabled_ids) + runtime_entries = [ + { + "id": entry["id"], + "name": entry["name"], + "storage_owned": entry["storage_owned"], + "settings_schema": entry["settings_schema"], + } + for entry in extension_state["extensions"] + ] if runtime_entries: config["extensions"] = runtime_entries return config diff --git a/static/extension_settings.js b/static/extension_settings.js index 406d9862..1b269d6e 100644 --- a/static/extension_settings.js +++ b/static/extension_settings.js @@ -5,6 +5,8 @@ const STORAGE_PREFIX='hermes.ext.storage.'; const FIELD_TYPES=new Set(['boolean','string','number','integer','enum']); const schemas=new Map(); + const trustedExtensions=new Map(); + let trustedSeeded=false; function extensionId(value){ return String(value||'').trim(); @@ -115,9 +117,28 @@ } function primeFromStatus(statusPayload){ + const entries=normalizeSchemas(statusPayload&&statusPayload.extensions); + if(!trustedSeeded){ + trustedExtensions.clear(); + for(const entry of entries){ + trustedExtensions.set(entry.id,{ + id:entry.id, + name:entry.name, + storage_owned:entry.storage_owned, + }); + } + trustedSeeded=true; + } schemas.clear(); - for(const entry of normalizeSchemas(statusPayload&&statusPayload.extensions)){ - schemas.set(entry.id,entry); + for(const entry of entries){ + const trusted=trustedExtensions.get(entry.id); + if(!trusted) continue; + schemas.set(entry.id,{ + id:entry.id, + name:entry.name, + storage_owned:trusted.storage_owned===true, + settings_schema:trusted.storage_owned===true?entry.settings_schema:[], + }); } } diff --git a/tests/test_extension_hooks.py b/tests/test_extension_hooks.py index a2371978..e595c35c 100644 --- a/tests/test_extension_hooks.py +++ b/tests/test_extension_hooks.py @@ -238,7 +238,13 @@ def test_extension_manifest_adds_bundled_assets_before_env_urls(tmp_path, monkey "name": "templates", "storage_owned": False, "settings_schema": [], - } + }, + { + "id": "disabled", + "name": "disabled", + "storage_owned": False, + "settings_schema": [], + }, ], } diff --git a/tests/test_extension_settings_runtime.py b/tests/test_extension_settings_runtime.py index 92e54e11..abe764e7 100644 --- a/tests/test_extension_settings_runtime.py +++ b/tests/test_extension_settings_runtime.py @@ -34,6 +34,26 @@ def test_extension_settings_runtime_normalizes_persists_resets_and_clears(): const assert = require('assert'); const store = new Map(); global.window = {{ + __HERMES_EXTENSION_CONFIG__: {{ + extensions: [{{ + id: 'demo.ext', + name: 'Demo', + storage_owned: true, + settings_schema: [ + {{key: 'flag', type: 'boolean', default: false}}, + {{key: 'mode', type: 'enum', options: ['compact', {{value: 'full', label: 'Full'}}], default: 'compact'}}, + {{key: 'count', type: 'integer', default: 2}}, + {{key: 'secret', type: 'string', sensitive: true, default: 'x'}}, + {{key: 'bad', type: 'enum', options: [{{label: 'missing value'}}]}}, + {{key: 'flag', type: 'boolean', default: true}} + ] + }}, {{ + id: 'denied.ext', + name: 'Denied', + storage_owned: false, + settings_schema: [{{key: 'flag', type: 'boolean', default: false}}], + }}] + }}, localStorage: {{ getItem(key) {{ return store.has(key) ? store.get(key) : null; }}, setItem(key, value) {{ store.set(key, String(value)); }}, @@ -42,22 +62,6 @@ def test_extension_settings_runtime_normalizes_persists_resets_and_clears(): }}; eval(fs.readFileSync({str(EXTENSION_SETTINGS_JS)!r}, 'utf8')); - window.HermesExtensionSettings.primeFromStatus({{ - extensions: [{{ - id: 'demo.ext', - name: 'Demo', - storage_owned: true, - settings_schema: [ - {{key: 'flag', type: 'boolean', default: false}}, - {{key: 'mode', type: 'enum', options: ['compact', {{value: 'full', label: 'Full'}}], default: 'compact'}}, - {{key: 'count', type: 'integer', default: 2}}, - {{key: 'secret', type: 'string', sensitive: true, default: 'x'}}, - {{key: 'bad', type: 'enum', options: [{{label: 'missing value'}}]}}, - {{key: 'flag', type: 'boolean', default: true}} - ] - }}] - }}); - const settings = window.HermesExtensionSettings.settingsForExtension('demo.ext'); assert.deepStrictEqual(settings.schema.map(field => field.key), ['flag', 'mode', 'count']); assert.deepStrictEqual(settings.values, {{flag: false, mode: 'compact', count: 2}}); @@ -76,15 +80,6 @@ def test_extension_settings_runtime_normalizes_persists_resets_and_clears(): assert.strictEqual(storage.clear(), true); assert.strictEqual(store.has('hermes.ext.storage.demo.ext'), false); - window.HermesExtensionSettings.primeFromStatus({{ - extensions: [{{ - id: 'denied.ext', - name: 'Denied', - storage_owned: false, - settings_schema: [{{key: 'flag', type: 'boolean', default: false}}], - }}] - }}); - const deniedSettings = window.HermesExtensionSettings.settingsForExtension('denied.ext'); assert.strictEqual(deniedSettings.setAll({{flag: true}}).ok, false); assert.strictEqual(store.has('hermes.ext.settings.denied.ext'), false); @@ -92,6 +87,19 @@ def test_extension_settings_runtime_normalizes_persists_resets_and_clears(): const deniedStorage = window.HermesExtensionSettings.storageForExtension('denied.ext'); assert.strictEqual(deniedStorage.set('note', 'blocked'), false); assert.strictEqual(store.has('hermes.ext.storage.denied.ext'), false); + + window.HermesExtensionSettings.primeFromStatus({{ + extensions: [{{ + id: 'unknown.ext', + name: 'Unknown', + storage_owned: true, + settings_schema: [{{key: 'flag', type: 'boolean', default: false}}], + }}] + }}); + + const unknownSettings = window.HermesExtensionSettings.settingsForExtension('unknown.ext'); + assert.strictEqual(unknownSettings.setAll({{flag: true}}).ok, false); + assert.strictEqual(store.has('hermes.ext.settings.unknown.ext'), false); """ ) _run_node(script)