VYPR
High severity7.3NVD Advisory· Published Jun 10, 2026

CVE-2026-53473

CVE-2026-53473

Description

A cross-site scripting vulnerability in migration-planner-ui-app allows attackers to execute JavaScript via a crafted discovery agent credential URL, compromising user sessions.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

A cross-site scripting vulnerability in migration-planner-ui-app allows attackers to execute JavaScript via a crafted discovery agent credential URL, compromising user sessions.

Vulnerability

A cross-site scripting (XSS) vulnerability exists in the migration-planner-ui-app due to insufficient validation of the credentialUrl provided by discovery agents when registering. The UI renders this URL as a link without proper scheme validation. Affected versions include those using react-router-dom 6.30.3 and React 18.3.1, where javascript: URLs are rendered verbatim in production builds [3].

Exploitation

An attacker can register a discovery agent with a malicious credentialUrl containing JavaScript code. When an organizational user clicks this link within the user interface, the embedded JavaScript code executes in the context of the user's browser session on the http://console.redhat.com origin [3].

Impact

Successful exploitation allows an attacker to compromise the victim's Red Hat Single Sign-On (SSO) session. This can lead to unauthorized cross-tenant data access and API actions performed as the victim user, resulting in a persistent compromise within the organization [3].

Mitigation

A fix has been implemented by introducing a safeExternalUrl() helper function to validate that the credentialUrl protocol is either http: or https:, and preventing non-http(s) URLs from rendering as links [2, 3]. The specific fixed version is not detailed in the references, but the vulnerability is addressed in the migration-planner-ui-app repository [2].

AI Insight generated on Jun 10, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

3

Patches

1
b271f2439ac1

ECOPROJECT-4725 | fix: prevent XSS via credentialUrl validation

