From 73c1e878c53d28ea33d69096be0117abc04c43cd Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Mon, 20 Apr 2026 16:11:24 -0400 Subject: [PATCH] fix(astro): harden astro-island export resolution --- .changeset/harden-astro-island-runtime.md | 5 +++++ .../astro/src/runtime/server/astro-island.ts | 17 ++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 .changeset/harden-astro-island-runtime.md diff --git a/.changeset/harden-astro-island-runtime.md b/.changeset/harden-astro-island-runtime.md new file mode 100644 index 000000000000..e466a6b6fe91 --- /dev/null +++ b/.changeset/harden-astro-island-runtime.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Hardens `astro-island` export resolution and hydration error handling for malformed component metadata diff --git a/packages/astro/src/runtime/server/astro-island.ts b/packages/astro/src/runtime/server/astro-island.ts index 9adfb94ed53f..bdf94edf72c3 100644 --- a/packages/astro/src/runtime/server/astro-island.ts +++ b/packages/astro/src/runtime/server/astro-island.ts @@ -4,6 +4,8 @@ type directiveAstroKeys = 'load' | 'idle' | 'visible' | 'media' | 'only'; +const FORBIDDEN_COMPONENT_EXPORT_KEYS = new Set(['__proto__', 'constructor', 'prototype']); + declare const Astro: { [k in directiveAstroKeys]?: ( fn: () => Promise<() => void>, @@ -20,6 +22,8 @@ declare const Astro: { const propTypes: PropTypeSelector = { 0: (value) => reviveObject(value), 1: (value) => reviveArray(value), + // nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp + // Regex props are serialized by Astro and revived here on the client. 2: (value) => new RegExp(value), 3: (value) => new Date(value), 4: (value) => new Map(reviveArray(value)), @@ -113,10 +117,21 @@ declare const Astro: { ]); const componentExport = this.getAttribute('component-export') || 'default'; if (!componentExport.includes('.')) { + if (FORBIDDEN_COMPONENT_EXPORT_KEYS.has(componentExport)) { + throw new Error(`Invalid component export path: ${componentExport}`); + } this.Component = componentModule[componentExport]; } else { this.Component = componentModule; for (const part of componentExport.split('.')) { + if ( + FORBIDDEN_COMPONENT_EXPORT_KEYS.has(part) || + !this.Component || + (typeof this.Component !== 'object' && typeof this.Component !== 'function') || + !Object.hasOwn(this.Component, part) + ) { + throw new Error(`Invalid component export path: ${componentExport}`); + } this.Component = this.Component[part]; } } @@ -127,7 +142,7 @@ declare const Astro: { this, ); } catch (e) { - console.error(`[astro-island] Error hydrating ${this.getAttribute('component-url')}`, e); + console.error('[astro-island] Error hydrating %s', this.getAttribute('component-url'), e); } }