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:
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();
|
||||
});
|
||||
});
|
||||
@@ -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 });
|
||||
Reference in new issue
Block a user