diff --git a/src/plugins/data_source/server/saved_objects/data_source_saved_objects_client_wrapper.ts b/src/plugins/data_source/server/saved_objects/data_source_saved_objects_client_wrapper.ts index 6b79248d1a94..a929efc1a68f 100644 --- a/src/plugins/data_source/server/saved_objects/data_source_saved_objects_client_wrapper.ts +++ b/src/plugins/data_source/server/saved_objects/data_source_saved_objects_client_wrapper.ts @@ -24,6 +24,7 @@ import { UsernamePasswordTypedContent, } from '../../common/data_sources'; import { EncryptionContext, CryptographyServiceSetup } from '../cryptography_service'; +import { isValidURL } from '../util'; /** * Describes the Credential Saved Objects Client Wrapper class, @@ -138,15 +139,6 @@ export class DataSourceSavedObjectsClientWrapper { }; }; - private isValidUrl(endpoint: string) { - try { - const url = new URL(endpoint); - return Boolean(url) && (url.protocol === 'http:' || url.protocol === 'https:'); - } catch (e) { - return false; - } - } - private async validateAndEncryptAttributes(attributes: T) { this.validateAttributes(attributes); @@ -254,8 +246,8 @@ export class DataSourceSavedObjectsClientWrapper { ); } - if (!this.isValidUrl(endpoint)) { - throw SavedObjectsErrorHelpers.createBadRequestError('"endpoint" attribute is not valid'); + if (!isValidURL(endpoint)) { + throw SavedObjectsErrorHelpers.createBadRequestError('"endpoint" attribute is not valid or allowed'); } if (!auth) { diff --git a/src/plugins/data_source/server/util/endpoint_validator.ts b/src/plugins/data_source/server/util/endpoint_validator.ts new file mode 100644 index 000000000000..4227e505efe1 --- /dev/null +++ b/src/plugins/data_source/server/util/endpoint_validator.ts @@ -0,0 +1,84 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import dns from 'dns-sync'; +import IPCIDR from 'ip-cidr'; + +const BLOCK_LIST = [ + '127.0.0.0/8', + '::1/128', + '169.254.0.0/16', + 'fe80::/10', + '10.0.0.0/8', + '172.16.0.0/12', + '192.168.0.0/16', + 'fc00::/7', + '0.0.0.0/8', + '100.64.0.0/10', + '192.0.0.0/24', + '192.0.2.0/24', + '198.18.0.0/15', + '192.88.99.0/24', + '198.51.100.0/24', + '203.0.113.0/24', + '224.0.0.0/4', + '240.0.0.0/4', + '255.255.255.255/32', + '::/128', + '2001:db8::/32', + 'ff00::/8', +]; + +export function isValidURL(endpoint: string) { + // Check the format of URL, URL has be in the format as + // scheme://server/path/resource otherwise an TypeError + // would be thrown. + let url; + try { + url = new URL(endpoint); + } catch (err) { + return false; + } + + if (!(Boolean(url) && (url.protocol === 'http:' || url.protocol === 'https:'))) { + return false; + } + + const ip = getIpAddress(url); + if (!ip) { + return false; + } + + // IP CIDR check if a specific IP address fall in the + // range of an IP address block + for (const bl of BLOCK_LIST) { + const cidr = new IPCIDR(bl); + if (cidr.contains(ip)) { + return false; + } + } + return true; +} + +/** + * Resolve hostname to IP address + * @param {object} urlObject + * @returns {string} configuredIP + * or null if it cannot be resolve + * According to RFC, all IPv6 IP address needs to be in [] + * such as [::1]. + * So if we detect a IPv6 address, we remove brackets. + */ +function getIpAddress(urlObject: URL) { + const hostname = urlObject.hostname; + const configuredIP = dns.resolve(hostname); + if (configuredIP) { + return configuredIP; + } + if (hostname.startsWith('[') && hostname.endsWith(']')) { + return hostname.substr(1).slice(0, -1); + } + return null; +} diff --git a/src/plugins/data_source/server/util/index.ts b/src/plugins/data_source/server/util/index.ts new file mode 100644 index 000000000000..e76c825a0837 --- /dev/null +++ b/src/plugins/data_source/server/util/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { isValidURL } from './endpoint_validator';