Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -23,21 +27,6 @@ export interface ExtUrlParams {
[key: string]: string;
}

/**
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was exposed to the users and deletion of this code is considered backwards-incompatible change. I think we need to proxy the call to new location

* 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
Expand Down
44 changes: 42 additions & 2 deletions packages/uix-host-react/src/components/GuestUIFrame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -98,8 +102,44 @@ export const GuestUIFrame = ({
if (!host) {
return null;
}

const guest = host.guests.get(guestId);
const frameUrl = new URL(src, guest.url.href);

// 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)) {
Copy link
Contributor

@kokoc kokoc Mar 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems incorrect. This code effectively check only absolute URLs but we also support relative urls for UI Frames built on top of main guest url. This is a part of the contract and many extension are built with this feature. The check needs to happen after we build the URL const frameUrl = new URL(src, guest.url.href);

// 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) {
Expand Down
39 changes: 38 additions & 1 deletion packages/uix-host/src/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -453,7 +454,8 @@ export class Host extends Emitter<HostEvents> {
};

const isExtensionObject = isExtension(extension);
const extensionUrl = isExtensionObject ? extension.url : extension;
const extensionUrl =
(isExtensionObject ? extension.url : extension) || "";
const extensionConfiguration = isExtensionObject
? extension.configuration
: undefined;
Expand All @@ -462,6 +464,41 @@ export class Host extends Emitter<HostEvents> {
? extension.extensionPoints
: [];

// 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,
Expand Down
1 change: 1 addition & 0 deletions packages/uix-host/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
100 changes: 100 additions & 0 deletions packages/uix-host/src/utils/url-validation.test.ts
Original file line number Diff line number Diff line change
@@ -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,<script>alert(1)</script>")).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);
});
});
});
31 changes: 31 additions & 0 deletions packages/uix-host/src/utils/url-validation.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading