-
Notifications
You must be signed in to change notification settings - Fork 9
Fix protocol validation with proper security checks for extension URLs #113
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,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)) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| // 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) { | ||
|
|
||
| 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); | ||
| }); | ||
| }); | ||
| }); |
| 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
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