From 84bd64c011b1e9ff1ac7e0040a0d9ba87eb9bb69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Molski?= Date: Tue, 24 Feb 2026 14:17:56 +0100 Subject: [PATCH 1/2] Fix protocols --- packages/uix-host-react/src/components/GuestUIFrame.tsx | 5 ++++- packages/uix-host/src/host.ts | 8 ++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/uix-host-react/src/components/GuestUIFrame.tsx b/packages/uix-host-react/src/components/GuestUIFrame.tsx index 135dc002..c0f544c9 100644 --- a/packages/uix-host-react/src/components/GuestUIFrame.tsx +++ b/packages/uix-host-react/src/components/GuestUIFrame.tsx @@ -99,7 +99,10 @@ export const GuestUIFrame = ({ return null; } const guest = host.guests.get(guestId); - const frameUrl = new URL(src, guest.url.href); + const readyUrl = src.startsWith("http") ? src : `https://${src}`; + const { href } = guest.url; + const readyHref = href.startsWith("http") ? href : `https://${href}`; + const frameUrl = new URL(readyUrl, readyHref); useEffect(() => { if (ref.current) { diff --git a/packages/uix-host/src/host.ts b/packages/uix-host/src/host.ts index d81116c6..0da83abc 100644 --- a/packages/uix-host/src/host.ts +++ b/packages/uix-host/src/host.ts @@ -453,7 +453,8 @@ export class Host extends Emitter { }; const isExtensionObject = isExtension(extension); - const extensionUrl = isExtensionObject ? extension.url : extension; + const extensionUrl = + (isExtensionObject ? extension.url : extension) || ""; const extensionConfiguration = isExtensionObject ? extension.configuration : undefined; @@ -462,7 +463,10 @@ export class Host extends Emitter { ? extension.extensionPoints : []; - const url = new URL(extensionUrl); + const readyExtUrl = extensionUrl.startsWith("http") + ? extensionUrl + : `https://${extensionUrl}`; + const url = new URL(readyExtUrl); guest = new Port({ owner: this.hostName, id, From b182f6234fbe44f84082e249246ce8c03030dbb6 Mon Sep 17 00:00:00 2001 From: Felix Delval Date: Thu, 12 Mar 2026 15:51:38 +0100 Subject: [PATCH 2/2] Fix protocol validation with proper security checks Replace weak .startsWith("http") validation with proper URL constructor and protocol checking. Only allow http:// and https:// protocols, rejecting dangerous protocols (javascript:, data:, file:, etc.). Failed extensions are logged but don't block other extensions from loading. Co-authored-by: Claude --- .../ExtensibleWrapper/UrlExtensionProvider.ts | 21 +--- .../src/components/GuestUIFrame.tsx | 47 +++++++- packages/uix-host/src/host.ts | 41 ++++++- packages/uix-host/src/index.ts | 1 + .../uix-host/src/utils/url-validation.test.ts | 100 ++++++++++++++++++ packages/uix-host/src/utils/url-validation.ts | 31 ++++++ 6 files changed, 216 insertions(+), 25 deletions(-) create mode 100644 packages/uix-host/src/utils/url-validation.test.ts create mode 100644 packages/uix-host/src/utils/url-validation.ts diff --git a/packages/uix-host-react/src/components/ExtensibleWrapper/UrlExtensionProvider.ts b/packages/uix-host-react/src/components/ExtensibleWrapper/UrlExtensionProvider.ts index 065d7590..22511ece 100644 --- a/packages/uix-host-react/src/components/ExtensibleWrapper/UrlExtensionProvider.ts +++ b/packages/uix-host-react/src/components/ExtensibleWrapper/UrlExtensionProvider.ts @@ -14,7 +14,11 @@ * is strictly forbidden unless prior written permission is obtained * from Adobe. **************************************************************************/ -import { ExtensionsProvider, InstalledExtensions } from "@adobe/uix-host"; +import { + ExtensionsProvider, + InstalledExtensions, + isValidHttpUrl, +} from "@adobe/uix-host"; import { Extension } from "@adobe/uix-core"; import { ExtensionPointId } from "./ExtensionManagerProvider"; const EXT_PARAM_PREFIX = "ext"; @@ -23,21 +27,6 @@ export interface ExtUrlParams { [key: string]: string; } -/** - * Validates if a URL is safe and only allows HTTP/HTTPS protocols - * @param url - The URL string to validate - * @returns true if the URL is valid and uses HTTP/HTTPS protocol, false otherwise - */ -export function isValidHttpUrl(url: string): boolean { - try { - const parsedUrl = new URL(url); - - return parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:"; - } catch { - return false; - } -} - /** * Extracts extension URLs from the query string * @ignore diff --git a/packages/uix-host-react/src/components/GuestUIFrame.tsx b/packages/uix-host-react/src/components/GuestUIFrame.tsx index c0f544c9..72155e17 100644 --- a/packages/uix-host-react/src/components/GuestUIFrame.tsx +++ b/packages/uix-host-react/src/components/GuestUIFrame.tsx @@ -15,7 +15,11 @@ import React, { useEffect, useRef } from "react"; import type { IframeHTMLAttributes } from "react"; import { useHost } from "../hooks/useHost.js"; import type { AttrTokens, SandboxToken } from "@adobe/uix-host"; -import { makeSandboxAttrs, requiredIframeProps } from "@adobe/uix-host"; +import { + makeSandboxAttrs, + requiredIframeProps, + isValidHttpUrl, +} from "@adobe/uix-host"; /** * @internal @@ -98,11 +102,44 @@ export const GuestUIFrame = ({ if (!host) { return null; } + const guest = host.guests.get(guestId); - const readyUrl = src.startsWith("http") ? src : `https://${src}`; - const { href } = guest.url; - const readyHref = href.startsWith("http") ? href : `https://${href}`; - const frameUrl = new URL(readyUrl, readyHref); + + // If guest failed to load (including URL validation failure), don't render + if (guest?.error) { + console.error( + `[UIX SDK] GuestUIFrame: Cannot render guest "${guestId}" - guest failed to load:`, + guest.error + ); + return null; + } + + // Validate and prepare src prop + let validSrc = src || ""; + if (!isValidHttpUrl(validSrc)) { + // Try prepending https:// for convenience (e.g., "localhost:3000" → "https://localhost:3000") + const withHttps = `https://${validSrc}`; + if (!isValidHttpUrl(withHttps)) { + console.error( + `[UIX SDK] GuestUIFrame: Invalid src URL for guest "${guestId}": "${src}". ` + + `Only http:// and https:// protocols are allowed.` + ); + return null; + } + validSrc = withHttps; + } + + // Construct frame URL (guest.url.href is already validated in Host.loadOneGuest) + let frameUrl: URL; + try { + frameUrl = new URL(validSrc, guest.url.href); + } catch (error) { + console.error( + `[UIX SDK] GuestUIFrame: Failed to construct URL for guest "${guestId}":`, + error + ); + return null; + } useEffect(() => { if (ref.current) { diff --git a/packages/uix-host/src/host.ts b/packages/uix-host/src/host.ts index 0da83abc..60c64095 100644 --- a/packages/uix-host/src/host.ts +++ b/packages/uix-host/src/host.ts @@ -23,6 +23,7 @@ import { Port, PortOptions } from "./port.js"; import { debugHost } from "./debug-host.js"; import { addMetrics } from "./metrics.js"; import { compareExtensions } from "./utils/compareExtensions.js"; +import { isValidHttpUrl } from "./utils/url-validation.js"; /** * Dictionary of {@link Port} objects by extension ID. @@ -463,10 +464,42 @@ export class Host extends Emitter { ? extension.extensionPoints : []; - const readyExtUrl = extensionUrl.startsWith("http") - ? extensionUrl - : `https://${extensionUrl}`; - const url = new URL(readyExtUrl); + // Validate URL protocol before attempting to create URL object + if (!isValidHttpUrl(extensionUrl)) { + const error = new Error( + `Invalid extension URL for "${id}": "${extensionUrl}". Only http:// and https:// protocols are allowed.` + ); + + // Log to console for developer visibility (always visible, not just in debug mode) + console.error(`[UIX SDK] ${error.message}`); + + // Create a Port with error set (so it's tracked as failed in the "failed" array) + guest = new Port({ + owner: this.hostName, + id, + url: new URL("about:blank"), // Safe fallback URL since Port requires URL object + runtimeContainer: this.runtimeContainer, + options: { + ...this.guestOptions, + ...options, + }, + logger: this.logger, + sharedContext: this.sharedContext, + configuration: extensionConfiguration, + extensionPoints, + events: this as Emits, + }); + guest.error = error; + this.guests.set(id, guest); + + // Emit error event (consistent with existing error handling at line 496) + this.emit("error", { host: this, guest, error }); + + // Return the failed port (don't throw - allow other extensions to load) + return guest; + } + + const url = new URL(extensionUrl); guest = new Port({ owner: this.hostName, id, diff --git a/packages/uix-host/src/index.ts b/packages/uix-host/src/index.ts index a82122aa..bdf9551f 100644 --- a/packages/uix-host/src/index.ts +++ b/packages/uix-host/src/index.ts @@ -75,3 +75,4 @@ export * from "./host.js"; export * from "./port.js"; export * from "./extensions-provider/index.js"; export * from "./dom-utils/index.js"; +export * from "./utils/url-validation.js"; diff --git a/packages/uix-host/src/utils/url-validation.test.ts b/packages/uix-host/src/utils/url-validation.test.ts new file mode 100644 index 00000000..abe49b06 --- /dev/null +++ b/packages/uix-host/src/utils/url-validation.test.ts @@ -0,0 +1,100 @@ +/* +Copyright 2022 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import { isValidHttpUrl } from "./url-validation"; + +describe("isValidHttpUrl", () => { + describe("valid URLs", () => { + it("should accept http URLs", () => { + expect(isValidHttpUrl("http://example.com")).toBe(true); + expect(isValidHttpUrl("http://localhost:3000")).toBe(true); + expect(isValidHttpUrl("http://localhost:3000/path?query=value")).toBe( + true, + ); + }); + + it("should accept https URLs", () => { + expect(isValidHttpUrl("https://example.com")).toBe(true); + expect(isValidHttpUrl("https://example.com/path")).toBe(true); + expect( + isValidHttpUrl("https://example.com:8080/path?query=value#hash"), + ).toBe(true); + }); + }); + + describe("dangerous protocols", () => { + it("should reject javascript: protocol", () => { + expect(isValidHttpUrl("javascript:alert(1)")).toBe(false); + expect(isValidHttpUrl("javascript:alert('xss')")).toBe(false); + }); + + it("should reject data: protocol", () => { + expect(isValidHttpUrl("data:text/html,")).toBe( + false, + ); + expect( + isValidHttpUrl( + "data:text/html;base64,PHNjcmlwdD5hbGVydCgneHNzJyk8L3NjcmlwdD4=", + ), + ).toBe(false); + }); + + it("should reject file: protocol", () => { + expect(isValidHttpUrl("file:///etc/passwd")).toBe(false); + expect(isValidHttpUrl("file://C:/Windows/System32/config/sam")).toBe( + false, + ); + }); + + it("should reject other protocols", () => { + expect(isValidHttpUrl("ftp://example.com")).toBe(false); + expect(isValidHttpUrl("ws://example.com")).toBe(false); + expect(isValidHttpUrl("wss://example.com")).toBe(false); + expect(isValidHttpUrl("about:blank")).toBe(false); + }); + }); + + describe("weak validation bypass attempts", () => { + it("should reject URLs starting with http but not http:", () => { + expect(isValidHttpUrl("httpx://evil.com")).toBe(false); + expect(isValidHttpUrl("httpsomething://bad.com")).toBe(false); + expect(isValidHttpUrl("http-evil://bad.com")).toBe(false); + }); + }); + + describe("malformed URLs", () => { + it("should reject invalid URL strings", () => { + expect(isValidHttpUrl("not a url")).toBe(false); + expect(isValidHttpUrl("://missing-protocol")).toBe(false); + expect(isValidHttpUrl("http://")).toBe(false); + expect(isValidHttpUrl("https://")).toBe(false); + }); + }); + + describe("edge cases", () => { + it("should reject null and undefined", () => { + expect(isValidHttpUrl(null as any)).toBe(false); + expect(isValidHttpUrl(undefined as any)).toBe(false); + }); + + it("should reject empty string", () => { + expect(isValidHttpUrl("")).toBe(false); + expect(isValidHttpUrl(" ")).toBe(false); + }); + + it("should reject non-string values", () => { + expect(isValidHttpUrl(123 as any)).toBe(false); + expect(isValidHttpUrl({} as any)).toBe(false); + expect(isValidHttpUrl([] as any)).toBe(false); + }); + }); +}); diff --git a/packages/uix-host/src/utils/url-validation.ts b/packages/uix-host/src/utils/url-validation.ts new file mode 100644 index 00000000..d3d84a6a --- /dev/null +++ b/packages/uix-host/src/utils/url-validation.ts @@ -0,0 +1,31 @@ +/* +Copyright 2022 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +/** + * Validates if a URL string is safe and uses only HTTP or HTTPS protocols. + * + * @param url - The URL string to validate + * @returns true if the URL is valid and uses http: or https: protocol, false otherwise + * @public + */ +export function isValidHttpUrl(url: string): boolean { + if (!url || typeof url !== "string") { + return false; + } + + try { + const parsedUrl = new URL(url); + return parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:"; + } catch { + return false; + } +}