Skip to content

Commit 2d19bcb

Browse files
pfaffeDevtools-frontend LUCI CQ
authored andcommitted
Apply runtime_blocked_hosts and runtime_allowed_hosts for extensions
Bug: 1429353 Change-Id: I2605e77f74f08e0e3619b88440641838adf34679 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/4637291 Reviewed-by: Danil Somsikov <[email protected]> Commit-Queue: Philip Pfaffe <[email protected]>
1 parent 5406f38 commit 2d19bcb

File tree

6 files changed

+146
-17
lines changed

6 files changed

+146
-17
lines changed

front_end/core/host/InspectorFrontendHostAPI.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,11 @@ export interface ExtensionDescriptor {
324324
startPage: string;
325325
name: string;
326326
exposeExperimentalAPIs: boolean;
327+
hostsPolicy?: ExtensionHostsPolicy;
328+
}
329+
export interface ExtensionHostsPolicy {
330+
runtimeAllowedHosts: string[];
331+
runtimeBlockedHosts: string[];
327332
}
328333
export interface ShowSurveyResult {
329334
surveyShown: boolean;

front_end/devtools_compatibility.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66

77
// DevToolsAPI ----------------------------------------------------------------
88

9+
/**
10+
* @typedef {{runtimeAllowedHosts: !Array<string>, runtimeBlockedHosts: !Array<string>}} ExtensionHostsPolicy
11+
*/
12+
/**
13+
* @typedef {{startPage: string, name: string, exposeExperimentalAPIs: boolean, hostsPolicy?: ExtensionHostsPolicy}} ExtensionDescriptor
14+
*/
915
const DevToolsAPIImpl = class {
1016
constructor() {
1117
/**
@@ -24,7 +30,7 @@ const DevToolsAPIImpl = class {
2430
this._pendingExtensionDescriptors = [];
2531

2632
/**
27-
* @type {?function(!ExtensionDescriptor)}
33+
* @type {?function(!ExtensionDescriptor): void}
2834
*/
2935
this._addExtensionCallback = null;
3036

front_end/models/extensions/ExtensionServer.ts

Lines changed: 73 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,15 @@
3131
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
3232
/* eslint-disable @typescript-eslint/naming-convention */
3333

34+
import {type Chrome} from '../../../extension-api/ExtensionAPI.js'; // eslint-disable-line rulesdir/es_modules_import
3435
import * as Common from '../../core/common/common.js';
3536
import * as Host from '../../core/host/host.js';
3637
import * as i18n from '../../core/i18n/i18n.js';
3738
import * as Platform from '../../core/platform/platform.js';
3839
import * as _ProtocolClient from '../../core/protocol_client/protocol_client.js'; // eslint-disable-line @typescript-eslint/no-unused-vars
3940
import * as Root from '../../core/root/root.js';
4041
import * as SDK from '../../core/sdk/sdk.js';
42+
import type * as Protocol from '../../generated/protocol.js';
4143
import * as Logs from '../../models/logs/logs.js';
4244
import * as Components from '../../ui/legacy/components/utils/utils.js';
4345
import * as UI from '../../ui/legacy/legacy.js';
@@ -46,15 +48,13 @@ import * as Bindings from '../bindings/bindings.js';
4648
import * as HAR from '../har/har.js';
4749
import type * as TextUtils from '../text_utils/text_utils.js';
4850
import * as Workspace from '../workspace/workspace.js';
49-
import type * as Protocol from '../../generated/protocol.js';
5051

52+
import {PrivateAPI} from './ExtensionAPI.js';
5153
import {ExtensionButton, ExtensionPanel, ExtensionSidebarPane} from './ExtensionPanel.js';
52-
54+
import {HostUrlPattern} from './HostUrlPattern.js';
5355
import {LanguageExtensionEndpoint} from './LanguageExtensionEndpoint.js';
5456
import {RecorderExtensionEndpoint} from './RecorderExtensionEndpoint.js';
55-
import {PrivateAPI} from './ExtensionAPI.js';
5657
import {RecorderPluginManager} from './RecorderPluginManager.js';
57-
import {type Chrome} from '../../../extension-api/ExtensionAPI.js'; // eslint-disable-line rulesdir/es_modules_import
5858

5959
const extensionOrigins: WeakMap<MessagePort, Platform.DevToolsPath.UrlString> = new WeakMap();
6060

@@ -68,6 +68,32 @@ const kAllowedOrigins = [].map(url => (new URL(url)).origin);
6868

6969
let extensionServerInstance: ExtensionServer|null;
7070

71+
export class HostsPolicy {
72+
static create(policy?: Host.InspectorFrontendHostAPI.ExtensionHostsPolicy): HostsPolicy|null {
73+
const runtimeAllowedHosts = [];
74+
const runtimeBlockedHosts = [];
75+
if (policy) {
76+
for (const pattern of policy.runtimeAllowedHosts) {
77+
const parsedPattern = HostUrlPattern.parse(pattern);
78+
if (!parsedPattern) {
79+
return null;
80+
}
81+
runtimeAllowedHosts.push(parsedPattern);
82+
}
83+
for (const pattern of policy.runtimeBlockedHosts) {
84+
const parsedPattern = HostUrlPattern.parse(pattern);
85+
if (!parsedPattern) {
86+
return null;
87+
}
88+
runtimeBlockedHosts.push(parsedPattern);
89+
}
90+
}
91+
return new HostsPolicy(runtimeAllowedHosts, runtimeBlockedHosts);
92+
}
93+
private constructor(readonly runtimeAllowedHosts: HostUrlPattern[], readonly runtimeBlockedHosts: HostUrlPattern[]) {
94+
}
95+
}
96+
7197
export class ExtensionServer extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
7298
private readonly clientObjects: Map<string, unknown>;
7399
private readonly handlers:
@@ -81,6 +107,7 @@ export class ExtensionServer extends Common.ObjectWrapper.ObjectWrapper<EventTyp
81107
private lastRequestId: number;
82108
private registeredExtensions: Map<string, {
83109
name: string,
110+
hostsPolicy: HostsPolicy,
84111
}>;
85112
private status: ExtensionStatus;
86113
private readonly sidebarPanesInternal: ExtensionSidebarPane[];
@@ -363,7 +390,9 @@ export class ExtensionServer extends Common.ObjectWrapper.ObjectWrapper<EventTyp
363390
}
364391
const message = {command: 'notify-' + type, arguments: Array.prototype.slice.call(arguments, 1)};
365392
for (const subscriber of subscribers) {
366-
subscriber.postMessage(message);
393+
if (this.extensionEnabled(subscriber)) {
394+
subscriber.postMessage(message);
395+
}
367396
}
368397
}
369398

@@ -953,7 +982,11 @@ export class ExtensionServer extends Common.ObjectWrapper.ObjectWrapper<EventTyp
953982
addExtensionForTest(extensionInfo: Host.InspectorFrontendHostAPI.ExtensionDescriptor, origin: string): boolean
954983
|undefined {
955984
const name = extensionInfo.name || `Extension ${origin}`;
956-
this.registeredExtensions.set(origin, {name});
985+
const hostsPolicy = HostsPolicy.create(extensionInfo.hostsPolicy);
986+
if (!hostsPolicy) {
987+
return false;
988+
}
989+
this.registeredExtensions.set(origin, {name, hostsPolicy});
957990
return true;
958991
}
959992

@@ -967,6 +1000,10 @@ export class ExtensionServer extends Common.ObjectWrapper.ObjectWrapper<EventTyp
9671000
if (!this.extensionsEnabled) {
9681001
return;
9691002
}
1003+
const hostsPolicy = HostsPolicy.create(extensionInfo.hostsPolicy);
1004+
if (!hostsPolicy) {
1005+
return;
1006+
}
9701007
try {
9711008
const startPageURL = new URL((startPage as string));
9721009
const extensionOrigin = startPageURL.origin;
@@ -979,7 +1016,7 @@ export class ExtensionServer extends Common.ObjectWrapper.ObjectWrapper<EventTyp
9791016
Host.InspectorFrontendHost.InspectorFrontendHostInstance.setInjectedScriptForOrigin(
9801017
extensionOrigin, injectedAPI);
9811018
const name = extensionInfo.name || `Extension ${extensionOrigin}`;
982-
this.registeredExtensions.set(extensionOrigin, {name});
1019+
this.registeredExtensions.set(extensionOrigin, {name, hostsPolicy});
9831020
}
9841021

9851022
const iframe = document.createElement('iframe');
@@ -1012,15 +1049,42 @@ export class ExtensionServer extends Common.ObjectWrapper.ObjectWrapper<EventTyp
10121049
}
10131050
};
10141051

