Skip to content

Commit 243b1d1

Browse files
committed
Add server side private IP blocking for data source endpoints validation
Signed-off-by: Kristen Tian <tyarong@amazon.com>
1 parent e74ab2d commit 243b1d1

8 files changed

+136
-14
lines changed

.lycheeexclude

+3
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ https://opensearch.org/redirect
8888
http://www.opensearch.org/painlessDocs
8989
https://www.hostedgraphite.com/
9090
https://connectionurl.com
91+
http://169.254.169.254/latest/meta-data/
9192

9293
# External urls
9394
https://www.zeek.org/
@@ -117,3 +118,5 @@ http://www.creedthoughts.gov
117118
https://media-for-the-masses.theacademyofperformingartsandscience.org/
118119
https://yarnpkg.com/latest.msi
119120
https://forum.opensearch.org/
121+
https://facebook.github.io/jest/
122+
https://facebook.github.io/jest/docs/cli.html

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
2222
- [CVE-2023-25653] Bump node-jose to 2.2.0 ([#3445](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3445))
2323
- [CVE-2023-26486][cve-2023-26487] Bump vega from 5.22.1 to 5.23.0 ([#3533](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3533))
2424
- [CVE-2023-0842] Bump xml2js from 0.4.23 to 0.5.0 ([#3842](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3842))
25+
- [Multi DataSource] Add private IP blocking validation on server side([#3912](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3912))
2526

2627
### 📈 Features/Enhancements
2728

config/opensearch_dashboards.yml

+26-1
Original file line numberDiff line numberDiff line change
@@ -238,5 +238,30 @@
238238
#data_source.encryption.wrappingKeyNamespace: 'changeme'
239239
#data_source.encryption.wrappingKey: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
240240

241+
#data_source.endpointDeniedIPs: [
242+
# '127.0.0.0/8',
243+
# '::1/128',
244+
# '169.254.0.0/16',
245+
# 'fe80::/10',
246+
# '10.0.0.0/8',
247+
# '172.16.0.0/12',
248+
# '192.168.0.0/16',
249+
# 'fc00::/7',
250+
# '0.0.0.0/8',
251+
# '100.64.0.0/10',
252+
# '192.0.0.0/24',
253+
# '192.0.2.0/24',
254+
# '198.18.0.0/15',
255+
# '192.88.99.0/24',
256+
# '198.51.100.0/24',
257+
# '203.0.113.0/24',
258+
# '224.0.0.0/4',
259+
# '240.0.0.0/4',
260+
# '255.255.255.255/32',
261+
# '::/128',
262+
# '2001:db8::/32',
263+
# 'ff00::/8',
264+
# ]
265+
241266
# Set the value of this setting to false to hide the help menu link to the OpenSearch Dashboards user survey
242-
# opensearchDashboards.survey.url: "https://survey.opensearch.org"
267+
# opensearchDashboards.survey.url: "https://survey.opensearch.org"

src/plugins/data_source/config.ts

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export const configSchema = schema.object({
3737
enabled: schema.boolean({ defaultValue: false }),
3838
appender: fileAppenderSchema,
3939
}),
40+
endpointDeniedIPs: schema.maybe(schema.arrayOf(schema.string())),
4041
});
4142

4243
export type DataSourcePluginConfigType = TypeOf<typeof configSchema>;

src/plugins/data_source/server/plugin.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ export class DataSourcePlugin implements Plugin<DataSourcePluginSetup, DataSourc
5858

5959
const dataSourceSavedObjectsClientWrapper = new DataSourceSavedObjectsClientWrapper(
6060
cryptographyServiceSetup,
61-
this.logger.get('data-source-saved-objects-client-wrapper-factory')
61+
this.logger.get('data-source-saved-objects-client-wrapper-factory'),
62+
config.endpointDeniedIPs
6263
);
6364

6465
// Add data source saved objects client wrapper factory

src/plugins/data_source/server/saved_objects/data_source_saved_objects_client_wrapper.ts

+10-12
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,13 @@ import {
2424
UsernamePasswordTypedContent,
2525
} from '../../common/data_sources';
2626
import { EncryptionContext, CryptographyServiceSetup } from '../cryptography_service';
27+
import { isValidURL } from '../util/endpoint_validator';
2728

2829
/**
2930
* Describes the Credential Saved Objects Client Wrapper class,
3031
* which contains the factory used to create Saved Objects Client Wrapper instances
3132
*/
3233
export class DataSourceSavedObjectsClientWrapper {
33-
constructor(private cryptography: CryptographyServiceSetup, private logger: Logger) {}
34-
3534
/**
3635
* Describes the factory used to create instances of Saved Objects Client Wrappers
3736
* for data source specific operations such as credentials encryption
@@ -138,14 +137,11 @@ export class DataSourceSavedObjectsClientWrapper {
138137
};
139138
};
140139

141-
private isValidUrl(endpoint: string) {
142-
try {
143-
const url = new URL(endpoint);
144-
return Boolean(url) && (url.protocol === 'http:' || url.protocol === 'https:');
145-
} catch (e) {
146-
return false;
147-
}
148-
}
140+
constructor(
141+
private cryptography: CryptographyServiceSetup,
142+
private logger: Logger,
143+
private endpointBlockedIps?: string[]
144+
) {}
149145

150146
private async validateAndEncryptAttributes<T = unknown>(attributes: T) {
151147
this.validateAttributes(attributes);
@@ -254,8 +250,10 @@ export class DataSourceSavedObjectsClientWrapper {
254250
);
255251
}
256252

257-
if (!this.isValidUrl(endpoint)) {
258-
throw SavedObjectsErrorHelpers.createBadRequestError('"endpoint" attribute is not valid');
253+
if (!isValidURL(endpoint, this.endpointBlockedIps)) {
254+
throw SavedObjectsErrorHelpers.createBadRequestError(
255+
'"endpoint" attribute is not valid or allowed'
256+
);
259257
}
260258

261259
if (!auth) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import * as validator from './endpoint_validator';
7+
8+
describe('endpoint_validator', function () {
9+
it('Url1 that should be blocked should return false', function () {
10+
expect(validator.isValidURL('http://127.0.0.1', ['127.0.0.0/8'])).toEqual(false);
11+
});
12+
13+
it('Url2 that is invalid should return false', function () {
14+
expect(validator.isValidURL('www.test.com', [])).toEqual(false);
15+
});
16+
17+
it('Url3 that is invalid should return false', function () {
18+
expect(validator.isValidURL('ftp://www.test.com', [])).toEqual(false);
19+
});
20+
21+
it('Url4 that should be blocked should return false', function () {
22+
expect(
23+
validator.isValidURL('http://169.254.169.254/latest/meta-data/', ['169.254.0.0/16'])
24+
).toEqual(false);
25+
});
26+
27+
it('Url5 that should not be blocked should return true', function () {
28+
expect(validator.isValidURL('https://www.opensearch.org', ['127.0.0.0/8'])).toEqual(true);
29+
});
30+
31+
it('Url6 that should not be blocked should return true when null IPs', function () {
32+
expect(validator.isValidURL('https://www.opensearch.org')).toEqual(true);
33+
});
34+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import dns from 'dns-sync';
7+
import IPCIDR from 'ip-cidr';
8+
9+
export function isValidURL(endpoint: string, deniedIPs?: string[]) {
10+
// Check the format of URL, URL has be in the format as
11+
// scheme://server/path/resource otherwise an TypeError
12+
// would be thrown.
13+
let url;
14+
try {
15+
url = new URL(endpoint);
16+
} catch (err) {
17+
return false;
18+
}
19+
20+
if (!(Boolean(url) && (url.protocol === 'http:' || url.protocol === 'https:'))) {
21+
return false;
22+
}
23+
24+
const ip = getIpAddress(url);
25+
if (!ip) {
26+
return false;
27+
}
28+
29+
// IP CIDR check if a specific IP address fall in the
30+
// range of an IP address block
31+
for (const deniedIP of deniedIPs ?? []) {
32+
const cidr = new IPCIDR(deniedIP);
33+
if (cidr.contains(ip)) {
34+
return false;
35+
}
36+
}
37+
return true;
38+
}
39+
40+
/**
41+
* Resolve hostname to IP address
42+
* @param {object} urlObject
43+
* @returns {string} configuredIP
44+
* or null if it cannot be resolve
45+
* According to RFC, all IPv6 IP address needs to be in []
46+
* such as [::1].
47+
* So if we detect a IPv6 address, we remove brackets.
48+
*/
49+
function getIpAddress(urlObject: URL) {
50+
const hostname = urlObject.hostname;
51+
const configuredIP = dns.resolve(hostname);
52+
if (configuredIP) {
53+
return configuredIP;
54+
}
55+
if (hostname.startsWith('[') && hostname.endsWith(']')) {
56+
return hostname.substr(1).slice(0, -1);
57+
}
58+
return null;
59+
}

0 commit comments

Comments
 (0)