Skip to content

Commit 30bcc1c

Browse files
[7.10] Restrict chromium requests (#433)
* 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]>
1 parent 49b68c4 commit 30bcc1c

File tree

8 files changed

+150
-49
lines changed

8 files changed

+150
-49
lines changed

.github/workflows/kibana-reports-test-and-build-workflow.yml

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,20 @@ jobs:
1818
with:
1919
repository: elastic/kibana
2020
ref: v7.10.2
21-
path: dashboards-reports/kibana
21+
path: kibana
2222

2323
- name: Setup Node
2424
uses: actions/setup-node@v1
2525
with:
2626
node-version: "10.23.1"
2727

2828
- name: Move Kibana Reports to Plugins Dir
29-
run: mv kibana-reports kibana/plugins/${{ env.PLUGIN_NAME }}
29+
run: mv kibana-reports ../kibana/plugins/${{ env.PLUGIN_NAME }}
3030

3131
- name: Add Chromium Binary to Reporting for Testing
3232
run: |
3333
sudo apt install -y libnss3-dev fonts-liberation libfontconfig1
34-
cd kibana/plugins/${{ env.PLUGIN_NAME }}
34+
cd ../kibana/plugins/${{ env.PLUGIN_NAME }}
3535
wget https://github.com/opendistro-for-elasticsearch/kibana-reports/releases/download/chromium-1.12.0.0/chromium-linux-x64.zip
3636
unzip chromium-linux-x64.zip
3737
rm chromium-linux-x64.zip
@@ -41,25 +41,25 @@ jobs:
4141
with:
4242
timeout_minutes: 30
4343
max_attempts: 3
44-
command: cd kibana/plugins/${{ env.PLUGIN_NAME }}; yarn kbn bootstrap
44+
command: cd ../kibana/plugins/${{ env.PLUGIN_NAME }}; yarn kbn bootstrap
4545

4646
- name: Test
4747
uses: nick-invision/retry@v1
4848
with:
4949
timeout_minutes: 30
5050
max_attempts: 3
51-
command: cd kibana/plugins/${{ env.PLUGIN_NAME }}; yarn test --coverage
51+
command: cd ../kibana/plugins/${{ env.PLUGIN_NAME }}; yarn test --coverage
5252

5353
- name: Upload coverage
5454
uses: codecov/codecov-action@v1
5555
with:
5656
flags: Kibana-reports
57-
directory: kibana/plugins/
57+
directory: ../kibana/plugins/
5858
token: ${{ secrets.CODECOV_TOKEN }}
5959

6060
- name: Build Artifact
6161
run: |
62-
cd kibana/plugins/${{ env.PLUGIN_NAME }}
62+
cd ../kibana/plugins/${{ env.PLUGIN_NAME }}
6363
yarn build
6464
6565
cd build
@@ -93,16 +93,16 @@ jobs:
9393
uses: actions/upload-artifact@v1
9494
with:
9595
name: kibana-reports-linux-x64
96-
path: kibana/plugins/${{ env.PLUGIN_NAME }}/build/${{ env.PLUGIN_NAME }}-${{ env.OD_VERSION }}-linux-x64.zip
96+
path: ../kibana/plugins/${{ env.PLUGIN_NAME }}/build/${{ env.PLUGIN_NAME }}-${{ env.OD_VERSION }}-linux-x64.zip
9797

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

104104
- name: Upload Artifact For Windows
105105
uses: actions/upload-artifact@v1
106106
with:
107107
name: kibana-reports-windows-x64
108-
path: kibana/plugins/${{ env.PLUGIN_NAME }}/build/${{ env.PLUGIN_NAME }}-${{ env.OD_VERSION }}-windows-x64.zip
108+
path: ../kibana/plugins/${{ env.PLUGIN_NAME }}/build/${{ env.PLUGIN_NAME }}-${{ env.OD_VERSION }}-windows-x64.zip

