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

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