1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

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 2022-11-17 08:10:54 -08:00 committed by GitHub
parent 1861fe2049
commit 56e7897bc4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 184 additions and 15 deletions

View File

@ -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();

View File

@ -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"/>);

View File

@ -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}/>);

View File

@ -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"

View File

@ -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();
});
});

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 });

View File

@ -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