@theia/core
Version:
Theia is a cloud & desktop IDE framework implemented in TypeScript.
204 lines (168 loc) • 8.88 kB
text/typescript
// *****************************************************************************
// Copyright (C) 2026 EclipseSource GmbH and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { expect } from 'chai';
import * as sinon from 'sinon';
import { BackendRequestFacade, isUrlAllowed } from './backend-request-facade';
import { BackendApplicationConfigProvider } from '../backend-application-config-provider';
describe('isUrlAllowed', () => {
it('should allow a URL that matches an exact origin pattern', () => {
const patterns = ['https://open-vsx.org'];
expect(isUrlAllowed('https://open-vsx.org/api/extensions', patterns)).to.be.true;
});
it('should allow a URL with a path when pattern has no path', () => {
const patterns = ['https://example.com'];
expect(isUrlAllowed('https://example.com/some/deep/path?query=1', patterns)).to.be.true;
});
it('should allow a URL that exactly matches the origin', () => {
const patterns = ['https://example.com'];
expect(isUrlAllowed('https://example.com', patterns)).to.be.true;
expect(isUrlAllowed('https://example.com/', patterns)).to.be.true;
});
it('should reject a URL with a different scheme', () => {
const patterns = ['https://example.com'];
expect(isUrlAllowed('http://example.com/path', patterns)).to.be.false;
});
it('should reject a URL with a different host', () => {
const patterns = ['https://example.com'];
expect(isUrlAllowed('https://evil.com/path', patterns)).to.be.false;
});
it('should reject a URL with a different port', () => {
const patterns = ['https://example.com'];
expect(isUrlAllowed('https://example.com:8443/path', patterns)).to.be.false;
});
it('should allow a URL when port matches explicitly', () => {
const patterns = ['https://example.com:8443'];
expect(isUrlAllowed('https://example.com:8443/path', patterns)).to.be.true;
});
it('should treat default ports as equivalent', () => {
const patterns = ['https://example.com:443'];
expect(isUrlAllowed('https://example.com/path', patterns)).to.be.true;
const httpPatterns = ['http://example.com:80'];
expect(isUrlAllowed('http://example.com/path', httpPatterns)).to.be.true;
const implicitPatterns = ['http://example.com'];
expect(isUrlAllowed('http://example.com:80/path', implicitPatterns)).to.be.true;
});
it('should match wildcard subdomains', () => {
const patterns = ['https://*.example.com'];
expect(isUrlAllowed('https://foo.example.com/path', patterns)).to.be.true;
expect(isUrlAllowed('https://bar.baz.example.com/path', patterns)).to.be.true;
});
it('should not match the bare domain with a wildcard subdomain pattern', () => {
const patterns = ['https://*.example.com'];
expect(isUrlAllowed('https://example.com/path', patterns)).to.be.false;
});
it('should reject non-http(s) schemes', () => {
const patterns = ['file:///etc/passwd'];
expect(isUrlAllowed('file:///etc/passwd', patterns)).to.be.false;
});
it('should reject ftp scheme', () => {
const patterns = ['ftp://files.example.com'];
expect(isUrlAllowed('ftp://files.example.com/data', patterns)).to.be.false;
});
it('should reject a URL when target uses non-http(s) scheme even if patterns exist', () => {
const patterns = ['https://example.com'];
expect(isUrlAllowed('file:///etc/passwd', patterns)).to.be.false;
expect(isUrlAllowed('ftp://example.com/data', patterns)).to.be.false;
});
it('should reject all URLs when allowlist is empty', () => {
expect(isUrlAllowed('https://example.com', [])).to.be.false;
expect(isUrlAllowed('http://localhost:3000', [])).to.be.false;
});
it('should reject invalid URLs', () => {
const patterns = ['https://example.com'];
expect(isUrlAllowed('not a url', patterns)).to.be.false;
expect(isUrlAllowed('', patterns)).to.be.false;
});
it('should allow private IPs only when explicitly in the allowlist', () => {
expect(isUrlAllowed('http://127.0.0.1:8080/secret', [])).to.be.false;
expect(isUrlAllowed('http://127.0.0.1:8080/secret', ['https://example.com'])).to.be.false;
const patternsWithLoopback = ['http://127.0.0.1:8080'];
expect(isUrlAllowed('http://127.0.0.1:8080/secret', patternsWithLoopback)).to.be.true;
});
it('should reject cloud metadata endpoint by default', () => {
expect(isUrlAllowed('http://169.254.169.254/latest/meta-data/', [])).to.be.false;
expect(isUrlAllowed('http://169.254.169.254/latest/meta-data/', ['https://example.com'])).to.be.false;
});
it('should merge multiple patterns correctly', () => {
const patterns = [
'https://open-vsx.org',
'https://marketplace.example.com',
'https://*.cdn.example.com'
];
expect(isUrlAllowed('https://open-vsx.org/api/v2/extensions', patterns)).to.be.true;
expect(isUrlAllowed('https://marketplace.example.com/download', patterns)).to.be.true;
expect(isUrlAllowed('https://files.cdn.example.com/ext.vsix', patterns)).to.be.true;
expect(isUrlAllowed('https://evil.com/malware', patterns)).to.be.false;
});
it('should handle patterns with trailing slashes', () => {
const patterns = ['https://example.com/'];
expect(isUrlAllowed('https://example.com/path', patterns)).to.be.true;
});
it('should handle invalid patterns gracefully', () => {
const patterns = ['not a valid pattern', 'https://example.com'];
expect(isUrlAllowed('https://example.com/path', patterns)).to.be.true;
expect(isUrlAllowed('https://other.com/path', patterns)).to.be.false;
});
it('should prevent host spoofing via subdomain prefix', () => {
const patterns = ['https://example.com'];
expect(isUrlAllowed('https://example.com.evil.com/path', patterns)).to.be.false;
});
});
describe('BackendRequestFacade', () => {
describe('configure', () => {
let configProviderStub: sinon.SinonStub;
beforeEach(() => {
configProviderStub = sinon.stub(BackendApplicationConfigProvider, 'get');
});
afterEach(() => {
configProviderStub.restore();
});
it('should be a no-op when configureProxyFromPreferences is false', async () => {
configProviderStub.returns({
singleInstance: true,
frontendConnectionTimeout: 0,
configurationFolder: '.theia',
configureProxyFromPreferences: false
});
const facade = new BackendRequestFacade();
const configureStub = sinon.stub().resolves();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(facade as any).requestService = {
configure: configureStub
};
await facade.configure({ proxyUrl: 'http://evil.com', strictSSL: false });
expect(configureStub.called).to.be.false;
});
it('should pass through to the underlying request service when configureProxyFromPreferences is true', async () => {
configProviderStub.returns({
singleInstance: true,
frontendConnectionTimeout: 0,
configurationFolder: '.theia',
configureProxyFromPreferences: true
});
const facade = new BackendRequestFacade();
const configureStub = sinon.stub().resolves();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(facade as any).requestService = {
configure: configureStub
};
const config = { proxyUrl: 'http://my-proxy.com:8080', strictSSL: true, proxyAuthorization: 'Bearer token' };
await facade.configure(config);
expect(configureStub.calledOnce).to.be.true;
expect(configureStub.firstCall.args[0]).to.deep.equal(config);
});
});
});