https://github.com/kubev2v/migration-planner-ui-appGuillaume VincentJun 2, 2026via nvd-ref
5 files changed · +179 13
  • src/ui/assessment/views/CreateFromOva.tsx+5 2 modified
    @@ -21,6 +21,7 @@ import React from "react";
     
     import { routes } from "../../../routing/Routes";
     import { AppPage } from "../../core/components/AppPage";
    +import { safeExternalUrl } from "../../core/utils/urlValidation";
     import { EnvironmentPageProvider } from "../../environment/view-models/EnvironmentPageContext";
     import { DiscoverySourceSetupModal } from "../../environment/views/DiscoverySourceSetupModal";
     import { SourcesTable } from "../../environment/views/SourcesTable";
    @@ -190,10 +191,12 @@ const CreateFromOvaContent: React.FC = () => {
                     variant="custom"
                     title="Discovery VM"
                     actionLinks={
    -                  vm.createdSource?.agent?.credentialUrl ? (
    +                  safeExternalUrl(vm.createdSource?.agent?.credentialUrl) ? (
                         <AlertActionLink
                           component="a"
    -                      href={vm.createdSource.agent.credentialUrl}
    +                      href={safeExternalUrl(
    +                        vm.createdSource.agent.credentialUrl,
    +                      )}
                           target="_blank"
                           rel="noopener noreferrer"
                         >
    
  • src/ui/core/utils/__tests__/urlValidation.test.ts+129 0 added
    @@ -0,0 +1,129 @@
    +import { safeExternalUrl } from "../urlValidation";
    +
    +describe("safeExternalUrl", () => {
    +  describe("should return the URL for valid http/https URLs", () => {
    +    test("https URL", () => {
    +      expect(safeExternalUrl("https://example.com")).toBe(
    +        "https://example.com",
    +      );
    +    });
    +
    +    test("http URL", () => {
    +      expect(safeExternalUrl("http://example.com")).toBe("http://example.com");
    +    });
    +
    +    test("https URL with path", () => {
    +      expect(safeExternalUrl("https://example.com/path/to/resource")).toBe(
    +        "https://example.com/path/to/resource",
    +      );
    +    });
    +
    +    test("https URL with query parameters", () => {
    +      expect(safeExternalUrl("https://example.com?foo=bar&baz=qux")).toBe(
    +        "https://example.com?foo=bar&baz=qux",
    +      );
    +    });
    +
    +    test("https URL with fragment", () => {
    +      expect(safeExternalUrl("https://example.com#section")).toBe(
    +        "https://example.com#section",
    +      );
    +    });
    +
    +    test("https URL with port", () => {
    +      expect(safeExternalUrl("https://example.com:8443/api")).toBe(
    +        "https://example.com:8443/api",
    +      );
    +    });
    +  });
    +
    +  describe("should return undefined for dangerous protocols (XSS prevention)", () => {
    +    test("javascript: protocol", () => {
    +      expect(safeExternalUrl("javascript:alert(1)")).toBeUndefined();
    +    });
    +
    +    test("javascript: protocol with encoded characters", () => {
    +      expect(
    +        safeExternalUrl("javascript:alert(document.cookie)"),
    +      ).toBeUndefined();
    +    });
    +
    +    test("data: protocol", () => {
    +      expect(
    +        safeExternalUrl("data:text/html,<script>alert(1)</script>"),
    +      ).toBeUndefined();
    +    });
    +
    +    test("vbscript: protocol", () => {
    +      expect(safeExternalUrl("vbscript:msgbox(1)")).toBeUndefined();
    +    });
    +
    +    test("file: protocol", () => {
    +      expect(safeExternalUrl("file:///etc/passwd")).toBeUndefined();
    +    });
    +
    +    test("ftp: protocol", () => {
    +      expect(safeExternalUrl("ftp://example.com")).toBeUndefined();
    +    });
    +  });
    +
    +  describe("should return undefined for null, undefined, and empty values", () => {
    +    test("null", () => {
    +      expect(safeExternalUrl(null)).toBeUndefined();
    +    });
    +
    +    test("undefined", () => {
    +      expect(safeExternalUrl(undefined)).toBeUndefined();
    +    });
    +
    +    test("empty string", () => {
    +      expect(safeExternalUrl("")).toBeUndefined();
    +    });
    +  });
    +
    +  describe("should return undefined for invalid URLs", () => {
    +    test("malformed URL", () => {
    +      expect(safeExternalUrl("not-a-url")).toBeUndefined();
    +    });
    +
    +    test("URL without protocol", () => {
    +      expect(safeExternalUrl("example.com")).toBeUndefined();
    +    });
    +
    +    test("invalid characters", () => {
    +      expect(safeExternalUrl("https://exam ple.com")).toBeUndefined();
    +    });
    +  });
    +
    +  describe("edge cases", () => {
    +    test("protocol-relative URL (//example.com)", () => {
    +      // Protocol-relative URLs are not valid for URL constructor without a base
    +      expect(safeExternalUrl("//example.com")).toBeUndefined();
    +    });
    +
    +    test("URL with credentials", () => {
    +      expect(safeExternalUrl("https://user:pass@example.com")).toBe(
    +        "https://user:pass@example.com",
    +      );
    +    });
    +
    +    test("localhost URL", () => {
    +      expect(safeExternalUrl("http://localhost:3000")).toBe(
    +        "http://localhost:3000",
    +      );
    +    });
    +
    +    test("IP address URL", () => {
    +      expect(safeExternalUrl("https://192.168.1.1")).toBe(
    +        "https://192.168.1.1",
    +      );
    +    });
    +
    +    test("mixed case protocol (hTTps:)", () => {
    +      // URL constructor normalizes protocols to lowercase
    +      expect(safeExternalUrl("hTTps://example.com")).toBe(
    +        "hTTps://example.com",
    +      );
    +    });
    +  });
    +});
    
  • src/ui/core/utils/urlValidation.ts+28 0 added
    @@ -0,0 +1,28 @@
    +/**
    + * Parses a URL and returns it only if the protocol is http: or https:.
    + * This prevents XSS attacks via javascript:, data:, vbscript:, and other malicious protocols.
    + *
    + * @param url - The URL string to validate
    + * @returns The original URL string if safe (http/https), or undefined otherwise
    + *
    + * @example
    + * safeExternalUrl('https://example.com') // 'https://example.com'
    + * safeExternalUrl('http://example.com')  // 'http://example.com'
    + * safeExternalUrl('javascript:alert(1)') // undefined
    + * safeExternalUrl('data:text/html,<script>') // undefined
    + * safeExternalUrl(null) // undefined
    + */
    +export const safeExternalUrl = (
    +  url: string | undefined | null,
    +): string | undefined => {
    +  if (!url) {
    +    return undefined;
    +  }
    +
    +  try {
    +    const parsed = new URL(url);
    +    return ["http:", "https:"].includes(parsed.protocol) ? url : undefined;
    +  } catch {
    +    return undefined;
    +  }
    +};
    
  • src/ui/environment/views/AgentStatusView.tsx+6 3 modified
    @@ -22,6 +22,7 @@ import React, { useMemo } from "react";
     import { Link } from "react-router-dom";
     
     import { VCenterSetupInstructions } from "../../core/components/VCenterSetupInstructions";
    +import { safeExternalUrl } from "../../core/utils/urlValidation";
     
     // eslint-disable-next-line @typescript-eslint/no-namespace
     export namespace AgentStatusView {
    @@ -38,6 +39,8 @@ export namespace AgentStatusView {
     const StatusInfoWaitingForCredentials: React.FC<{
       credentialUrl?: Agent["credentialUrl"];
     }> = ({ credentialUrl }) => {
    +  const safeUrl = safeExternalUrl(credentialUrl);
    +
       return (
         <>
           <Content>
    @@ -46,9 +49,9 @@ const StatusInfoWaitingForCredentials: React.FC<{
               VMware environment.
             </Content>
           </Content>
    -      {credentialUrl && (
    -        <Link to={credentialUrl} target="_blank">
    -          {credentialUrl}
    +      {safeUrl && (
    +        <Link to={safeUrl} target="_blank">
    +          {safeUrl}
             </Link>
           )}
         </>
    
  • src/ui/environment/views/Environment.tsx+11 8 modified
    @@ -8,6 +8,7 @@ import {
     import React, { useCallback, useEffect, useState } from "react";
     
     import { LoadingSpinner } from "../../core/components/LoadingSpinner";
    +import { safeExternalUrl } from "../../core/utils/urlValidation";
     import { useEnvironmentPage } from "../view-models/EnvironmentPageContext";
     import type { EnvironmentPageViewModel } from "../view-models/useEnvironmentPageViewModel";
     import { DiscoverySourceSetupModal } from "./DiscoverySourceSetupModal";
    @@ -126,14 +127,16 @@ const EnvironmentContent: React.FC<EnvironmentContentProps> = ({ vm }) => {
                   variant="custom"
                   title="Discovery VM"
                   actionLinks={
    -                <AlertActionLink
    -                  component="a"
    -                  href={sourceSelected?.agent.credentialUrl}
    -                  target="_blank"
    -                  rel="noopener noreferrer"
    -                >
    -                  {sourceSelected?.agent.credentialUrl}
    -                </AlertActionLink>
    +                safeExternalUrl(sourceSelected?.agent.credentialUrl) ? (
    +                  <AlertActionLink
    +                    component="a"
    +                    href={safeExternalUrl(sourceSelected.agent.credentialUrl)}
    +                    target="_blank"
    +                    rel="noopener noreferrer"
    +                  >
    +                    {sourceSelected.agent.credentialUrl}
    +                  </AlertActionLink>
    +                ) : undefined
                   }
                 >
                   <Content>
    

Vulnerability mechanics

Root cause

"The UI renders a discovery agent's credentialUrl without validating its scheme, allowing malicious JavaScript execution."

Attack vector

An attacker can register a discovery agent with a specially crafted `credentialUrl` containing JavaScript code, such as `javascript:...` [ref_id=2]. When an organizational user clicks this link within the user interface, the embedded malicious code executes in the context of the user's browser session [ref_id=2]. This cross-site scripting vulnerability can lead to the compromise of the victim's Red Hat Single Sign-On (SSO) session [ref_id=2].

Affected code

The vulnerability resides in the rendering of the `credentialUrl` property of a discovery agent. Specifically, the UI components `Environment.tsx`, `AgentStatusView.tsx`, and `CreateFromOva.tsx` were found to render this URL without proper validation [ref_id=2]. The patch modifies `src/ui/core/utils/urlValidation.ts` to include the `safeExternalUrl` function and updates `src/ui/environment/views/Environment.tsx`, `src/ui/environment/views/AgentStatusView.tsx`, and `src/ui/assessment/views/CreateFromOva.tsx` to utilize this validation.

What the fix does

The patch introduces a `safeExternalUrl()` helper function that validates the `credentialUrl` before rendering it. This function ensures that only `http:` and `https:` protocols are allowed, effectively blocking malicious schemes like `javascript:` and `data:` [patch_id=5486707]. The affected UI components now use this helper to gate the rendering of the `credentialUrl` as a clickable link, preventing the execution of arbitrary JavaScript [patch_id=5486707].

Preconditions

  • authThe attacker must have the ability to register a discovery agent.
  • inputThe attacker must provide a malicious `credentialUrl` containing JavaScript code.
  • networkThe victim user must interact with the UI where the malicious link is displayed.

Generated on Jun 10, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

3

News mentions

0

No linked articles in our index yet.