Skip to content

Commit

Permalink
ssrf patch
Browse files Browse the repository at this point in the history
Signed-off-by: Anan Zhuang <[email protected]>
  • Loading branch information
ananzh committed Jun 1, 2021
1 parent 1856d48 commit 63598eb
Show file tree
Hide file tree
Showing 10 changed files with 286 additions and 13 deletions.
6 changes: 6 additions & 0 deletions config/opensearch_dashboards.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,9 @@
# Specifies locale to be used for all localizable strings, dates and number formats.
# Supported languages are the following: English - en , by default , Chinese - zh-CN .
#i18n.locale: "en"

# Set the allowlist to check input graphite url.
vis_type_timeline.graphiteUrls: ['https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite']

# Set the blocklist to check input graphite url.
#vis_type_timeline.blocklist: []
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@
"cypress-promise": "^1.1.0",
"deep-freeze-strict": "^1.1.1",
"del": "^5.1.0",
"dns-sync": "^0.2.1",
"elastic-apm-node": "^3.7.0",
"elasticsearch": "^16.7.0",
"execa": "^4.0.2",
Expand All @@ -169,6 +170,7 @@
"https-proxy-agent": "^5.0.0",
"inert": "^5.1.0",
"inline-style": "^2.0.0",
"ip-cidr": "^2.1.0",
"joi": "^13.5.2",
"js-yaml": "^3.14.0",
"json-stable-stringify": "^1.0.1",
Expand Down
1 change: 1 addition & 0 deletions src/plugins/vis_type_timeline/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const configSchema = schema.object(
enabled: schema.boolean({ defaultValue: true }),
ui: schema.object({ enabled: schema.boolean({ defaultValue: false }) }),
graphiteUrls: schema.maybe(schema.arrayOf(schema.string())),
blocklist: schema.maybe(schema.arrayOf(schema.string())),
},
// This option should be removed as soon as we entirely migrate config from legacy Timeline plugin.
{ unknowns: 'allow' }
Expand Down
9 changes: 9 additions & 0 deletions src/plugins/vis_type_timeline/server/lib/config_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,17 @@ import { configSchema } from '../../config';
export class ConfigManager {
private opensearchShardTimeout: number = 0;
private graphiteUrls: string[] = [];
private blockedUrls: string[] = [];

constructor(config: PluginInitializerContext['config']) {
config.create<TypeOf<typeof configSchema>>().subscribe((configUpdate) => {
this.graphiteUrls = configUpdate.graphiteUrls || [];
});

config.create<TypeOf<typeof configSchema>>().subscribe((configUpdate) => {
this.blockedUrls = configUpdate.blocklist || [];
});

config.legacy.globalConfig$.subscribe((configUpdate) => {
this.opensearchShardTimeout = configUpdate.opensearch.shardTimeout.asMilliseconds();
});
Expand All @@ -55,4 +60,8 @@ export class ConfigManager {
getGraphiteUrls() {
return this.graphiteUrls;
}

getBlockedUrls() {
return this.blockedUrls;
}
}
5 changes: 1 addition & 4 deletions src/plugins/vis_type_timeline/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,9 @@ export class Plugin {
}),
value: config.graphiteUrls && config.graphiteUrls.length ? config.graphiteUrls[0] : null,
description: i18n.translate('timeline.uiSettings.graphiteURLDescription', {
defaultMessage:
'{experimentalLabel} The <a href="https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite" target="_blank" rel="noopener">URL</a> of your graphite host',
defaultMessage: '{experimentalLabel} The URL of your graphite host',
values: { experimentalLabel: `<em>[${experimentalLabel}]</em>` },
}),
type: 'select',
options: config.graphiteUrls || [],
category: ['timeline'],
schema: schema.nullable(schema.string()),
},
Expand Down
1 change: 1 addition & 0 deletions src/plugins/vis_type_timeline/server/routes/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export function runRoute(
getFunction,
getStartServices: core.getStartServices,
allowedGraphiteUrls: configManager.getGraphiteUrls(),
blockedGraphiteUrls: configManager.getBlockedUrls(),
opensearchShardTimeout: configManager.getOpenSearchShardTimeout(),
savedObjectsClient: context.core.savedObjects.client,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export default function () {

opensearchShardTimeout: moment.duration(30000),
allowedGraphiteUrls: ['https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite'],
blockedGraphiteUrls: [],
});