1052+
private extensionEnabled(port: MessagePort): boolean {
1053+
if (!this.extensionsEnabled) {
1054+
return false;
1055+
}
1056+
const origin = extensionOrigins.get(port);
1057+
if (!origin) {
1058+
return false;
1059+
}
1060+
const extension = this.registeredExtensions.get(origin);
1061+
if (!extension) {
1062+
return false;
1063+
}
1064+
1065+
const inspectedURL = SDK.TargetManager.TargetManager.instance().primaryPageTarget()?.inspectedURL();
1066+
if (!inspectedURL) {
1067+
// If there aren't any blocked hosts retain the old behavior and don't worry about the inspectedURL
1068+
return extension.hostsPolicy.runtimeBlockedHosts.length === 0;
1069+
}
1070+
if (extension.hostsPolicy.runtimeBlockedHosts.some(pattern => pattern.matchesUrl(inspectedURL)) &&
1071+
!extension.hostsPolicy.runtimeAllowedHosts.some(pattern => pattern.matchesUrl(inspectedURL))) {
1072+
return false;
1073+
}
1074+
1075+
return true;
1076+
}
1077+
10151078
private async onmessage(event: MessageEvent): Promise<void> {
10161079
const message = event.data;
10171080
let result;
10181081

1082+
const port = event.currentTarget as MessagePort;
10191083
const handler = this.handlers.get(message.command);
10201084

10211085
if (!handler) {
10221086
result = this.status.E_NOTSUPPORTED(message.command);
1023-
} else if (!this.extensionsEnabled) {
1087+
} else if (!this.extensionEnabled(port)) {
10241088
result = this.status.E_FAILED('Permission denied');
10251089
} else {
10261090
result = await handler(message, event.target as MessagePort);
@@ -1217,6 +1281,7 @@ export class ExtensionServer extends Common.ObjectWrapper.ObjectWrapper<EventTyp
12171281
[]).includes(parsedURL.origin)) {
12181282
return false;
12191283
}
1284+
12201285
return true;
12211286
}
12221287

