Improve cross-tool protocol's origin allow listing

Test: npm run build:all && npm run test:all
Bug: b/260994827
Change-Id: Iab8db927a55f060784a375f00a831bd10020ed0c
This commit is contained in:
Kean Mariotti
2022-12-16 13:57:23 +00:00
parent eaa86c789c
commit c987bf5abe
5 changed files with 128 additions and 19 deletions

View File

@@ -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<GlobalConfig, "set">;
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);

View File

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

View File

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

View File

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

View File

@@ -19,7 +19,6 @@ import {globalConfig} from "common/utils/global_config";
globalConfig.set({
MODE: "DEV",
REMOTE_TOOL_URL: "http://localhost:8081"
});
platformBrowserDynamic().bootstrapModule(AppModule)