Skip to content
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

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

Merged
merged 6 commits into from Nov 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -3,11 +3,13 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
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 type { CatalogCategorySpec } from "../../../../common/catalog";
import { CatalogCategory } from "../../../../common/catalog";
import { CatalogAddButton } from "../catalog-add-button";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import { type DiRender, renderFor } from "../../test-utils/renderFor";

class TestCatalogCategory extends CatalogCategory {
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
Expand All @@ -26,6 +28,14 @@ class TestCatalogCategory extends CatalogCategory {
}

describe("CatalogAddButton", () => {
let render: DiRender;

beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });

render = renderFor(di);
});

it("opens Add menu", async () => {
const category = new TestCatalogCategory();

Expand Down
11 changes: 10 additions & 1 deletion src/renderer/components/avatar/__tests__/avatar.test.tsx
Expand Up @@ -5,11 +5,20 @@

import React from "react";
import "@testing-library/jest-dom/extend-expect";
import { render } from "@testing-library/react";
import { Avatar } from "../avatar";
import { Icon } from "../../icon";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import { type DiRender, renderFor } from "../../test-utils/renderFor";

describe("<Avatar/>", () => {
let render: DiRender;

beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });

render = renderFor(di);
});

test("renders w/o errors", () => {
const { container } = render(<Avatar title="John Ferguson"/>);

Expand Down
13 changes: 12 additions & 1 deletion src/renderer/components/dock/logs/__test__/to-bottom.test.tsx
Expand Up @@ -4,11 +4,22 @@
*/
import React from "react";
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 { noop } from "../../../../utils";
import type { DiRender } from "../../../test-utils/renderFor";
import { renderFor } from "../../../test-utils/renderFor";
import { getDiForUnitTesting } from "../../../../getDiForUnitTesting";

describe("<ToBottom/>", () => {
let render: DiRender;

beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });

render = renderFor(di);
});

it("renders w/o errors", () => {
const { container } = render(<ToBottom onClick={noop}/>);

Expand Down
Expand Up @@ -4,14 +4,19 @@
*/

import type { RenderResult } from "@testing-library/react";
import { render } from "@testing-library/react";
import React from "react";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import { type DiRender, renderFor } from "../test-utils/renderFor";
import { DrawerParamToggler } from "./drawer-param-toggler";

describe("<DrawerParamToggler />", () => {
let result: RenderResult;
let render: DiRender;

beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });

render = renderFor(di);
result = render((
<DrawerParamToggler
label="Foo"
Expand Down
93 changes: 93 additions & 0 deletions src/renderer/components/icon/icon.test.tsx
@@ -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();
});
});
43 changes: 33 additions & 10 deletions src/renderer/components/icon/icon.tsx
Expand Up @@ -34,6 +34,13 @@ import User from "./user.svg";
import Users from "./users.svg";
import Wheel from "./wheel.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
Expand Down Expand Up @@ -155,16 +162,21 @@ export function isSvg(content: string): boolean {
return String(content).includes("<svg");
}

const RawIcon = withTooltip((props: IconProps) => {
interface Dependencies {
logger: Logger;
}

const RawIcon = (props: IconProps & Dependencies) => {
const ref = createRef<HTMLAnchorElement>();

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,
disabled, sticker, active,
focusable = true,
children,
interactive, onClick, onKeyDown,
logger,
...elemProps
} = props;
const isInteractive = interactive ?? !!(onClick || href || link);
Expand Down Expand Up @@ -245,16 +257,27 @@ const RawIcon = withTooltip((props: IconProps) => {
}

if (href) {
return (
<a
{...iconProps}
href={href}
ref={ref}
/>
);
if (hrefIsSafe(href)) {
return (
<a
{...iconProps}
href={href}
ref={ref}
/>
);
}

logger.warn("[ICON]: href prop is unsafe, blocking", { href });
}

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 });
Expand Up @@ -5,9 +5,11 @@

import React from "react";
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 { RecursiveTreeView } from "../../tree-view";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import { type DiRender, renderFor } from "../../test-utils/renderFor";

const observe = jest.fn();

Expand All @@ -20,6 +22,14 @@ Object.defineProperty(window, "IntersectionObserver", {
});

describe("<ScrollSpy/>", () => {
let render: DiRender;

beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });

render = renderFor(di);
});

it("renders w/o errors", () => {
const { container } = render((
<ScrollSpy
Expand Down Expand Up @@ -94,6 +104,14 @@ describe("<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 () => {
render((
<ScrollSpy
Expand Down