Skip to content

Commit

Permalink
[7.10] Restrict chromium requests (opensearch-project#433)
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]>

* 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 49b68c4 commit 30bcc1c
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 49 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,20 +18,20 @@ jobs:
with:
repository: elastic/kibana
ref: v7.10.2
path: dashboards-reports/kibana
path: kibana

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

- name: Move Kibana Reports to Plugins Dir
run: mv kibana-reports kibana/plugins/${{ env.PLUGIN_NAME }}
run: 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 @@ -41,25 +41,25 @@ 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: Upload coverage
uses: codecov/codecov-action@v1
with:
flags: Kibana-reports
directory: kibana/plugins/
directory: ../kibana/plugins/
token: ${{ secrets.CODECOV_TOKEN }}

- 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 @@ -24,7 +24,7 @@
"babel-polyfill": "^6.26.0",
"cheerio": "0.22.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 @@ -47,7 +47,7 @@
},
"devDependencies": {
"@elastic/eslint-import-resolver-kibana": "link:../../packages/kbn-eslint-import-resolver-kibana",
"@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 @@ -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('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('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 @@ -85,7 +86,36 @@ export const SECURITY_CONSTANTS = {
TENANT_LOCAL_STORAGE_KEY: 'opendistro::security::tenant::show_popup',
};

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
104 changes: 90 additions & 14 deletions kibana-reports/server/routes/utils/visual_report/visualReportHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,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 cheerio from 'cheerio';

Expand All @@ -36,7 +38,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 @@ -55,10 +58,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 @@ -95,13 +109,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 All @@ -111,13 +160,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 @@ -177,12 +238,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
16 changes: 8 additions & 8 deletions kibana-reports/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -568,10 +568,10 @@
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==

"@types/dompurify@^2.0.4":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.0.4.tgz#25fce15f1f4b1bc0df0ad957040cf226416ac2d7"
integrity sha512-y6K7NyXTQvjr8hJNsAFAD8yshCsIJ0d+OYEFzULuIqWyWOKL2hRru1I+rorI5U0K4SLAROTNuSUFXPDTu278YA==
"@types/dompurify@^2.3.3":
version "2.3.3"
resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.3.3.tgz#c24c92f698f77ed9cc9d9fa7888f90cf2bfaa23f"
integrity sha512-nnVQSgRVuZ/843oAfhA25eRSNzUFcBPk/LOiw5gm8mD9/X7CNcbRkQu/OsjCewO8+VIYfPxUnXvPEVGenw14+w==
dependencies:
"@types/trusted-types" "*"

Expand Down Expand Up @@ -2286,10 +2286,10 @@ domhandler@^3.0, domhandler@^3.0.0:
dependencies:
domelementtype "^2.0.1"

dompurify@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.1.1.tgz#b5aa988676b093a9c836d8b855680a8598af25fe"
integrity sha512-NijiNVkS/OL8mdQL1hUbCD6uty/cgFpmNiuFxrmJ5YPH2cXrPKIewoixoji56rbZ6XBPmtM8GA8/sf9unlSuwg==
dompurify@^2.3.8:
version "2.3.8"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.8.tgz#224fe9ae57d7ebd9a1ae1ac18c1c1ca3f532226f"
integrity sha512-eVhaWoVibIzqdGYjwsBWodIQIaXFSB+cKDf4cfxLMsK0xiud6SE+/WCVx/Xw/UwQsa4cS3T2eITcdtmTg2UKcw==

[email protected]:
version "1.5.1"
Expand Down

0 comments on commit 30bcc1c

Please sign in to comment.