Block renderering non http(s):// links via <Icon> (#6588)

* Block renderering non http(s):// links via `<Icon>`

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix type error

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Still render icon, just without href

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix unit tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton authored and GitHub committed 2022-11-17 11:10:54 -05:00
1 parent 1861fe2049
commit 56e7897bc4
7 files changed
+184 -15

No files matched your search

@@ -3,11 +3,13 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import React from "react"; import React from "react";
import { render, screen } from "@testing-library/react"; import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import type { CatalogCategorySpec } from "../../../../common/catalog"; import type { CatalogCategorySpec } from "../../../../common/catalog";
import { CatalogCategory } from "../../../../common/catalog"; import { CatalogCategory } from "../../../../common/catalog";
import { CatalogAddButton } from "../catalog-add-button"; import { CatalogAddButton } from "../catalog-add-button";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import { type DiRender, renderFor } from "../../test-utils/renderFor";
class TestCatalogCategory extends CatalogCategory { class TestCatalogCategory extends CatalogCategory {
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1"; public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
@@ -26,6 +28,14 @@ class TestCatalogCategory extends CatalogCategory {
} }
describe("CatalogAddButton", () => { describe("CatalogAddButton", () => {
let render: DiRender;
beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
render = renderFor(di);
});
it("opens Add menu", async () => { it("opens Add menu", async () => {
const category = new TestCatalogCategory(); const category = new TestCatalogCategory();
@@ -5,11 +5,20 @@
import React from "react"; import React from "react";
import "@testing-library/jest-dom/extend-expect"; import "@testing-library/jest-dom/extend-expect";
import { render } from "@testing-library/react";
import { Avatar } from "../avatar"; import { Avatar } from "../avatar";
import { Icon } from "../../icon"; import { Icon } from "../../icon";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import { type DiRender, renderFor } from "../../test-utils/renderFor";
describe("<Avatar/>", () => { describe("<Avatar/>", () => {
let render: DiRender;
beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
render = renderFor(di);
});
test("renders w/o errors", () => { test("renders w/o errors", () => {
const { container } = render(<Avatar title="John Ferguson"/>); const { container } = render(<Avatar title="John Ferguson"/>);
@@ -4,11 +4,22 @@
*/ */
import React from "react"; import React from "react";
import "@testing-library/jest-dom/extend-expect"; import "@testing-library/jest-dom/extend-expect";
import { fireEvent, render } from "@testing-library/react"; import { fireEvent } from "@testing-library/react";
import { ToBottom } from "../to-bottom"; import { ToBottom } from "../to-bottom";
import { noop } from "../../../../utils"; import { noop } from "../../../../utils";
import type { DiRender } from "../../../test-utils/renderFor";
import { renderFor } from "../../../test-utils/renderFor";
import { getDiForUnitTesting } from "../../../../getDiForUnitTesting";
describe("<ToBottom/>", () => { describe("<ToBottom/>", () => {
let render: DiRender;
beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
render = renderFor(di);
});
it("renders w/o errors", () => { it("renders w/o errors", () => {
const { container } = render(<ToBottom onClick={noop}/>); const { container } = render(<ToBottom onClick={noop}/>);
@@ -4,14 +4,19 @@
*/ */
import type { RenderResult } from "@testing-library/react"; import type { RenderResult } from "@testing-library/react";
import { render } from "@testing-library/react";
import React from "react"; import React from "react";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import { type DiRender, renderFor } from "../test-utils/renderFor";
import { DrawerParamToggler } from "./drawer-param-toggler"; import { DrawerParamToggler } from "./drawer-param-toggler";
describe("<DrawerParamToggler />", () => { describe("<DrawerParamToggler />", () => {
let result: RenderResult; let result: RenderResult;
let render: DiRender;
beforeEach(() => { beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
render = renderFor(di);
result = render(( result = render((
<DrawerParamToggler <DrawerParamToggler
label="Foo" label="Foo"
@@ -0,0 +1,93 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import type { Logger } from "../../../common/logger";
import loggerInjectable from "../../../common/logger.injectable";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import type { DiRender } from "../test-utils/renderFor";
import { renderFor } from "../test-utils/renderFor";
import { Icon } from "./icon";
describe("<Icon> href technical tests", () => {
let render: DiRender;
let logger: jest.MockedObject<Logger>;
beforeEach(() => {
const di = getDiForUnitTesting();
logger = {
debug: jest.fn(),
error: jest.fn(),
info: jest.fn(),
silly: jest.fn(),
warn: jest.fn(),
};
di.override(loggerInjectable, () => logger);
render = renderFor(di);
});
it("should render an <Icon> with http href", () => {
const result = render((
<Icon
data-testid="my-icon"
href="http://localhost"
/>
));
const icon = result.queryByTestId("my-icon");
expect(icon).toBeInTheDocument();
expect(icon).toHaveAttribute("href", "http://localhost");
expect(logger.warn).not.toBeCalled();
});
it("should render an <Icon> with https href", () => {
const result = render((
<Icon
data-testid="my-icon"
href="https://localhost"
/>
));
const icon = result.queryByTestId("my-icon");
expect(icon).toBeInTheDocument();
expect(icon).toHaveAttribute("href", "https://localhost");
expect(logger.warn).not.toBeCalled();
});
it("should warn about ws hrefs", () => {
const result = render((
<Icon
data-testid="my-icon"
href="ws://localhost"
/>
));
const icon = result.queryByTestId("my-icon");
expect(icon).toBeInTheDocument();
expect(icon).not.toHaveAttribute("href", "ws://localhost");
expect(logger.warn).toBeCalled();
});
it("should warn about javascript: hrefs", () => {
const result = render((
<Icon
data-testid="my-icon"
href="javascript:void 0"
/>
));
const icon = result.queryByTestId("my-icon");
expect(icon).toBeInTheDocument();
expect(icon).not.toHaveAttribute("href", "javascript:void 0");
expect(logger.warn).toBeCalled();
});
});
+33 -10
View File
@@ -34,6 +34,13 @@ import User from "./user.svg";
import Users from "./users.svg"; import Users from "./users.svg";
import Wheel from "./wheel.svg"; import Wheel from "./wheel.svg";
import Workloads from "./workloads.svg"; import Workloads from "./workloads.svg";
import type { Logger } from "../../../common/logger";
import { withInjectables } from "@ogre-tools/injectable-react";
import loggerInjectable from "../../../common/logger.injectable";
const hrefValidation = /https?:\/\//;
const hrefIsSafe = (href: string) => Boolean(href.match(hrefValidation));
/** /**
* Mapping between the local file names and the svgs * Mapping between the local file names and the svgs
@@ -155,16 +162,21 @@ export function isSvg(content: string): boolean {
return String(content).includes("<svg"); return String(content).includes("<svg");
} }
const RawIcon = withTooltip((props: IconProps) => { interface Dependencies {
logger: Logger;
}
const RawIcon = (props: IconProps & Dependencies) => {
const ref = createRef<HTMLAnchorElement>(); const ref = createRef<HTMLAnchorElement>();
const { const {
// skip passing props to icon's html element // skip passing props to icon's html element
className, href, link, material, svg, size, smallest, small, big, className, href, link, material, svg, size, smallest, small, big,
disabled, sticker, active, disabled, sticker, active,
focusable = true, focusable = true,
children, children,
interactive, onClick, onKeyDown, interactive, onClick, onKeyDown,
logger,
...elemProps ...elemProps
} = props; } = props;
const isInteractive = interactive ?? !!(onClick || href || link); const isInteractive = interactive ?? !!(onClick || href || link);
@@ -245,16 +257,27 @@ const RawIcon = withTooltip((props: IconProps) => {
} }
if (href) { if (href) {
return ( if (hrefIsSafe(href)) {
<a return (
{...iconProps} <a
href={href} {...iconProps}
ref={ref} href={href}
/> ref={ref}
); />
);
}
logger.warn("[ICON]: href prop is unsafe, blocking", { href });
} }
return <i {...iconProps} ref={ref} />; return <i {...iconProps} ref={ref} />;
};
const InjectedIcon = withInjectables<Dependencies, IconProps>(RawIcon, {
getProps: (di, props) => ({
...props,
logger: di.inject(loggerInjectable),
}),
}); });
export const Icon = Object.assign(RawIcon, { isSvg }); export const Icon = Object.assign(withTooltip(InjectedIcon), { isSvg });
@@ -5,9 +5,11 @@
import React from "react"; import React from "react";
import "@testing-library/jest-dom/extend-expect"; import "@testing-library/jest-dom/extend-expect";
import { render, screen, waitFor } from "@testing-library/react"; import { screen, waitFor } from "@testing-library/react";
import { ScrollSpy } from "../scroll-spy"; import { ScrollSpy } from "../scroll-spy";
import { RecursiveTreeView } from "../../tree-view"; import { RecursiveTreeView } from "../../tree-view";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import { type DiRender, renderFor } from "../../test-utils/renderFor";
const observe = jest.fn(); const observe = jest.fn();
@@ -20,6 +22,14 @@ Object.defineProperty(window, "IntersectionObserver", {
}); });
describe("<ScrollSpy/>", () => { describe("<ScrollSpy/>", () => {
let render: DiRender;
beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
render = renderFor(di);
});
it("renders w/o errors", () => { it("renders w/o errors", () => {
const { container } = render(( const { container } = render((
<ScrollSpy <ScrollSpy
@@ -94,6 +104,14 @@ describe("<ScrollSpy/>", () => {
describe("<TreeView/> dataTree inside <ScrollSpy/>", () => { describe("<TreeView/> dataTree inside <ScrollSpy/>", () => {
let render: DiRender;
beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
render = renderFor(di);
});
it("contains links to all sections", async () => { it("contains links to all sections", async () => {
render(( render((
<ScrollSpy <ScrollSpy