From dc3803a3cabe189eb6d727224cb8af5358d1be68 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Tue, 14 Jun 2022 15:59:55 -0700 Subject: [PATCH 1/8] Fix ci (#2) Signed-off-by: Joshua Li --- .../workflows/dashboards-reports-test-and-build-workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dashboards-reports-test-and-build-workflow.yml b/.github/workflows/dashboards-reports-test-and-build-workflow.yml index 3edea050..54eec23d 100644 --- a/.github/workflows/dashboards-reports-test-and-build-workflow.yml +++ b/.github/workflows/dashboards-reports-test-and-build-workflow.yml @@ -20,7 +20,7 @@ jobs: with: repository: opensearch-project/Opensearch-Dashboards ref: ${{ env.OPENSEARCH_VERSION }} - path: dashboards-reports/OpenSearch-Dashboards + path: private-dashboards-reports/OpenSearch-Dashboards - name: Setup Node uses: actions/setup-node@v1 From db5fd3f3476acba83697fbe0b0d6f5570c3c52e2 Mon Sep 17 00:00:00 2001 From: David Cui <53581635+davidcui1225@users.noreply.github.com> Date: Wed, 15 Jun 2022 13:31:09 -0700 Subject: [PATCH 2/8] Markdown patch fix (#1) Signed-off-by: David Cui --- dashboards-reports/package.json | 4 +-- .../report_definition_details.tsx | 4 +-- .../main/report_details/report_details.tsx | 4 +-- .../create/create_report_definition.tsx | 11 -------- .../report_settings/report_settings.tsx | 4 +-- .../server/routes/utils/constants.ts | 28 +++++++++++++++++++ .../utils/visual_report/visualReportHelper.ts | 22 +++++++++++---- dashboards-reports/yarn.lock | 16 +++++------ 8 files changed, 61 insertions(+), 32 deletions(-) diff --git a/dashboards-reports/package.json b/dashboards-reports/package.json index b158ce88..7d8ad0ff 100644 --- a/dashboards-reports/package.json +++ b/dashboards-reports/package.json @@ -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", @@ -47,7 +47,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", diff --git a/dashboards-reports/public/components/main/report_definition_details/report_definition_details.tsx b/dashboards-reports/public/components/main/report_definition_details/report_definition_details.tsx index 432a5ad3..87cca3ca 100644 --- a/dashboards-reports/public/components/main/report_definition_details/report_definition_details.tsx +++ b/dashboards-reports/public/components/main/report_definition_details/report_definition_details.tsx @@ -410,12 +410,12 @@ export function ReportDefinitionDetails(props) { 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 diff --git a/dashboards-reports/public/components/main/report_details/report_details.tsx b/dashboards-reports/public/components/main/report_details/report_details.tsx index 8b4a9af4..71ea744a 100644 --- a/dashboards-reports/public/components/main/report_details/report_details.tsx +++ b/dashboards-reports/public/components/main/report_details/report_details.tsx @@ -217,12 +217,12 @@ export function ReportDetails(props) { 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`, diff --git a/dashboards-reports/public/components/report_definitions/create/create_report_definition.tsx b/dashboards-reports/public/components/report_definitions/create/create_report_definition.tsx index 7748432c..14a3ea5a 100644 --- a/dashboards-reports/public/components/report_definitions/create/create_report_definition.tsx +++ b/dashboards-reports/public/components/report_definitions/create/create_report_definition.tsx @@ -279,17 +279,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), diff --git a/dashboards-reports/public/components/report_definitions/report_settings/report_settings.tsx b/dashboards-reports/public/components/report_definitions/report_settings/report_settings.tsx index 9521af62..e1c17d3f 100644 --- a/dashboards-reports/public/components/report_definitions/report_settings/report_settings.tsx +++ b/dashboards-reports/public/components/report_definitions/report_settings/report_settings.tsx @@ -390,13 +390,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); } } }) diff --git a/dashboards-reports/server/routes/utils/constants.ts b/dashboards-reports/server/routes/utils/constants.ts index 6556e4ac..3b514a55 100644 --- a/dashboards-reports/server/routes/utils/constants.ts +++ b/dashboards-reports/server/routes/utils/constants.ts @@ -25,6 +25,7 @@ */ import { CountersType } from './types'; +import Showdown from 'showdown'; export enum FORMAT { pdf = 'pdf', @@ -104,7 +105,34 @@ 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 replaceBlockedKeywords = (htmlString: string) => { + // replace : + 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: + htmlString = htmlString.replace(localhostRegex, BLOCKED_KEYWORD); + return htmlString; +} + export const CHROMIUM_PATH = `${__dirname}/../../../.chromium/headless_shell`; + /** * Metric constants diff --git a/dashboards-reports/server/routes/utils/visual_report/visualReportHelper.ts b/dashboards-reports/server/routes/utils/visual_report/visualReportHelper.ts index 72de49f0..d4601fbb 100644 --- a/dashboards-reports/server/routes/utils/visual_report/visualReportHelper.ts +++ b/dashboards-reports/server/routes/utils/visual_report/visualReportHelper.ts @@ -39,6 +39,7 @@ import { 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'; @@ -66,10 +67,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({ @@ -171,8 +183,8 @@ export const createVisualReport = async ( await waitForDynamicContent(page); await addReportStyle(page); - await addReportHeader(page, reportHeader); - await addReportFooter(page, reportFooter); + await addReportHeader(page, keywordFilteredHeader); + await addReportFooter(page, keywordFilteredFooter); // create pdf or png accordingly if (reportFormat === FORMAT.pdf) { diff --git a/dashboards-reports/yarn.lock b/dashboards-reports/yarn.lock index e6659b07..25560f0b 100644 --- a/dashboards-reports/yarn.lock +++ b/dashboards-reports/yarn.lock @@ -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" "*" @@ -2292,10 +2292,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== domutils@1.5.1: version "1.5.1" From 33270a4ca928aaa4c3747e8ec5af8c533cc5da75 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Tue, 2 Aug 2022 00:08:11 +0000 Subject: [PATCH 3/8] Fix regex validation, detect iframe, embed, object tags Signed-off-by: Joshua Li --- ...boards-reports-test-and-build-workflow.yml | 20 ++++++------ .../routes/utils/visual_report/style.css | 4 +++ .../utils/visual_report/visualReportHelper.ts | 12 ++++++- .../utils/__tests__/validationHelper.test.ts | 32 ++++++++++++++++++- .../server/utils/validationHelper.ts | 2 +- 5 files changed, 57 insertions(+), 13 deletions(-) diff --git a/.github/workflows/dashboards-reports-test-and-build-workflow.yml b/.github/workflows/dashboards-reports-test-and-build-workflow.yml index 54eec23d..8bef8dbf 100644 --- a/.github/workflows/dashboards-reports-test-and-build-workflow.yml +++ b/.github/workflows/dashboards-reports-test-and-build-workflow.yml @@ -20,7 +20,7 @@ jobs: with: repository: opensearch-project/Opensearch-Dashboards ref: ${{ env.OPENSEARCH_VERSION }} - path: private-dashboards-reports/OpenSearch-Dashboards + path: OpenSearch-Dashboards - name: Setup Node uses: actions/setup-node@v1 @@ -28,12 +28,12 @@ jobs: 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 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 @@ -43,25 +43,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 @@ -95,16 +95,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 diff --git a/dashboards-reports/server/routes/utils/visual_report/style.css b/dashboards-reports/server/routes/utils/visual_report/style.css index 58628427..c329e281 100644 --- a/dashboards-reports/server/routes/utils/visual_report/style.css +++ b/dashboards-reports/server/routes/utils/visual_report/style.css @@ -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 { diff --git a/dashboards-reports/server/routes/utils/visual_report/visualReportHelper.ts b/dashboards-reports/server/routes/utils/visual_report/visualReportHelper.ts index d4601fbb..72b4da5f 100644 --- a/dashboards-reports/server/routes/utils/visual_report/visualReportHelper.ts +++ b/dashboards-reports/server/routes/utils/visual_report/visualReportHelper.ts @@ -182,9 +182,19 @@ export const createVisualReport = async ( // wait for dynamic page content to render await waitForDynamicContent(page); - await addReportStyle(page); await addReportHeader(page, keywordFilteredHeader); await addReportFooter(page, keywordFilteredFooter); + await addReportStyle(page); + + 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'); + } // create pdf or png accordingly if (reportFormat === FORMAT.pdf) { diff --git a/dashboards-reports/server/utils/__tests__/validationHelper.test.ts b/dashboards-reports/server/utils/__tests__/validationHelper.test.ts index 41ce3b49..394725b7 100644 --- a/dashboards-reports/server/utils/__tests__/validationHelper.test.ts +++ b/dashboards-reports/server/utils/__tests__/validationHelper.test.ts @@ -31,7 +31,7 @@ import { REPORT_TYPE, TRIGGER_TYPE, } from '../../routes/utils/constants'; -import { validateReport, validateReportDefinition } from '../validationHelper'; +import { isValidRelativeUrl, validateReport, validateReportDefinition } from '../validationHelper'; const SAMPLE_SAVED_OBJECT_ID = '3ba638e0-b894-11e8-a6d9-e546fe2bba5f'; const createReportDefinitionInput: ReportDefinitionSchemaType = { @@ -137,7 +137,37 @@ describe('test input validation', () => { `saved object with id dashboard:${SAMPLE_SAVED_OBJECT_ID} does not exist` ); }); + + test('validation against query_url', async () => { + const urls: [string, boolean][] = [ + ['/app/dashboards#/view/7adfa750-4c81-11e8-b3d7-01146121b73d?_g=', true], + [ + '/_plugin/kibana/app/dashboards#/view/7adfa750-4c81-11e8-b3d7-01146121b73d?_g=', + true, + ], + [ + '/_dashboards/app/dashboards#/view/7adfa750-4c81-11e8-b3d7-01146121b73d?_g=', + true, + ], + [ + '/_dashboards/app/dashboards#/edit/7adfa750-4c81-11e8-b3d7-01146121b73d?_g=', + true, + ], + [ + '/app/notebooks-dashboards?view=output_only&security_tenant=private#/M4dlPIIB0-fJ8Bh1nLc7?security_tenant=private', + true, + ], + [ + '/_dashboards/app/visualize&security_tenant=/.%2e/.%2e/.%2e/.%2e/_dashboards?#/view/id', + false, + ], + ]; + expect(urls.map((url) => isValidRelativeUrl(url[0]))).toEqual( + urls.map((url) => url[1]) + ); + }); }); + // TODO: merge this with other mock clients used in testing, to create some mock helpers file const mockOpenSearchClient = (mockSavedObjectIds: string[]) => { const client = { diff --git a/dashboards-reports/server/utils/validationHelper.ts b/dashboards-reports/server/utils/validationHelper.ts index 763b8037..b2cd3798 100644 --- a/dashboards-reports/server/utils/validationHelper.ts +++ b/dashboards-reports/server/utils/validationHelper.ts @@ -56,7 +56,7 @@ export const regexDuration = /^(-?)P(?=\d|T\d)(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)([DW]))?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/; export const regexEmailAddress = /\S+@\S+\.\S+/; export const regexReportName = /^[\w\-\s\(\)\[\]\,\_\-+]+$/; -export const regexRelativeUrl = /^\/(_plugin\/kibana\/|_dashboards\/)?app\/(dashboards|visualize|discover|notebooks-dashboards\?view=output_only)([?&]security_tenant=.+|)#\/(view\/|edit\/)?[^\/]+$/; +export const regexRelativeUrl = /^\/(_plugin\/kibana\/|_dashboards\/)?app\/(dashboards|visualize|discover|notebooks-dashboards\?view=output_only(&security_tenant=.+)?)(\?security_tenant=.+)?#\/(view\/|edit\/)?[^\/]+$/; export const validateReport = async ( client: ILegacyScopedClusterClient, From 17d2a3f2e21fdf2c84fbbf7a506363130787e089 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Tue, 2 Aug 2022 22:05:32 +0000 Subject: [PATCH 4/8] Disallow redirection to non-localhost urls Signed-off-by: Joshua Li --- .../utils/visual_report/visualReportHelper.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/dashboards-reports/server/routes/utils/visual_report/visualReportHelper.ts b/dashboards-reports/server/routes/utils/visual_report/visualReportHelper.ts index 72b4da5f..dd261cc2 100644 --- a/dashboards-reports/server/routes/utils/visual_report/visualReportHelper.ts +++ b/dashboards-reports/server/routes/utils/visual_report/visualReportHelper.ts @@ -104,6 +104,25 @@ export const createVisualReport = async ( }, }); const page = await browser.newPage(); + + await page.setRequestInterception(true); + page.on('request', (req) => { + // disallow non-localhost redirections + if ( + req.isNavigationRequest() && + req.redirectChain().length > 0 && + !/^(0|0.0.0.0|127.0.0.1|localhost)$/.test(new URL(req.url()).hostname) + ) { + logger.error( + 'Reporting does not allow redirections to outside of localhost, aborting. URL received: ' + + 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 From 80bb27969cb7b367db52328211529b4bdef392b4 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Fri, 5 Aug 2022 01:24:09 +0000 Subject: [PATCH 5/8] Disallow connection to non-allowlisted urls Signed-off-by: Joshua Li --- .../__tests__/visualReportHelper.test.ts | 8 ++- .../server/routes/utils/constants.ts | 2 + .../utils/visual_report/visualReportHelper.ts | 53 +++++++++++++------ 3 files changed, 45 insertions(+), 18 deletions(-) diff --git a/dashboards-reports/server/routes/utils/__tests__/visualReportHelper.test.ts b/dashboards-reports/server/routes/utils/__tests__/visualReportHelper.test.ts index fa3808b7..f94aa5e3 100644 --- a/dashboards-reports/server/routes/utils/__tests__/visualReportHelper.test.ts +++ b/dashboards-reports/server/routes/utils/__tests__/visualReportHelper.test.ts @@ -76,7 +76,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'); @@ -92,7 +94,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'); diff --git a/dashboards-reports/server/routes/utils/constants.ts b/dashboards-reports/server/routes/utils/constants.ts index 3b514a55..71b7e8e5 100644 --- a/dashboards-reports/server/routes/utils/constants.ts +++ b/dashboards-reports/server/routes/utils/constants.ts @@ -119,6 +119,8 @@ const ipv6Regex = /(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:) 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 : htmlString = htmlString.replace(ipv4Regex, BLOCKED_KEYWORD); diff --git a/dashboards-reports/server/routes/utils/visual_report/visualReportHelper.ts b/dashboards-reports/server/routes/utils/visual_report/visualReportHelper.ts index dd261cc2..604dea83 100644 --- a/dashboards-reports/server/routes/utils/visual_report/visualReportHelper.ts +++ b/dashboards-reports/server/routes/utils/visual_report/visualReportHelper.ts @@ -35,6 +35,7 @@ import { SELECTOR, CHROMIUM_PATH, SECURITY_CONSTANTS, + ALLOWED_HOSTS, } from '../constants'; import { getFileName } from '../helpers'; import { CreateReportResultType } from '../types'; @@ -48,7 +49,8 @@ export const createVisualReport = async ( queryUrl: string, logger: Logger, extraHeaders: Headers, - timezone?: string + timezone?: string, + validRequestProtocol = /^(data:image)/, ): Promise => { const { core_params, @@ -106,17 +108,24 @@ export const createVisualReport = async ( const page = await browser.newPage(); await page.setRequestInterception(true); + let localStorageAvailable = true; page.on('request', (req) => { - // disallow non-localhost redirections + // disallow non-allowlisted connections. urls with valid protocols do not need ALLOWED_HOSTS check if ( - req.isNavigationRequest() && - req.redirectChain().length > 0 && - !/^(0|0.0.0.0|127.0.0.1|localhost)$/.test(new URL(req.url()).hostname) + !validRequestProtocol.test(req.url()) && + !ALLOWED_HOSTS.test(new URL(req.url()).hostname) ) { - logger.error( - 'Reporting does not allow redirections to outside of localhost, aborting. URL received: ' + - req.url() - ); + if (req.isNavigationRequest() && req.redirectChain().length > 0) { + 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() + ); + } + localStorageAvailable = true; req.abort(); } else { req.continue(); @@ -132,13 +141,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()}`); From 3f80cb573649e66882a2b23ef524aae4c3d91094 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Fri, 5 Aug 2022 01:24:17 +0000 Subject: [PATCH 6/8] Disable JIT Signed-off-by: Joshua Li --- .../server/routes/utils/visual_report/visualReportHelper.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dashboards-reports/server/routes/utils/visual_report/visualReportHelper.ts b/dashboards-reports/server/routes/utils/visual_report/visualReportHelper.ts index 604dea83..1e511644 100644 --- a/dashboards-reports/server/routes/utils/visual_report/visualReportHelper.ts +++ b/dashboards-reports/server/routes/utils/visual_report/visualReportHelper.ts @@ -99,6 +99,8 @@ export const createVisualReport = async ( '--no-zygote', '--single-process', '--font-render-hinting=none', + '--js-flags="--jitless --no-opt"', + '--disable-features=V8OptimizeJavascript', ], executablePath: CHROMIUM_PATH, env: { From 2128caa21ab46ce973d8abf4f902fd1374872a34 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Tue, 16 Aug 2022 22:54:08 +0000 Subject: [PATCH 7/8] Fix localstorage logic Signed-off-by: Joshua Li --- .../server/routes/utils/visual_report/visualReportHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashboards-reports/server/routes/utils/visual_report/visualReportHelper.ts b/dashboards-reports/server/routes/utils/visual_report/visualReportHelper.ts index 1e511644..14bb8eef 100644 --- a/dashboards-reports/server/routes/utils/visual_report/visualReportHelper.ts +++ b/dashboards-reports/server/routes/utils/visual_report/visualReportHelper.ts @@ -118,6 +118,7 @@ export const createVisualReport = async ( !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() @@ -127,7 +128,6 @@ export const createVisualReport = async ( 'Disabled connection to non-allowlist domains: ' + req.url() ); } - localStorageAvailable = true; req.abort(); } else { req.continue(); From 0190ec8e81ef76a52a4964cbb339fedb5a9ce414 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Wed, 17 Aug 2022 00:23:08 +0000 Subject: [PATCH 8/8] Try to fix CI Signed-off-by: Joshua Li --- .../utils/visual_report/visualReportHelper.ts | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/dashboards-reports/server/routes/utils/visual_report/visualReportHelper.ts b/dashboards-reports/server/routes/utils/visual_report/visualReportHelper.ts index 14bb8eef..cdf345c2 100644 --- a/dashboards-reports/server/routes/utils/visual_report/visualReportHelper.ts +++ b/dashboards-reports/server/routes/utils/visual_report/visualReportHelper.ts @@ -228,14 +228,19 @@ export const createVisualReport = async ( await addReportFooter(page, keywordFilteredFooter); await addReportStyle(page); - 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'); + // 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