From c987bf5abef7dea1ad33aa2a456a99a9c311feec Mon Sep 17 00:00:00 2001 From: Kean Mariotti Date: Fri, 16 Dec 2022 13:57:23 +0000 Subject: [PATCH] Improve cross-tool protocol's origin allow listing Test: npm run build:all && npm run test:all Bug: b/260994827 Change-Id: Iab8db927a55f060784a375f00a831bd10020ed0c --- .../src/common/utils/global_config.ts | 10 ++-- .../src/cross_tool/cross_tool_protocol.ts | 30 +++++++---- .../src/cross_tool/origin_allow_list.spec.ts | 54 +++++++++++++++++++ .../src/cross_tool/origin_allow_list.ts | 52 ++++++++++++++++++ tools/winscope-ng/src/main.dev.ts | 1 - 5 files changed, 128 insertions(+), 19 deletions(-) create mode 100644 tools/winscope-ng/src/cross_tool/origin_allow_list.spec.ts create mode 100644 tools/winscope-ng/src/cross_tool/origin_allow_list.ts diff --git a/tools/winscope-ng/src/common/utils/global_config.ts b/tools/winscope-ng/src/common/utils/global_config.ts index eaff74462..d196feadd 100644 --- a/tools/winscope-ng/src/common/utils/global_config.ts +++ b/tools/winscope-ng/src/common/utils/global_config.ts @@ -14,14 +14,10 @@ * limitations under the License. */ -export interface Schema { - MODE: "DEV"|"PROD"; - REMOTE_TOOL_URL: "http://localhost:8081"|"https://android-build.googleplex.com/builds/bug_tool"; -} +export type Schema = Omit; -export class GlobalConfig implements Schema { - readonly MODE = "PROD" as const; - readonly REMOTE_TOOL_URL = "https://android-build.googleplex.com/builds/bug_tool" as const; +class GlobalConfig { + readonly MODE: "DEV"|"PROD" = "PROD" as const; set(config: Schema) { Object.assign(this, config); diff --git a/tools/winscope-ng/src/cross_tool/cross_tool_protocol.ts b/tools/winscope-ng/src/cross_tool/cross_tool_protocol.ts index c404f5733..cd08662ac 100644 --- a/tools/winscope-ng/src/cross_tool/cross_tool_protocol.ts +++ b/tools/winscope-ng/src/cross_tool/cross_tool_protocol.ts @@ -15,6 +15,7 @@ */ import {Message, MessageBugReport, MessagePong, MessageTimestamp, MessageType} from "./messages"; +import {OriginAllowList} from "./origin_allow_list"; import { CrossToolProtocolDependencyInversion, OnBugreportReceived, @@ -23,8 +24,15 @@ import {RealTimestamp} from "common/trace/timestamp"; import {FunctionUtils} from "common/utils/function_utils"; import {globalConfig} from "common/utils/global_config"; -class CrossToolProtocol implements CrossToolProtocolDependencyInversion { - private remoteToolWindow?: Window; +class RemoteTool { + constructor( + public readonly window: Window, + public readonly origin: string) { + } +} + +export class CrossToolProtocol implements CrossToolProtocolDependencyInversion { + private remoteTool?: RemoteTool; private onBugreportReceived: OnBugreportReceived = FunctionUtils.DO_NOTHING_ASYNC; private onTimestampReceived: OnTimestampReceived = FunctionUtils.DO_NOTHING_ASYNC; @@ -43,34 +51,36 @@ class CrossToolProtocol implements CrossToolProtocolDependencyInversion { } sendTimestamp(timestamp: RealTimestamp) { - if (!this.remoteToolWindow) { + if (!this.remoteTool) { return; } const message = new MessageTimestamp(timestamp.getValueNs()); - this.remoteToolWindow.postMessage(message, globalConfig.REMOTE_TOOL_URL); + this.remoteTool.window.postMessage(message, this.remoteTool.origin); console.log("Cross-tool protocol sent timestamp message:", message); } private async onMessageReceived(event: MessageEvent) { - if (event.origin !== globalConfig.REMOTE_TOOL_URL) { - console.log("Cross-tool protocol ignoring message from unexpected origin.", + if (!OriginAllowList.isAllowed(event.origin)) { + console.log("Cross-tool protocol ignoring message from non-allowed origin.", "Origin:", event.origin, "Message:", event.data); return; } - this.remoteToolWindow = event.source as Window; - const message = event.data as Message; if (!message.type) { console.log("Cross-tool protocol received invalid message:", message); return; } + if (!this.remoteTool) { + this.remoteTool = new RemoteTool(event.source as Window, event.origin); + } + switch(message.type) { case MessageType.PING: console.log("Cross-tool protocol received ping message:", message); - (event.source as Window).postMessage(new MessagePong(), globalConfig.REMOTE_TOOL_URL); + (event.source as Window).postMessage(new MessagePong(), event.origin); break; case MessageType.PONG: console.log("Cross-tool protocol received unexpected pong message:", message); @@ -104,5 +114,3 @@ class CrossToolProtocol implements CrossToolProtocolDependencyInversion { await this.onTimestampReceived(timestamp); } } - -export {CrossToolProtocol}; diff --git a/tools/winscope-ng/src/cross_tool/origin_allow_list.spec.ts b/tools/winscope-ng/src/cross_tool/origin_allow_list.spec.ts new file mode 100644 index 000000000..97432d662 --- /dev/null +++ b/tools/winscope-ng/src/cross_tool/origin_allow_list.spec.ts @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {OriginAllowList} from "./origin_allow_list"; + +describe("OriginAllowList", () => { + describe("dev mode", () => { + const mode = "DEV" as const; + + it("allows localhost", () => { + expect(OriginAllowList.isAllowed("http://localhost:8081", mode)).toBeTrue(); + expect(OriginAllowList.isAllowed("https://localhost:8081", mode)).toBeTrue(); + }); + }); + + describe("prod mode", () => { + const mode = "PROD" as const; + + it("allows google.com", () => { + expect(OriginAllowList.isAllowed("https://google.com", mode)).toBeTrue(); + expect(OriginAllowList.isAllowed("https://subdomain.google.com", mode)).toBeTrue(); + }); + + it("denies pseudo google.com", () => { + expect(OriginAllowList.isAllowed("https://evilgoogle.com", mode)).toBeFalse(); + expect(OriginAllowList.isAllowed("https://evil.com/google.com", mode)).toBeFalse(); + }); + + it("allows googleplex.com", () => { + expect(OriginAllowList.isAllowed("https://googleplex.com", mode)).toBeTrue(); + expect(OriginAllowList.isAllowed("https://subdomain.googleplex.com", mode)) + .toBeTrue(); + }); + + it("denies pseudo googleplex.com", () => { + expect(OriginAllowList.isAllowed("https://evilgoogleplex.com", mode)).toBeFalse(); + expect(OriginAllowList.isAllowed("https://evil.com/subdomain.googleplex.com", mode)) + .toBeFalse(); + }); + }); +}); diff --git a/tools/winscope-ng/src/cross_tool/origin_allow_list.ts b/tools/winscope-ng/src/cross_tool/origin_allow_list.ts new file mode 100644 index 000000000..f58cbd536 --- /dev/null +++ b/tools/winscope-ng/src/cross_tool/origin_allow_list.ts @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {globalConfig} from "common/utils/global_config"; + +export class OriginAllowList { + private static readonly ALLOW_LIST_PROD = [ + new RegExp("^https://([^\\/]*\\.)*googleplex\\.com$"), + new RegExp("^https://([^\\/]*\\.)*google\\.com$"), + ]; + + private static readonly ALLOW_LIST_DEV = [ + ...OriginAllowList.ALLOW_LIST_PROD, + new RegExp("^(http|https)://localhost:8081$"), // remote tool mock + ]; + + static isAllowed(originUrl: string, mode = globalConfig.MODE): boolean { + const list = OriginAllowList.getList(mode); + + for (const regex of list) { + if (regex.test(originUrl)) { + return true; + } + } + + return false; + } + + private static getList(mode: typeof globalConfig.MODE): RegExp[] { + switch(mode) { + case "DEV": + return OriginAllowList.ALLOW_LIST_DEV; + case "PROD": + return OriginAllowList.ALLOW_LIST_PROD; + default: + throw new Error(`Unhandled mode: ${globalConfig.MODE}`); + } + } +} diff --git a/tools/winscope-ng/src/main.dev.ts b/tools/winscope-ng/src/main.dev.ts index b42bfe79c..d942cfa72 100644 --- a/tools/winscope-ng/src/main.dev.ts +++ b/tools/winscope-ng/src/main.dev.ts @@ -19,7 +19,6 @@ import {globalConfig} from "common/utils/global_config"; globalConfig.set({ MODE: "DEV", - REMOTE_TOOL_URL: "http://localhost:8081" }); platformBrowserDynamic().bootstrapModule(AppModule)