test/unittests/front_end/helpers/EnvironmentHelpers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ export async function initializeGlobalVars({reset = true} = {}) {
252252
Common.Settings.Settings.instance(
253253
{forceNew: reset, syncedStorage: storage, globalStorage: storage, localStorage: storage});
254254

255+
Root.Runtime.experiments.clearForTest();
255256
for (const experimentName of REGISTERED_EXPERIMENTS) {
256257
Root.Runtime.experiments.register(experimentName, '');
257258
}

test/unittests/front_end/models/extensions/ExtensionServer_test.ts

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,19 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import type * as Platform from '../../../../../front_end/core/platform/platform.js';
6+
import * as SDK from '../../../../../front_end/core/sdk/sdk.js';
57
import * as Extensions from '../../../../../front_end/models/extensions/extensions.js';
68
import * as UI from '../../../../../front_end/ui/legacy/legacy.js';
79

8-
import type * as Platform from '../../../../../front_end/core/platform/platform.js';
9-
1010
const {assert} = chai;
1111

12-
import {describeWithDummyExtension} from './helpers.js';
12+
import {describeWithDevtoolsExtension} from './helpers.js';
1313
import {type Chrome} from '../../../../../extension-api/ExtensionAPI.js';
14+
import {createTarget} from '../../helpers/EnvironmentHelpers.js';
15+
import {describeWithMockConnection} from '../../helpers/MockConnection.js';
1416

15-
describeWithDummyExtension('Extensions', context => {
17+
describeWithDevtoolsExtension('Extensions', {}, context => {
1618
it('can register a recorder extension for export', async () => {
1719
class RecorderPlugin {
1820
async stringify(recording: object) {
@@ -197,6 +199,50 @@ describeWithDummyExtension('Extensions', context => {
197199
});
198200
});
199201

202+
const hostsPolicy = {
203+
runtimeAllowedHosts: ['http://example.com'],
204+
runtimeBlockedHosts: ['http://example.com', 'http://web.dev'],
205+
};
206+
207+
describeWithMockConnection('Extensions', () => {
208+
describeWithDevtoolsExtension('Runtime hosts policy', {hostsPolicy}, context => {
209+
it('blocks API calls on blocked hosts', async () => {
210+
const target = createTarget({type: SDK.Target.Type.Frame});
211+
212+
{
213+
const result = await new Promise<object>(cb => context.chrome.devtools?.network.getHAR(cb));
214+
assert.strictEqual('isError' in result && result.isError, true);
215+
}
216+
217+
target.setInspectedURL('http://web.dev' as Platform.DevToolsPath.UrlString);
218+
{
219+
const result = await new Promise<object>(cb => context.chrome.devtools?.network.getHAR(cb));
220+
assert.strictEqual('isError' in result && result.isError, true);
221+
}
222+
});
223+
224+
it('allows API calls on allowlisted hosts', async () => {
225+
const target = createTarget({type: SDK.Target.Type.Frame});
226+
target.setInspectedURL('http://example.com' as Platform.DevToolsPath.UrlString);
227+
{
228+
const result = await new Promise<object>(cb => context.chrome.devtools?.network.getHAR(cb));
229+
// eslint-disable-next-line rulesdir/compare_arrays_with_assert_deepequal
230+
assert.doesNotHaveAnyKeys(result, ['isError']);
231+
}
232+
});
233+
234+
it('allows API calls on non-blocked hosts', async () => {
235+
const target = createTarget({type: SDK.Target.Type.Frame});
236+
target.setInspectedURL('http://example.com2' as Platform.DevToolsPath.UrlString);
237+
{
238+
const result = await new Promise<object>(cb => context.chrome.devtools?.network.getHAR(cb));
239+
// eslint-disable-next-line rulesdir/compare_arrays_with_assert_deepequal
240+
assert.doesNotHaveAnyKeys(result, ['isError']);
241+
}
242+
});
243+
});
244+
});
245+
200246
describe('ExtensionServer', () => {
201247
it('can correctly expand resource paths', async () => {
202248
// Ideally this would be a chrome-extension://, but that doesn't work with URL in chrome headless.

test/unittests/front_end/models/extensions/helpers.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import * as Extensions from '../../../../../front_end/models/extensions/extensions.js';
65
import {type Chrome} from '../../../../../extension-api/ExtensionAPI.js';
6+
import type * as Host from '../../../../../front_end/core/host/host.js';
7+
import * as Extensions from '../../../../../front_end/models/extensions/extensions.js';
78
import {describeWithEnvironment} from '../../helpers/EnvironmentHelpers.js';
89

910
interface ExtensionContext {
1011
chrome: Partial<Chrome.DevTools.Chrome>;
1112
}
1213

13-
export function describeWithDummyExtension(title: string, fn: (this: Mocha.Suite, context: ExtensionContext) => void) {
14+
export function describeWithDevtoolsExtension(
15+
title: string, extension: Partial<Host.InspectorFrontendHostAPI.ExtensionDescriptor>,
16+
fn: (this: Mocha.Suite, context: ExtensionContext) => void) {
1417
const context: ExtensionContext = {
1518
chrome: {},
1619
};
@@ -21,6 +24,7 @@ export function describeWithDummyExtension(title: string, fn: (this: Mocha.Suite
2124
startPage: 'blank.html',
2225
name: 'TestExtension',
2326
exposeExperimentalAPIs: true,
27+
...extension,
2428
};
2529
server.addExtensionForTest(extensionDescriptor, window.location.origin);
2630
const chrome: Partial<Chrome.DevTools.Chrome> = {};
@@ -46,9 +50,11 @@ export function describeWithDummyExtension(title: string, fn: (this: Mocha.Suite
4650
});
4751
}
4852

49-
describeWithDummyExtension.only = function(title: string, fn: (this: Mocha.Suite, context: ExtensionContext) => void) {
53+
describeWithDevtoolsExtension.only = function(
54+
title: string, extension: Partial<Host.InspectorFrontendHostAPI.ExtensionDescriptor>,
55+
fn: (this: Mocha.Suite, context: ExtensionContext) => void) {
5056
// eslint-disable-next-line rulesdir/no_only
5157
return describe.only('.only', function() {
52-
return describeWithDummyExtension(title, fn);
58+
return describeWithDevtoolsExtension(title, extension, fn);
5359
});
5460
};

0 commit comments

Comments
 (0)