Skip to content

Commit

Permalink
[7.9.1] Restrict chromium requests (#434)
Browse files Browse the repository at this point in the history
* Fix ci (#2)

Signed-off-by: Joshua Li <[email protected]>

* Markdown patch fix (#1)

Signed-off-by: David Cui <[email protected]>

* update target

Signed-off-by: Joshua Li <[email protected]>

* Detect iframe, embed, object tags

Signed-off-by: Joshua Li <[email protected]>

* Disallow redirection to non-localhost urls

Signed-off-by: Joshua Li <[email protected]>

* Disallow connection to non-allowlisted urls

Signed-off-by: Joshua Li <[email protected]>

* Disable JIT

Signed-off-by: Joshua Li <[email protected]>

* Fix localstorage logic

Signed-off-by: Joshua Li <[email protected]>

* Try to fix CI

Signed-off-by: Joshua Li <[email protected]>

Signed-off-by: Joshua Li <[email protected]>
Signed-off-by: David Cui <[email protected]>
Co-authored-by: David Cui <[email protected]>
  • Loading branch information
joshuali925 and davidcui1225 authored Aug 18, 2022
1 parent e9c1163 commit 24d8c5c
Show file tree
Hide file tree
Showing 11 changed files with 357 additions and 243 deletions.
20 changes: 10 additions & 10 deletions .github/workflows/kibana-reports-test-and-build-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
with:
repository: elastic/kibana
ref: v7.9.1
path: dashboards-reports/kibana
path: kibana

- name: Setup Node
uses: actions/setup-node@v1
Expand All @@ -27,13 +27,13 @@ jobs:

- name: Move Kibana Reports to Plugins Dir
run: |
mkdir kibana/plugins
mv kibana-reports kibana/plugins/${{ env.PLUGIN_NAME }}
mkdir -p ../kibana/plugins
mv kibana-reports ../kibana/plugins/${{ env.PLUGIN_NAME }}
- name: Add Chromium Binary to Reporting for Testing
run: |
sudo apt install -y libnss3-dev fonts-liberation libfontconfig1
cd kibana/plugins/${{ env.PLUGIN_NAME }}
cd ../kibana/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 @@ -43,14 +43,14 @@ jobs:
with:
timeout_minutes: 30
max_attempts: 3
command: cd kibana/plugins/${{ env.PLUGIN_NAME }}; yarn kbn bootstrap
command: cd ../kibana/plugins/${{ env.PLUGIN_NAME }}; yarn kbn bootstrap

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

- name: Uploads coverage
uses: codecov/codecov-action@v1
Expand All @@ -59,7 +59,7 @@ jobs:

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

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

- name: Upload Artifact For Windows
uses: actions/upload-artifact@v1
with:
name: kibana-reports-windows-x64
path: kibana/plugins/${{ env.PLUGIN_NAME }}/build/${{ env.PLUGIN_NAME }}-${{ env.OD_VERSION }}-windows-x64.zip
path: ../kibana/plugins/${{ env.PLUGIN_NAME }}/build/${{ env.PLUGIN_NAME }}-${{ env.OD_VERSION }}-windows-x64.zip
4 changes: 2 additions & 2 deletions kibana-reports/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@
"@elastic/elasticsearch": "^7.8.0",
"@elastic/eui": "^26.0.0",
"@nteract/markdown": "^4.5.1",
"@types/dompurify": "^2.0.4",
"@types/dompurify": "^2.3.3",
"@types/jsdom": "^16.2.4",
"@types/react-addons-test-utils": "^0.14.25",
"async-mutex": "^0.2.6",
"babel-polyfill": "^6.26.0",
"cheerio": "^1.0.0-rc.3",
"cron-validator": "^1.1.1",
"cypress": "^5.5.0",
"dompurify": "^2.1.1",
"dompurify": "^2.3.8",
"elastic-builder": "^2.7.1",
"enzyme-adapter-react-16": "^1.15.5",
"jest-fetch-mock": "^3.0.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,17 +251,6 @@ export function CreateReport(props) {
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 @@ -326,13 +326,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 @@ -74,7 +74,10 @@ describe.skip('test create visual report', () => {
const { dataUrl, fileName } = await createVisualReport(
reportParams as ReportParamsSchemaType,
queryUrl,
mockLogger
mockLogger,
undefined,
undefined,
/^(data:image|file:\/\/)/
);
expect(fileName).toContain(`${reportParams.report_name}`);
expect(fileName).toContain('.png');
Expand All @@ -89,7 +92,10 @@ describe.skip('test create visual report', () => {
const { dataUrl, fileName } = await createVisualReport(
reportParams as ReportParamsSchemaType,
queryUrl,
mockLogger
mockLogger,
undefined,
undefined,
/^(data:image|file:\/\/)/
);
expect(fileName).toContain(`${reportParams.report_name}`);
expect(fileName).toContain('.pdf');
Expand Down
30 changes: 30 additions & 0 deletions kibana-reports/server/routes/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
*/

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

export enum FORMAT {
pdf = 'pdf',
Expand Down Expand Up @@ -81,7 +82,36 @@ export const DEFAULT_REPORT_HEADER = '<h1>Open Distro Kibana Reports</h1>';

export const SECURITY_AUTH_COOKIE_NAME = 'security_authentication';

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 @@ -23,10 +23,12 @@ import {
FORMAT,
SELECTOR,
CHROMIUM_PATH,
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 cheerio from 'cheerio';

Expand All @@ -35,7 +37,8 @@ export const createVisualReport = async (
queryUrl: string,
logger: Logger,
cookie?: SetCookie,
timezone?: string
timezone?: string,
validRequestProtocol = /^(data:image)/
): Promise<CreateReportResultType> => {
const {
core_params,
Expand All @@ -54,10 +57,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);
}

// add waitForDynamicContent function
const waitForDynamicContent = async (
Expand Down Expand Up @@ -94,13 +108,48 @@ export const createVisualReport = async (
* TODO: temp fix to disable sandbox when launching chromium on Linux instance
* https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#setting-up-chrome-linux-sandbox
*/
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu', '--no-zygote', '--single-process'],
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-gpu',
'--no-zygote',
'--single-process',
'--font-render-hinting=none',
'--js-flags="--jitless --no-opt"',
'--disable-features=V8OptimizeJavascript',
],
executablePath: CHROMIUM_PATH,
env: {
TZ: timezone || 'UTC',
},
});
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
if (cookie) {
Expand Down Expand Up @@ -167,12 +216,27 @@ export const createVisualReport = async (
const screenshot = await page.screenshot({ fullPage: true });

const templateHtml = composeReportHtml(
reportHeader,
reportFooter,
keywordFilteredHeader,
keywordFilteredFooter,
screenshot.toString('base64')
);
await page.setContent(templateHtml);

// 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) {
const scrollHeight = await page.evaluate(
Expand Down
Loading

0 comments on commit 24d8c5c

Please sign in to comment.