diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..b1a54ec9d0 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,9 @@ +# Security Policy + +## Reporting a Vulnerability + +Team Lens encourages users who become aware of a security vulnerability in Lens to contact Team Lens with details of the vulnerability. Team Lens has established an email address that should be used for reporting a vulnerability. Please send descriptions of any vulnerabilities found to security@k8slens.dev. Please include details on the software and hardware configuration of your system so that we can duplicate the issue being reported. + +Team Lens hopes that users encountering a new vulnerability will contact us privately as it is in the best interests of our users that Team Lens has an opportunity to investigate and confirm a suspected vulnerability before it becomes public knowledge. + +In the case of vulnerabilities found in third-party software components used in Lens, please also notify Team Lens as described above. diff --git a/package.json b/package.json index e346f8c11f..d56f40afc9 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "kontena-lens", "productName": "Lens", "description": "Lens - The Kubernetes IDE", - "version": "4.2.0-rc.2", + "version": "4.2.0", "main": "static/build/main.js", "copyright": "© 2021, Mirantis, Inc.", "license": "MIT", diff --git a/src/main/__test__/router.test.ts b/src/main/__test__/router.test.ts new file mode 100644 index 0000000000..8c2fa9c822 --- /dev/null +++ b/src/main/__test__/router.test.ts @@ -0,0 +1,40 @@ +import { Router } from "../router"; + +const staticRoot = __dirname; + +class TestRouter extends Router { + protected resolveStaticRootPath() { + return staticRoot; + } +} + +describe("Router", () => { + it("blocks path traversal attacks", async () => { + const router = new TestRouter(); + const res = { + statusCode: 200, + end: jest.fn() + }; + + await router.handleStaticFile("../index.ts", res as any, {} as any, 0); + + expect(res.statusCode).toEqual(404); + }); + + it("serves files under static root", async () => { + const router = new TestRouter(); + const res = { + statusCode: 200, + write: jest.fn(), + setHeader: jest.fn(), + end: jest.fn() + }; + const req = { + url: "" + }; + + await router.handleStaticFile("router.test.ts", res as any, req as any, 0); + + expect(res.statusCode).toEqual(200); + }); +}); diff --git a/src/main/lens-proxy.ts b/src/main/lens-proxy.ts index 7e1aa98b7e..0bc3528a33 100644 --- a/src/main/lens-proxy.ts +++ b/src/main/lens-proxy.ts @@ -28,7 +28,7 @@ export class LensProxy { } listen(port = this.port): this { - this.proxyServer = this.buildCustomProxy().listen(port); + this.proxyServer = this.buildCustomProxy().listen(port, "127.0.0.1"); logger.info(`[LENS-PROXY]: Proxy server has started at ${this.origin}`); return this; diff --git a/src/main/router.ts b/src/main/router.ts index bb49aacdab..6fa14e1444 100644 --- a/src/main/router.ts +++ b/src/main/router.ts @@ -40,10 +40,16 @@ export interface LensApiRequest

{ export class Router { protected router: any; + protected staticRootPath: string; public constructor() { this.router = new Call.Router(); this.addRoutes(); + this.staticRootPath = this.resolveStaticRootPath(); + } + + protected resolveStaticRootPath() { + return path.resolve(__static); } public async route(cluster: Cluster, req: http.IncomingMessage, res: http.ServerResponse): Promise { @@ -102,7 +108,15 @@ export class Router { } async handleStaticFile(filePath: string, res: http.ServerResponse, req: http.IncomingMessage, retryCount = 0) { - const asset = path.join(__static, filePath); + const asset = path.join(this.staticRootPath, filePath); + const normalizedFilePath = path.resolve(asset); + + if (!normalizedFilePath.startsWith(this.staticRootPath)) { + res.statusCode = 404; + res.end(); + + return; + } try { const filename = path.basename(req.url); diff --git a/src/renderer/components/ace-editor/ace-editor.tsx b/src/renderer/components/ace-editor/ace-editor.tsx index 2dceb48bba..180c142ffd 100644 --- a/src/renderer/components/ace-editor/ace-editor.tsx +++ b/src/renderer/components/ace-editor/ace-editor.tsx @@ -32,6 +32,7 @@ const defaultProps: Partial = { useWorker: false, onBlur: noop, onFocus: noop, + cursorPos: { row: 0, column: 0 }, }; @observer diff --git a/src/renderer/components/layout/sidebar-item.tsx b/src/renderer/components/layout/sidebar-item.tsx index da36b43c17..64956842aa 100644 --- a/src/renderer/components/layout/sidebar-item.tsx +++ b/src/renderer/components/layout/sidebar-item.tsx @@ -26,7 +26,6 @@ interface SidebarItemProps { * this item should be shown as active */ isActive?: boolean; - subMenus?: React.ReactNode | React.ComponentType[]; } @observer @@ -53,11 +52,9 @@ export class SidebarItem extends React.Component { } @computed get isExpandable(): boolean { - if (this.compact) { - return false; // not available currently - } + if (this.compact) return false; // not available in compact-mode currently - return Boolean(this.props.subMenus || this.props.children); + return Boolean(this.props.children); } toggleExpand = () => { @@ -66,8 +63,22 @@ export class SidebarItem extends React.Component { }); }; + renderSubMenu() { + const { isExpandable, expanded, isActive } = this; + + if (!isExpandable || !expanded) { + return; + } + + return ( +

+ ); + } + render() { - const { isHidden, icon, text, children, url, className, subMenus } = this.props; + const { isHidden, icon, text, url, className } = this.props; if (isHidden) return null; @@ -90,12 +101,7 @@ export class SidebarItem extends React.Component { material={expanded ? "keyboard_arrow_up" : "keyboard_arrow_down"} />} - {isExpandable && expanded && ( - - )} + {this.renderSubMenu()} ); } diff --git a/src/renderer/components/layout/sidebar.tsx b/src/renderer/components/layout/sidebar.tsx index 522e06674c..531c9db1a0 100644 --- a/src/renderer/components/layout/sidebar.tsx +++ b/src/renderer/components/layout/sidebar.tsx @@ -51,25 +51,20 @@ export class Sidebar extends React.Component { } return Object.entries(crdStore.groups).map(([group, crds]) => { - const crdGroupSubMenu: React.ReactNode = crds.map((crd) => { - return ( - - ); - }); + const id = `crd-group:${group}`; + const crdGroupsPageUrl = crdURL({ query: { groups: group } }); return ( - + + {crds.map((crd) => ( + + ))} + ); }); } @@ -147,8 +142,9 @@ export class Sidebar extends React.Component { isActive={isActive} text={menuItem.title} icon={} - subMenus={this.renderTreeFromTabRoutes(tabRoutes)} - /> + > + {this.renderTreeFromTabRoutes(tabRoutes)} + ); }); } @@ -175,88 +171,94 @@ export class Sidebar extends React.Component {
} /> } /> } - /> + > + {this.renderTreeFromTabRoutes(Workloads.tabRoutes)} + } - /> + > + {this.renderTreeFromTabRoutes(Config.tabRoutes)} + } - /> + > + {this.renderTreeFromTabRoutes(Network.tabRoutes)} + } - text="Storage" - /> + > + {this.renderTreeFromTabRoutes(Storage.tabRoutes)} + } - text="Namespaces" /> } - text="Events" /> } - text="Apps" - /> + > + {this.renderTreeFromTabRoutes(Apps.tabRoutes)} + } - text="Access Control" - /> + > + {this.renderTreeFromTabRoutes(UserManagement.tabRoutes)} +