kibana-reports/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"babel-polyfill": "^6.26.0",
2525
"cheerio": "0.22.0",
2626
"cron-validator": "^1.1.1",
27-
"dompurify": "^2.1.1",
27+
"dompurify": "^2.3.8",
2828
"elastic-builder": "^2.7.1",
2929
"enzyme-adapter-react-16": "^1.15.2",
3030
"jest-fetch-mock": "^3.0.3",
@@ -47,7 +47,7 @@
4747
},
4848
"devDependencies": {
4949
"@elastic/eslint-import-resolver-kibana": "link:../../packages/kbn-eslint-import-resolver-kibana",
50-
"@types/dompurify": "^2.0.4",
50+
"@types/dompurify": "^2.3.3",
5151
"@types/enzyme-adapter-react-16": "^1.0.6",
5252
"@types/jsdom": "^16.2.3",
5353
"@types/puppeteer-core": "^2.0.0",

kibana-reports/public/components/report_definitions/create/create_report_definition.tsx

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -251,17 +251,6 @@ export function CreateReport(props) {
251251
setPreErrorData(metadata);
252252
setComingFromError(true);
253253
} else {
254-
// convert header and footer to html
255-
if ('header' in metadata.report_params.core_params) {
256-
metadata.report_params.core_params.header = converter.makeHtml(
257-
metadata.report_params.core_params.header
258-
);
259-
}
260-
if ('footer' in metadata.report_params.core_params) {
261-
metadata.report_params.core_params.footer = converter.makeHtml(
262-
metadata.report_params.core_params.footer
263-
);
264-
}
265254
httpClient
266255
.post('../api/reporting/reportDefinition', {
267256
body: JSON.stringify(metadata),

kibana-reports/public/components/report_definitions/report_settings/report_settings.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -326,13 +326,13 @@ export function ReportSettings(props: ReportSettingProps) {
326326
if (header) {
327327
checkboxIdSelectHeaderFooter.header = true;
328328
if (!unmounted) {
329-
setHeader(converter.makeMarkdown(header));
329+
setHeader(header);
330330
}
331331
}
332332
if (footer) {
333333
checkboxIdSelectHeaderFooter.footer = true;
334334
if (!unmounted) {
335-
setFooter(converter.makeMarkdown(footer));
335+
setFooter(footer);
336336
}
337337
}
338338
})

kibana-reports/server/routes/utils/__tests__/visualReportHelper.test.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,10 @@ describe('test create visual report', () => {
7474
const { dataUrl, fileName } = await createVisualReport(
7575
reportParams as ReportParamsSchemaType,
7676
queryUrl,
77-
mockLogger
77+
mockLogger,
78+
undefined,
79+
undefined,
80+
/^(data:image|file:\/\/)/
7881
);
7982
expect(fileName).toContain(`${reportParams.report_name}`);
8083
expect(fileName).toContain('.png');
@@ -89,7 +92,10 @@ describe('test create visual report', () => {
8992
const { dataUrl, fileName } = await createVisualReport(
9093
reportParams as ReportParamsSchemaType,
9194
queryUrl,
92-
mockLogger
95+
mockLogger,
96+
undefined,
97+
undefined,
98+
/^(data:image|file:\/\/)/
9399
);
94100
expect(fileName).toContain(`${reportParams.report_name}`);
95101
expect(fileName).toContain('.pdf');

kibana-reports/server/routes/utils/constants.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
*/
1515

1616
import { CountersType } from './types';
17+
import Showdown from 'showdown';
1718

1819
export enum FORMAT {
1920
pdf = 'pdf',
@@ -85,7 +86,36 @@ export const SECURITY_CONSTANTS = {
8586
TENANT_LOCAL_STORAGE_KEY: 'opendistro::security::tenant::show_popup',
8687
};
8788

89+
export const converter = new Showdown.Converter({
90+
tables: true,
91+
simplifiedAutoLink: true,
92+
strikethrough: true,
93+
tasklists: true,
94+
noHeaderId: true,
95+
});
96+
97+
const BLOCKED_KEYWORD = 'BLOCKED_KEYWORD';
98+
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
99+
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;
100+
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;
101+
const iframeRegex = /iframe/g;
102+
103+
export const ALLOWED_HOSTS = /^(0|0.0.0.0|127.0.0.1|localhost|(.*\.)?(opensearch.org|aws.a2z.com))$/;
104+
105+
export const replaceBlockedKeywords = (htmlString: string) => {
106+
// replace <ipv4>:<port>
107+
htmlString = htmlString.replace(ipv4Regex, BLOCKED_KEYWORD);
108+
// replace ipv6 addresses
109+
htmlString = htmlString.replace(ipv6Regex, BLOCKED_KEYWORD);
110+
// replace iframe keyword
111+
htmlString = htmlString.replace(iframeRegex, BLOCKED_KEYWORD);
112+
// replace localhost:<port>
113+
htmlString = htmlString.replace(localhostRegex, BLOCKED_KEYWORD);
114+
return htmlString;
115+
}
116+
88117
export const CHROMIUM_PATH = `${__dirname}/../../../.chromium/headless_shell`;
118+
89119

90120
/**
91121
* Metric constants

kibana-reports/server/routes/utils/visual_report/visualReportHelper.ts

Lines changed: 90 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ import {
2424
SELECTOR,
2525
CHROMIUM_PATH,
2626
SECURITY_CONSTANTS,
27+
ALLOWED_HOSTS,
2728
} from '../constants';
2829
import { getFileName } from '../helpers';
2930
import { CreateReportResultType } from '../types';
3031
import { ReportParamsSchemaType, VisualReportSchemaType } from 'server/model';
32+
import { converter, replaceBlockedKeywords } from '../constants';
3133
import fs from 'fs';
3234
import cheerio from 'cheerio';
3335

@@ -36,7 +38,8 @@ export const createVisualReport = async (
3638
queryUrl: string,
3739
logger: Logger,
3840
cookie?: SetCookie,
39-
timezone?: string
41+
timezone?: string,
42+
validRequestProtocol = /^(data:image)/
4043
): Promise<CreateReportResultType> => {
4144
const {
4245
core_params,
@@ -55,10 +58,21 @@ export const createVisualReport = async (
5558
const window = new JSDOM('').window;
5659
const DOMPurify = createDOMPurify(window);
5760

58-
const reportHeader = header
59-
? DOMPurify.sanitize(header)
61+
let keywordFilteredHeader = header
62+
? converter.makeHtml(header)
6063
: DEFAULT_REPORT_HEADER;
61-
const reportFooter = footer ? DOMPurify.sanitize(footer) : '';
64+
let keywordFilteredFooter = footer ? converter.makeHtml(footer) : '';
65+
66+
keywordFilteredHeader = DOMPurify.sanitize(keywordFilteredHeader);
67+
keywordFilteredFooter = DOMPurify.sanitize(keywordFilteredFooter);
68+
69+
// filter blocked keywords in header and footer
70+
if (keywordFilteredHeader !== '') {
71+
keywordFilteredHeader = replaceBlockedKeywords(keywordFilteredHeader);
72+
}
73+
if (keywordFilteredFooter !== '') {
74+
keywordFilteredFooter = replaceBlockedKeywords(keywordFilteredFooter);
75+
}
6276

6377
// add waitForDynamicContent function
6478
const waitForDynamicContent = async (
@@ -95,13 +109,48 @@ export const createVisualReport = async (
95109
* TODO: temp fix to disable sandbox when launching chromium on Linux instance
96110
* https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#setting-up-chrome-linux-sandbox
97111
*/
98-
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu', '--no-zygote', '--single-process'],
112+
args: [
113+
'--no-sandbox',
114+
'--disable-setuid-sandbox',
115+
'--disable-gpu',
116+
'--no-zygote',
117+
'--single-process',
118+
'--font-render-hinting=none',
119+
'--js-flags="--jitless --no-opt"',
120+
'--disable-features=V8OptimizeJavascript',
121+
],
99122
executablePath: CHROMIUM_PATH,
100123
env: {
101124
TZ: timezone || 'UTC',
102125
},
103126
});
104127
const page = await browser.newPage();
128+
129+
await page.setRequestInterception(true);
130+
let localStorageAvailable = true;
131+
page.on('request', (req) => {
132+
// disallow non-allowlisted connections. urls with valid protocols do not need ALLOWED_HOSTS check
133+
if (
134+
!validRequestProtocol.test(req.url()) &&
135+
!ALLOWED_HOSTS.test(new URL(req.url()).hostname)
136+
) {
137+
if (req.isNavigationRequest() && req.redirectChain().length > 0) {
138+
localStorageAvailable = false;
139+
logger.error(
140+
'Reporting does not allow redirections to outside of localhost, aborting. URL received: ' +
141+
req.url()
142+
);
143+
} else {
144+
logger.warn(
145+
'Disabled connection to non-allowlist domains: ' + req.url()
146+
);
147+
}
148+
req.abort();
149+
} else {
150+
req.continue();
151+
}
152+
});
153+
105154
page.setDefaultNavigationTimeout(0);
106155
page.setDefaultTimeout(100000); // use 100s timeout instead of default 30s
107156
if (cookie) {
@@ -111,13 +160,25 @@ export const createVisualReport = async (
111160
logger.info(`original queryUrl ${queryUrl}`);
112161
await page.goto(queryUrl, { waitUntil: 'networkidle0' });
113162
// 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
114-
await page.evaluate(
115-
/* istanbul ignore next */
116-
(key) => {
117-
localStorage.setItem(key, 'false');
118-
},
119-
SECURITY_CONSTANTS.TENANT_LOCAL_STORAGE_KEY
120-
);
163+
try {
164+
await page.evaluate(
165+
/* istanbul ignore next */
166+
(key) => {
167+
try {
168+
if (
169+
localStorageAvailable &&
170+
typeof localStorage !== 'undefined' &&
171+
localStorage !== null
172+
) {
173+
localStorage.setItem(key, 'false');
174+
}
175+
} catch (err) {}
176+
},
177+
SECURITY_CONSTANTS.TENANT_LOCAL_STORAGE_KEY
178+
);
179+
} catch (err) {
180+
logger.error(err);
181+
}
121182
await page.goto(queryUrl, { waitUntil: 'networkidle0' });
122183
logger.info(`page url ${page.url()}`);
123184

@@ -177,12 +238,27 @@ export const createVisualReport = async (
177238
const screenshot = await page.screenshot({ fullPage: true });
178239

179240
const templateHtml = composeReportHtml(
180-
reportHeader,
181-
reportFooter,
241+
keywordFilteredHeader,
242+
keywordFilteredFooter,
182243
screenshot.toString('base64')
183244
);
184245
await page.setContent(templateHtml);
185246

247+
// this causes UT to fail in github CI but works locally
248+
try {
249+
const numDisallowedTags = await page.evaluate(
250+
() =>
251+
document.getElementsByTagName('iframe').length +
252+
document.getElementsByTagName('embed').length +
253+
document.getElementsByTagName('object').length
254+
);
255+
if (numDisallowedTags > 0) {
256+
throw Error('Reporting does not support "iframe", "embed", or "object" tags, aborting');
257+
}
258+
} catch (error) {
259+
logger.error(error);
260+
}
261+
186262
// create pdf or png accordingly
187263
if (reportFormat === FORMAT.pdf) {
188264
const scrollHeight = await page.evaluate(

kibana-reports/yarn.lock

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -568,10 +568,10 @@
568568
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
569569
integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
570570

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

@@ -2286,10 +2286,10 @@ domhandler@^3.0, domhandler@^3.0.0:
22862286
dependencies:
22872287
domelementtype "^2.0.1"
22882288

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

22942294
22952295
version "1.5.1"

0 commit comments

Comments
 (0)