Skip to content

Commit b04e784

Browse files
pfaffeDevtools-frontend LUCI CQ
authored andcommitted
Do not pass file urls to language plugins
Unless the owning extension has filesystem access enabled. Bug: none Change-Id: I3d71f635974cc348d3741fd8335cdf63fbc7fec0 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/5999725 Commit-Queue: Philip Pfaffe <[email protected]> Reviewed-by: Danil Somsikov <[email protected]>
1 parent 862c918 commit b04e784

File tree

8 files changed

+212
-25
lines changed

8 files changed

+212
-25
lines changed

front_end/models/extensions/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ ts_library("unittests") {
6464
sources = [
6565
"ExtensionServer.test.ts",
6666
"HostUrlPattern.test.ts",
67+
"LanguageExtensionEndpoint.test.ts",
6768
"RecorderPluginManager.test.ts",
6869
]
6970

front_end/models/extensions/ExtensionServer.test.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,47 @@ describeWithDevtoolsExtension('Wasm extension API', {}, context => {
787787
});
788788
});
789789

790+
class StubLanguageExtension implements Chrome.DevTools.LanguageExtensionPlugin {
791+
async addRawModule(): Promise<string[]|{missingSymbolFiles: string[]}> {
792+
return [];
793+
}
794+
async sourceLocationToRawLocation(): Promise<Chrome.DevTools.RawLocationRange[]> {
795+
return [];
796+
}
797+
async rawLocationToSourceLocation(): Promise<Chrome.DevTools.SourceLocation[]> {
798+
return [];
799+
}
800+
async getScopeInfo(): Promise<Chrome.DevTools.ScopeInfo> {
801+
throw new Error('Method not implemented.');
802+
}
803+
async listVariablesInScope(): Promise<Chrome.DevTools.Variable[]> {
804+
return [];
805+
}
806+
async removeRawModule(): Promise<void> {
807+
}
808+
async getFunctionInfo(): Promise<{frames: Array<Chrome.DevTools.FunctionInfo>, missingSymbolFiles: Array<string>}|
809+
{missingSymbolFiles: Array<string>}|{frames: Array<Chrome.DevTools.FunctionInfo>}> {
810+
return {frames: []};
811+
}
812+
async getInlinedFunctionRanges(): Promise<Chrome.DevTools.RawLocationRange[]> {
813+
return [];
814+
}
815+
async getInlinedCalleesRanges(): Promise<Chrome.DevTools.RawLocationRange[]> {
816+
return [];
817+
}
818+
async getMappedLines(): Promise<number[]|undefined> {
819+
return undefined;
820+
}
821+
async evaluate(): Promise<Chrome.DevTools.RemoteObject|Chrome.DevTools.ForeignObject|null> {
822+
return null;
823+
}
824+
async getProperties(): Promise<Chrome.DevTools.PropertyDescriptor[]> {
825+
return [];
826+
}
827+
async releaseObject(): Promise<void> {
828+
}
829+
}
830+
790831
describeWithDevtoolsExtension('Language Extension API', {}, context => {
791832
it('reports loaded resources', async () => {
792833
const target = createTarget();
@@ -814,3 +855,47 @@ describeWithDevtoolsExtension('Language Extension API', {}, context => {
814855
assert.deepEqual(resource, expectedResource);
815856
});
816857
});
858+
859+
for (const allowFileAccess of [true, false]) {
860+
describeWithDevtoolsExtension(
861+
`Language Extension API with {allowFileAccess: ${allowFileAccess}}`, {allowFileAccess}, context => {
862+
let target: SDK.Target.Target;
863+
beforeEach(() => {
864+
target = createTarget();
865+
const targetManager = target.targetManager();
866+
const workspace = Workspace.Workspace.WorkspaceImpl.instance();
867+
const resourceMapping = new Bindings.ResourceMapping.ResourceMapping(targetManager, workspace);
868+
target.setInspectedURL('http://example.com' as Platform.DevToolsPath.UrlString);
869+
const debuggerWorkspaceBinding = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance(
870+
{forceNew: true, targetManager, resourceMapping});
871+
Bindings.IgnoreListManager.IgnoreListManager.instance({forceNew: true, debuggerWorkspaceBinding});
872+
});
873+
874+
it('passes allowFileAccess to the LanguageExtensionEndpoint', async () => {
875+
const endpointSpy =
876+
sinon.spy(Extensions.LanguageExtensionEndpoint.LanguageExtensionEndpoint.prototype, 'handleScript');
877+
const plugin = new StubLanguageExtension();
878+
await context.chrome.devtools?.languageServices.registerLanguageExtensionPlugin(plugin, 'plugin', {
879+
language: Protocol.Debugger.ScriptLanguage.JavaScript,
880+
symbol_types: [Protocol.Debugger.DebugSymbolsType.SourceMap],
881+
});
882+
883+
const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel);
884+
assert.isOk(debuggerModel);
885+
debuggerModel.parsedScriptSource(
886+
'0' as Protocol.Runtime.ScriptId, 'file:///source/url' as Platform.DevToolsPath.UrlString, 0, 0, 100, 100,
887+
0, '', {}, false, 'file:///source/url.map', false, false, 200, true, null, null,
888+
Protocol.Debugger.ScriptLanguage.JavaScript, [{
889+
type: Protocol.Debugger.DebugSymbolsType.SourceMap,
890+
externalURL: 'file:///source/url.map',
891+
}],
892+
null);
893+
894+
assert.isTrue(endpointSpy.calledOnce);
895+
assert.strictEqual(
896+
(endpointSpy.thisValues[0] as Extensions.LanguageExtensionEndpoint.LanguageExtensionEndpoint)
897+
.allowFileAccess,
898+
allowFileAccess);
899+
});
900+
});
901+
}

front_end/models/extensions/ExtensionServer.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,8 +313,12 @@ export class ExtensionServer extends Common.ObjectWrapper.ObjectWrapper<EventTyp
313313
const symbol_types_array =
314314
(Array.isArray(symbol_types) && symbol_types.every(e => typeof e === 'string') ? symbol_types : []);
315315
const extensionOrigin = this.getExtensionOrigin(_shared_port);
316-
const endpoint =
317-
new LanguageExtensionEndpoint(extensionOrigin, pluginName, {language, symbol_types: symbol_types_array}, port);
316+
const registration = this.registeredExtensions.get(extensionOrigin);
317+
if (!registration) {
318+
throw new Error('Received a message from an unregistered extension');
319+
}
320+
const endpoint = new LanguageExtensionEndpoint(
321+
registration.allowFileAccess, extensionOrigin, pluginName, {language, symbol_types: symbol_types_array}, port);
318322
pluginManager.addPlugin(endpoint);
319323
return this.status.OK();
320324
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright 2024 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import type * as Platform from '../../core/platform/platform.js';
6+
import * as SDK from '../../core/sdk/sdk.js';
7+
import * as Protocol from '../../generated/protocol.js';
8+
9+
import * as Extensions from './extensions.js';
10+
11+
for (const allowFileAccess of [true, false]) {
12+
describe(`LanguageExtensionEndpoint ${allowFileAccess ? 'with' : 'without'} file access`, () => {
13+
let endpoint: Extensions.LanguageExtensionEndpoint.LanguageExtensionEndpoint;
14+
beforeEach(() => {
15+
const channel = new MessageChannel();
16+
endpoint = new Extensions.LanguageExtensionEndpoint.LanguageExtensionEndpoint(
17+
allowFileAccess, '', '', {language: 'lang', symbol_types: [Protocol.Debugger.DebugSymbolsType.SourceMap]},
18+
channel.port1);
19+
});
20+
21+
it('canAccessURL respects allowFileAccess correctly', () => {
22+
assert.isTrue(endpoint.canAccessURL('http://example.com'));
23+
assert.strictEqual(endpoint.canAccessURL('file:///file'), allowFileAccess);
24+
});
25+
26+
it('handleScript respects allowFileAccess correctly', () => {
27+
const script = sinon.createStubInstance(SDK.Script.Script);
28+
script.debugSymbols = {type: Protocol.Debugger.DebugSymbolsType.SourceMap};
29+
script.scriptLanguage.returns('lang');
30+
31+
script.contentURL.returns('file:///file' as Platform.DevToolsPath.UrlString);
32+
assert.strictEqual(endpoint.handleScript(script), allowFileAccess);
33+
script.contentURL.returns('http://example.com' as Platform.DevToolsPath.UrlString);
34+
assert.isTrue(endpoint.handleScript(script));
35+
36+
script.hasSourceURL = true;
37+
script.sourceURL = 'file:///file' as Platform.DevToolsPath.UrlString;
38+
assert.strictEqual(endpoint.handleScript(script), allowFileAccess);
39+
script.sourceURL = 'http://example.com' as Platform.DevToolsPath.UrlString;
40+
assert.isTrue(endpoint.handleScript(script));
41+
42+
script.debugSymbols.externalURL = 'file:///file';
43+
assert.strictEqual(endpoint.handleScript(script), allowFileAccess);
44+
script.debugSymbols.externalURL = 'http://example.com';
45+
assert.isTrue(endpoint.handleScript(script));
46+
});
47+
48+
it('addRawModule respects allowFileAccess correctly', async () => {
49+
const endpointProxyStub = sinon.stub(Extensions.ExtensionEndpoint.ExtensionEndpoint.prototype, 'sendRequest');
50+
endpointProxyStub.resolves([]);
51+
await endpoint.addRawModule('', 'file:///file', {url: 'http://example.com'});
52+
assert.strictEqual(endpointProxyStub.calledOnce, allowFileAccess);
53+
await endpoint.addRawModule('', 'http://example.com', {url: 'file:///file'});
54+
assert.strictEqual(endpointProxyStub.calledTwice, allowFileAccess);
55+
await endpoint.addRawModule('', 'http://example.com', {url: 'http://example.com'});
56+
assert.lengthOf(endpointProxyStub.getCalls(), allowFileAccess ? 3 : 1);
57+
});
58+
});
59+
}

front_end/models/extensions/LanguageExtensionEndpoint.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,17 @@ class LanguageExtensionEndpointImpl extends ExtensionEndpoint {
3131
export class LanguageExtensionEndpoint implements Bindings.DebuggerLanguagePlugins.DebuggerLanguagePlugin {
3232
private readonly supportedScriptTypes: {
3333
language: string,
34-
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
3534
// eslint-disable-next-line @typescript-eslint/naming-convention
3635
symbol_types: Array<string>,
3736
};
38-
private endpoint: LanguageExtensionEndpointImpl;
39-
private extensionOrigin: string;
40-
name: string;
37+
private readonly endpoint: LanguageExtensionEndpointImpl;
38+
private readonly extensionOrigin: string;
39+
readonly allowFileAccess: boolean;
40+
readonly name: string;
4141

4242
constructor(
43-
extensionOrigin: string, name: string, supportedScriptTypes: {
43+
allowFileAccess: boolean, extensionOrigin: string, name: string, supportedScriptTypes: {
4444
language: string,
45-
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
4645
// eslint-disable-next-line @typescript-eslint/naming-convention
4746
symbol_types: Array<string>,
4847
},
@@ -51,9 +50,26 @@ export class LanguageExtensionEndpoint implements Bindings.DebuggerLanguagePlugi
5150
this.extensionOrigin = extensionOrigin;
5251
this.supportedScriptTypes = supportedScriptTypes;
5352
this.endpoint = new LanguageExtensionEndpointImpl(this, port);
53+
this.allowFileAccess = allowFileAccess;
54+
}
55+
56+
canAccessURL(url: string): boolean {
57+
try {
58+
return this.allowFileAccess || new URL(url).protocol !== 'file:';
59+
} catch (e) {
60+
return false;
61+
}
5462
}
5563

5664
handleScript(script: SDK.Script.Script): boolean {
65+
try {
66+
if (!this.canAccessURL(script.contentURL()) || (script.hasSourceURL && !this.canAccessURL(script.sourceURL)) ||
67+
(script.debugSymbols?.externalURL && !this.canAccessURL(script.debugSymbols.externalURL))) {
68+
return false;
69+
}
70+
} catch (e) {
71+
return false;
72+
}
5773
const language = script.scriptLanguage();
5874
return language !== null && script.debugSymbols !== null && language === this.supportedScriptTypes.language &&
5975
this.supportedScriptTypes.symbol_types.includes(script.debugSymbols.type);
@@ -71,6 +87,9 @@ export class LanguageExtensionEndpoint implements Bindings.DebuggerLanguagePlugi
7187
/** Notify the plugin about a new script
7288
*/
7389
addRawModule(rawModuleId: string, symbolsURL: string, rawModule: Chrome.DevTools.RawModule): Promise<string[]> {
90+
if (!this.canAccessURL(symbolsURL) || !this.canAccessURL(rawModule.url)) {
91+
return Promise.resolve([]);
92+
}
7493
return this.endpoint.sendRequest(
7594
PrivateAPI.LanguageExtensionPluginCommands.AddRawModule, {rawModuleId, symbolsURL, rawModule}) as
7695
Promise<string[]>;

front_end/models/extensions/extensions.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,23 @@
33
// found in the LICENSE file.
44

55
import * as ExtensionAPI from './ExtensionAPI.js';
6+
import * as ExtensionEndpoint from './ExtensionEndpoint.js';
67
import * as ExtensionPanel from './ExtensionPanel.js';
78
import * as ExtensionServer from './ExtensionServer.js';
89
import * as ExtensionView from './ExtensionView.js';
910
import * as HostUrlPattern from './HostUrlPattern.js';
11+
import * as LanguageExtensionEndpoint from './LanguageExtensionEndpoint.js';
1012
import * as RecorderExtensionEndpoint from './RecorderExtensionEndpoint.js';
1113
import * as RecorderPluginManager from './RecorderPluginManager.js';
1214

1315
export {
1416
ExtensionAPI,
17+
ExtensionEndpoint,
1518
ExtensionPanel,
1619
ExtensionServer,
1720
ExtensionView,
1821
HostUrlPattern,
22+
LanguageExtensionEndpoint,
1923
RecorderExtensionEndpoint,
2024
RecorderPluginManager,
2125
};

0 commit comments

Comments
 (0)