Skip to content

Commit

Permalink
[1.3.2] Restrict chromium requests (#429)
Browse files Browse the repository at this point in the history
Signed-off-by: Joshua Li <joshuali925@gmail.com>
Signed-off-by: David Cui <davidcui@amazon.com>
Co-authored-by: David Cui <53581635+davidcui1225@users.noreply.github.com>
  • Loading branch information
joshuali925 and davidcui1225 authored Aug 18, 2022
1 parent 5b7c1e9 commit a60c08c
Show file tree
Hide file tree
Showing 13 changed files with 184 additions and 54 deletions.
20 changes: 10 additions & 10 deletions .github/workflows/dashboards-reports-test-and-build-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,21 @@ jobs:
with:
repository: opensearch-project/Opensearch-Dashboards
ref: ${{ env.OPENSEARCH_VERSION }}
path: dashboards-reports/OpenSearch-Dashboards
path: OpenSearch-Dashboards

- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: "10.24.1"

- name: Move Dashboards Reports to Plugins Dir
run: mv dashboards-reports OpenSearch-Dashboards/plugins/${{ env.PLUGIN_NAME }}
run: mv dashboards-reports ../OpenSearch-Dashboards/plugins/${{ env.PLUGIN_NAME }}

- name: Add Chromium Binary to Reporting for Testing
run: |
sudo apt update
sudo apt install -y libnss3-dev fonts-liberation libfontconfig1
cd OpenSearch-Dashboards/plugins/${{ env.PLUGIN_NAME }}
cd ../OpenSearch-Dashboards/plugins/${{ env.PLUGIN_NAME }}
wget https://github.com/opendistro-for-elasticsearch/kibana-reports/releases/download/chromium-1.12.0.0/chromium-linux-x64.zip
unzip chromium-linux-x64.zip
rm chromium-linux-x64.zip
Expand All @@ -44,25 +44,25 @@ jobs:
with:
timeout_minutes: 30
max_attempts: 3
command: cd OpenSearch-Dashboards/plugins/${{ env.PLUGIN_NAME }}; yarn osd bootstrap
command: cd ../OpenSearch-Dashboards/plugins/${{ env.PLUGIN_NAME }}; yarn osd bootstrap

- name: Test
uses: nick-invision/retry@v1
with:
timeout_minutes: 30
max_attempts: 3
command: cd OpenSearch-Dashboards/plugins/${{ env.PLUGIN_NAME }}; yarn test --coverage
command: cd ../OpenSearch-Dashboards/plugins/${{ env.PLUGIN_NAME }}; yarn test --coverage

- name: Upload coverage
uses: codecov/codecov-action@v1
with:
flags: dashboards-reports
directory: OpenSearch-Dashboards/plugins/
directory: ../OpenSearch-Dashboards/plugins/
token: ${{ secrets.CODECOV_TOKEN }}

- name: Build Artifact
run: |
cd OpenSearch-Dashboards/plugins/${{ env.PLUGIN_NAME }}
cd ../OpenSearch-Dashboards/plugins/${{ env.PLUGIN_NAME }}
yarn build
cd build
Expand Down Expand Up @@ -96,16 +96,16 @@ jobs:
uses: actions/upload-artifact@v1
with:
name: dashboards-reports-linux-x64
path: OpenSearch-Dashboards/plugins/${{ env.PLUGIN_NAME }}/build/${{ env.ARTIFACT_NAME }}-${{ env.OPENSEARCH_PLUGIN_VERSION }}-linux-x64.zip
path: ../OpenSearch-Dashboards/plugins/${{ env.PLUGIN_NAME }}/build/${{ env.ARTIFACT_NAME }}-${{ env.OPENSEARCH_PLUGIN_VERSION }}-linux-x64.zip

- name: Upload Artifact For Linux arm64
uses: actions/upload-artifact@v1
with:
name: dashboards-reports-linux-arm64
path: OpenSearch-Dashboards/plugins/${{ env.PLUGIN_NAME }}/build/${{ env.ARTIFACT_NAME }}-${{ env.OPENSEARCH_PLUGIN_VERSION }}-linux-arm64.zip
path: ../OpenSearch-Dashboards/plugins/${{ env.PLUGIN_NAME }}/build/${{ env.ARTIFACT_NAME }}-${{ env.OPENSEARCH_PLUGIN_VERSION }}-linux-arm64.zip

- name: Upload Artifact For Windows
uses: actions/upload-artifact@v1
with:
name: dashboards-reports-windows-x64
path: OpenSearch-Dashboards/plugins/${{ env.PLUGIN_NAME }}/build/${{ env.ARTIFACT_NAME }}-${{ env.OPENSEARCH_PLUGIN_VERSION }}-windows-x64.zip
path: ../OpenSearch-Dashboards/plugins/${{ env.PLUGIN_NAME }}/build/${{ env.ARTIFACT_NAME }}-${{ env.OPENSEARCH_PLUGIN_VERSION }}-windows-x64.zip
4 changes: 2 additions & 2 deletions dashboards-reports/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"async-mutex": "^0.2.6",
"babel-polyfill": "^6.26.0",
"cron-validator": "^1.1.1",
"dompurify": "^2.1.1",
"dompurify": "^2.3.8",
"elastic-builder": "^2.7.1",
"enzyme-adapter-react-16": "^1.15.2",
"jest-fetch-mock": "^3.0.3",
Expand All @@ -41,7 +41,7 @@
},
"devDependencies": {
"@elastic/eslint-import-resolver-kibana": "link:../../packages/osd-eslint-import-resolver-opensearch-dashboards",
"@types/dompurify": "^2.0.4",
"@types/dompurify": "^2.3.3",
"@types/enzyme-adapter-react-16": "^1.0.6",
"@types/jsdom": "^16.2.3",
"@types/puppeteer-core": "^2.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -426,12 +426,12 @@ export function ReportDefinitionDetails(props: { match?: any; setBreadcrumbs?: a
reportHeader:
reportParams.core_params.hasOwnProperty('header') &&
reportParams.core_params.header != ''
? converter.makeMarkdown(reportParams.core_params.header)
? reportParams.core_params.header
: `\u2014`,
reportFooter:
reportParams.core_params.hasOwnProperty('footer') &&
reportParams.core_params.footer != ''
? converter.makeMarkdown(reportParams.core_params.footer)
? reportParams.core_params.footer
: `\u2014`,
triggerType: triggerType,
scheduleDetails: triggerParams
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,12 +229,12 @@ export function ReportDetails(props: { match?: any; setBreadcrumbs?: any; httpCl
reportHeader:
reportParams.core_params.hasOwnProperty('header') &&
reportParams.core_params.header != ''
? converter.makeMarkdown(reportParams.core_params.header)
? reportParams.core_params.header
: `\u2014`,
reportFooter:
reportParams.core_params.hasOwnProperty('footer') &&
reportParams.core_params.footer != ''
? converter.makeMarkdown(reportParams.core_params.footer)
? reportParams.core_params.footer
: `\u2014`,
triggerType: triggerType,
scheduleType: triggerParams ? triggerParams.schedule_type : `\u2014`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,17 +250,6 @@ export function CreateReport(props: { [x: string]: any; setBreadcrumbs?: any; ht
setPreErrorData(metadata);
setComingFromError(true);
} else {
// convert header and footer to html
if ('header' in metadata.report_params.core_params) {
metadata.report_params.core_params.header = converter.makeHtml(
metadata.report_params.core_params.header
);
}
if ('footer' in metadata.report_params.core_params) {
metadata.report_params.core_params.footer = converter.makeHtml(
metadata.report_params.core_params.footer
);
}
httpClient
.post('../api/reporting/reportDefinition', {
body: JSON.stringify(metadata),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -374,13 +374,13 @@ export function ReportSettings(props: ReportSettingProps) {
if (header) {
checkboxIdSelectHeaderFooter.header = true;
if (!unmounted) {
setHeader(converter.makeMarkdown(header));
setHeader(header);
}
}
if (footer) {
checkboxIdSelectHeaderFooter.footer = true;
if (!unmounted) {
setFooter(converter.makeMarkdown(footer));
setFooter(footer);
}
}
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ describe('test create visual report', () => {
reportParams as ReportParamsSchemaType,
mockHtmlPath,
mockLogger,
mockHeader
mockHeader,
undefined,
/^(data:image|file:\/\/)/
);
expect(fileName).toContain(`${reportParams.report_name}`);
expect(fileName).toContain('.png');
Expand All @@ -71,7 +73,9 @@ describe('test create visual report', () => {
reportParams as ReportParamsSchemaType,
mockHtmlPath,
mockLogger,
mockHeader
mockHeader,
undefined,
/^(data:image|file:\/\/)/
);
expect(fileName).toContain(`${reportParams.report_name}`);
expect(fileName).toContain('.pdf');
Expand Down
30 changes: 30 additions & 0 deletions dashboards-reports/server/routes/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import { CountersType } from './types';
import Showdown from 'showdown';

export enum FORMAT {
pdf = 'pdf',
Expand Down Expand Up @@ -78,7 +79,36 @@ export const EXTRA_HEADERS = [
'x-forwarded-for',
];

export const converter = new Showdown.Converter({
tables: true,
simplifiedAutoLink: true,
strikethrough: true,
tasklists: true,
noHeaderId: true,
});

const BLOCKED_KEYWORD = 'BLOCKED_KEYWORD';
const ipv4Regex = /(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?):([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])/g
const ipv6Regex = /(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/g;
const localhostRegex = /localhost:([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])/g;
const iframeRegex = /iframe/g;

export const ALLOWED_HOSTS = /^(0|0.0.0.0|127.0.0.1|localhost|(.*\.)?(opensearch.org|aws.a2z.com))$/;

export const replaceBlockedKeywords = (htmlString: string) => {
// replace <ipv4>:<port>
htmlString = htmlString.replace(ipv4Regex, BLOCKED_KEYWORD);
// replace ipv6 addresses
htmlString = htmlString.replace(ipv6Regex, BLOCKED_KEYWORD);
// replace iframe keyword
htmlString = htmlString.replace(iframeRegex, BLOCKED_KEYWORD);
// replace localhost:<port>
htmlString = htmlString.replace(localhostRegex, BLOCKED_KEYWORD);
return htmlString;
}

export const CHROMIUM_PATH = `${__dirname}/../../../.chromium/headless_shell`;


/**
* Metric constants
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ body {
padding: 0;
}

iframe, embed, object {
display: none !important;
}

/* nice padding + matches Kibana default UI colors you could also set this to inherit if
the wrapper gets inserted inside a kibana section. I might also remove the manual text color here as well, potentially */
.reportWrapper {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import {
SELECTOR,
CHROMIUM_PATH,
SECURITY_CONSTANTS,
ALLOWED_HOSTS,
} from '../constants';
import { getFileName } from '../helpers';
import { CreateReportResultType } from '../types';
import { ReportParamsSchemaType, VisualReportSchemaType } from 'server/model';
import { converter, replaceBlockedKeywords } from '../constants';
import fs from 'fs';
import _ from 'lodash';

Expand All @@ -26,7 +28,8 @@ export const createVisualReport = async (
queryUrl: string,
logger: Logger,
extraHeaders: Headers,
timezone?: string
timezone?: string,
validRequestProtocol = /^(data:image)/,
): Promise<CreateReportResultType> => {
const {
core_params,
Expand All @@ -45,10 +48,21 @@ export const createVisualReport = async (
const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);

const reportHeader = header
? DOMPurify.sanitize(header)
let keywordFilteredHeader = header
? converter.makeHtml(header)
: DEFAULT_REPORT_HEADER;
const reportFooter = footer ? DOMPurify.sanitize(footer) : '';
let keywordFilteredFooter = footer ? converter.makeHtml(footer) : '';

keywordFilteredHeader = DOMPurify.sanitize(keywordFilteredHeader);
keywordFilteredFooter = DOMPurify.sanitize(keywordFilteredFooter);

// filter blocked keywords in header and footer
if (keywordFilteredHeader !== '') {
keywordFilteredHeader = replaceBlockedKeywords(keywordFilteredHeader);
}
if (keywordFilteredFooter !== '') {
keywordFilteredFooter = replaceBlockedKeywords(keywordFilteredFooter);
}

// set up puppeteer
const browser = await puppeteer.launch({
Expand All @@ -64,6 +78,8 @@ export const createVisualReport = async (
'--no-zygote',
'--single-process',
'--font-render-hinting=none',
'--js-flags="--jitless --no-opt"',
'--disable-features=V8OptimizeJavascript',
],
executablePath: CHROMIUM_PATH,
ignoreHTTPSErrors: true,
Expand All @@ -72,6 +88,32 @@ export const createVisualReport = async (
},
});
const page = await browser.newPage();

await page.setRequestInterception(true);
let localStorageAvailable = true;
page.on('request', (req) => {
// disallow non-allowlisted connections. urls with valid protocols do not need ALLOWED_HOSTS check
if (
!validRequestProtocol.test(req.url()) &&
!ALLOWED_HOSTS.test(new URL(req.url()).hostname)
) {
if (req.isNavigationRequest() && req.redirectChain().length > 0) {
localStorageAvailable = false;
logger.error(
'Reporting does not allow redirections to outside of localhost, aborting. URL received: ' +
req.url()
);
} else {
logger.warn(
'Disabled connection to non-allowlist domains: ' + req.url()
);
}
req.abort();
} else {
req.continue();
}
});

page.setDefaultNavigationTimeout(0);
page.setDefaultTimeout(100000); // use 100s timeout instead of default 30s
// Set extra headers that are needed
Expand All @@ -81,13 +123,25 @@ export const createVisualReport = async (
logger.info(`original queryUrl ${queryUrl}`);
await page.goto(queryUrl, { waitUntil: 'networkidle0' });
// should add to local storage after page.goto, then access the page again - browser must have an url to register local storage item on it
await page.evaluate(
/* istanbul ignore next */
(key) => {
localStorage.setItem(key, 'false');
},
SECURITY_CONSTANTS.TENANT_LOCAL_STORAGE_KEY
);
try {
await page.evaluate(
/* istanbul ignore next */
(key) => {
try {
if (
localStorageAvailable &&
typeof localStorage !== 'undefined' &&
localStorage !== null
) {
localStorage.setItem(key, 'false');
}
} catch (err) {}
},
SECURITY_CONSTANTS.TENANT_LOCAL_STORAGE_KEY
);
} catch (err) {
logger.error(err);
}
await page.goto(queryUrl, { waitUntil: 'networkidle0' });
logger.info(`page url ${page.url()}`);

Expand Down Expand Up @@ -150,9 +204,24 @@ export const createVisualReport = async (
// wait for dynamic page content to render
await waitForDynamicContent(page);

await addReportHeader(page, keywordFilteredHeader);
await addReportFooter(page, keywordFilteredFooter);
await addReportStyle(page);
await addReportHeader(page, reportHeader);
await addReportFooter(page, reportFooter);

// this causes UT to fail in github CI but works locally
try {
const numDisallowedTags = await page.evaluate(
() =>
document.getElementsByTagName('iframe').length +
document.getElementsByTagName('embed').length +
document.getElementsByTagName('object').length
);
if (numDisallowedTags > 0) {
throw Error('Reporting does not support "iframe", "embed", or "object" tags, aborting');
}
} catch (error) {
logger.error(error);
}

// create pdf or png accordingly
if (reportFormat === FORMAT.pdf) {
Expand Down
Loading

0 comments on commit a60c08c

Please sign in to comment.