tlConfig.time = {
Expand Down
101 changes: 94 additions & 7 deletions src/plugins/vis_type_timeline/server/series_functions/graphite.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,86 @@ import _ from 'lodash';
import fetch from 'node-fetch';
import moment from 'moment';
import Datasource from '../lib/classes/datasource';
import dns from 'dns-sync';
import IPCIDR from 'ip-cidr';

const MISS_CHECKLIST_MESSAGE = `Please configure on the opensearch_dashbpards.yml file.
You can always enable the default allowlist configuration.`;

const INVALID_URL_MESSAGE = `The Graphite URL provided by you is invalid.
Please update your config from OpenSearch Dashboards's Advanced Setting.`;

/**
* 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) {
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;
}

function isValidURL(configuredUrl, blockedUrls) {
// Check the format of URL, URL has be in the format as
// scheme://server/path/resource otherwise an TypeError
// would be thrown
let configuredUrlObject;
try {
configuredUrlObject = new URL(configuredUrl);
} catch (err) {
return false;
}

const ip = getIpAddress(configuredUrlObject);
if (!ip) {
return false;
}

// IPCIDR check if a specific IP address fall in the
// range of an IP address block
// @param {string} bl
// @returns {object} cidr
for (const bl of blockedUrls) {
const cidr = new IPCIDR(bl);
if (cidr.contains(ip)) {
return false;
}
}
return true;
}

/**
* Check configured url using blocklist and allowlist
* If allowlist is used, return false if allowlist does not contain configured url
* If blocklist is used, return false if blocklist contains configured url
* If both allowlist and blocklist are used, return false if allowlist does not contain or if blocklist contains configured url
* @param {Array|string} blockedUrls
* @param {Array|string} allowedUrls
* @param {string} configuredUrls
* @returns {boolean} true if the configuredUrl is valid
*/
function checkConfigUrls(blockedUrls, allowedUrls, configuredUrl) {
if (blockedUrls.length === 0) {
if (!allowedUrls.includes(configuredUrl)) return false;
} else if (allowedUrls.length === 0) {
if (!isValidURL(configuredUrl, blockedUrls)) return false;
} else {
if (!allowedUrls.includes(configuredUrl) || !isValidURL(configuredUrl, blockedUrls))
return false;
}
return true;
}

export default new Datasource('graphite', {
args: [
Expand All @@ -60,18 +140,25 @@ export default new Datasource('graphite', {
max: moment(tlConfig.time.to).format('HH:mm[_]YYYYMMDD'),
};
const allowedUrls = tlConfig.allowedGraphiteUrls;
const blockedUrls = tlConfig.blockedGraphiteUrls;
const configuredUrl = tlConfig.settings['timeline:graphite.url'];
if (!allowedUrls.includes(configuredUrl)) {
if (allowedUrls.length === 0 && blockedUrls.length === 0) {
throw new Error(
i18n.translate('timeline.help.functions.missCheckGraphiteUrl', {
defaultMessage: MISS_CHECKLIST_MESSAGE,
})
);
}

if (!checkConfigUrls(blockedUrls, allowedUrls, configuredUrl)) {
throw new Error(
i18n.translate('timeline.help.functions.notAllowedGraphiteUrl', {
defaultMessage: `This graphite URL is not configured on the opensearch_dashbpards.yml file.
Please configure your graphite server list in the opensearch_dashbpards.yml file under 'timeline.graphiteUrls' and
select one from OpenSearch Dashboards's Advanced Settings`,
i18n.translate('timeline.help.functions.invalidGraphiteUrl', {
defaultMessage: INVALID_URL_MESSAGE,
})
);
}

const URL =
const GRAPHITE_URL =
tlConfig.settings['timeline:graphite.url'] +
'/render/' +
'?format=json' +
Expand All @@ -82,7 +169,7 @@ export default new Datasource('graphite', {
'&target=' +
config.metric;

return fetch(URL)
return fetch(GRAPHITE_URL, { redirect: 'error' })
.then(function (resp) {
return resp.json();
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ const expect = require('chai').expect;

import fn from './graphite';

const MISS_CHECKLIST_MESSAGE = `Please configure on the opensearch_dashbpards.yml file.
You can always enable the default allowlist configuration. `;

const INVALID_URL_MESSAGE = `The Graphite URL provided by you is invalid.
Please update your config from OpenSearch Dashboards's Advanced Setting.`;

jest.mock('node-fetch', () => () => {
return Promise.resolve({
json: function () {
Expand Down Expand Up @@ -73,4 +79,92 @@ describe('graphite', function () {
expect(result.output.list[0].label).to.eql('__beer__');
});
});

it('should return error message if both allowlist and blocklist are disabled', function () {
return invoke(fn, [], {
settings: { 'timelion:graphite.url': 'http://127.0.0.1' },
allowedGraphiteUrls: [],
}).catch((e) => {
expect(e.message).to.eql(MISS_CHECKLIST_MESSAGE);
});
});

it('setting with matched allowlist url should return result ', function () {
return invoke(fn, [], {
settings: {
'timelion:graphite.url': 'https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite',
},
}).then((result) => {
console.log(result);
expect(result.output.list.length).to.eql(1);
});
});

it('setting with unmatched allowlist url should return error message ', function () {
return invoke(fn, [], {
settings: { 'timelion:graphite.url': 'http://127.0.0.1' },
}).catch((e) => {
expect(e.message).to.eql(INVALID_URL_MESSAGE);
});
});

it('setting with matched blocklist url should return error message', function () {
return invoke(fn, [], {
settings: { 'timelion:graphite.url': 'http://127.0.0.1' },
allowedGraphiteUrls: [],
blockedGraphiteUrls: ['127.0.0.0/8'],
}).catch((e) => {
expect(e.message).to.eql(INVALID_URL_MESSAGE);
});
});

it('setting with matched blocklist localhost should return error message', function () {
return invoke(fn, [], {
settings: { 'timelion:graphite.url': 'http://localhost' },
allowedGraphiteUrls: [],
blockedGraphiteUrls: ['127.0.0.0/8'],
}).catch((e) => {
expect(e.message).to.eql(INVALID_URL_MESSAGE);
});
});

it('setting with unmatched blocklist https url should return result', function () {
return invoke(fn, [], {
settings: { 'timelion:graphite.url': 'https://www.amazon.com' },
allowedGraphiteUrls: [],
blockedGraphiteUrls: ['127.0.0.0/8'],
}).then((result) => {
console.log(result);
expect(result.output.list.length).to.eql(1);
});
});

it('setting with unmatched blocklist ftp url should return result', function () {
return invoke(fn, [], {
settings: { 'timelion:graphite.url': 'ftp://www.amazon.com' },
allowedGraphiteUrls: [],
blockedGraphiteUrls: ['127.0.0.0/8'],
}).then((result) => {
console.log(result);
expect(result.output.list.length).to.eql(1);
});
});

it('setting with invalid url should return error message', function () {
return invoke(fn, [], {
settings: { 'timelion:graphite.url': 'www.amazon.com' },
allowedGraphiteUrls: [],
blockedGraphiteUrls: ['127.0.0.0/8'],
}).catch((e) => {
expect(e.message).to.eql(INVALID_URL_MESSAGE);
});
});

it('setting with redirection error message', function () {
return invoke(fn, [], {
settings: { 'timelion:graphite.url': 'https://amazon.com/redirect' },
}).catch((e) => {
expect(e.message).to.includes('maximum redirect reached');
});
});
});
Loading

0 comments on commit 63598eb

Please sign in to comment.