Skip to content

Commit 2ab046e

Browse files
author
Joel Griffith
committed
Adds a new xpack.reporting.csv.escapeFormulaValues boolean to auto-escape potentially bad cells
1 parent e44cf28 commit 2ab046e

File tree

10 files changed

+94
-6
lines changed

10 files changed

+94
-6
lines changed

x-pack/legacy/plugins/reporting/common/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const API_GENERATE_IMMEDIATE = `${API_BASE_URL_V1}/generate/immediate/csv
2020
export const CONTENT_TYPE_CSV = 'text/csv';
2121
export const CSV_REPORTING_ACTION = 'downloadCsvReport';
2222
export const CSV_BOM_CHARS = '\ufeff';
23+
export const CSV_FORMULA_CHARS = ['=', '+', '-', '@'];
2324

2425
export const WHITELISTED_JOB_CONTENT_TYPES = [
2526
'application/json',

x-pack/legacy/plugins/reporting/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ export async function config(Joi: any) {
138138
useByteOrderMarkEncoding: Joi.boolean().default(false),
139139
checkForFormulas: Joi.boolean().default(true),
140140
enablePanelActionDownload: Joi.boolean().default(true),
141+
escapeFormulaValues: Joi.boolean().default(false),
141142
maxSizeBytes: Joi.number()
142143
.integer()
143144
.default(1024 * 1024 * 10), // bytes in a kB * kB in a mB * 10

x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,50 @@ describe('CSV Execute Job', function() {
446446
});
447447
});
448448

449+
describe('Formula values', () => {
450+
it('escapes values with formulas', async () => {
451+
configGetStub.withArgs('csv', 'escapeFormulaValues').returns(true);
452+
callAsCurrentUserStub.onFirstCall().returns({
453+
hits: {
454+
hits: [{ _source: { one: `=cmd|' /C calc'!A0`, two: 'bar' } }],
455+
},
456+
_scroll_id: 'scrollId',
457+
});
458+
459+
const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger);
460+
const jobParams = getJobDocPayload({
461+
headers: encryptedHeaders,
462+
fields: ['one', 'two'],
463+
conflictedTypesFields: [],
464+
searchRequest: { index: null, body: null },
465+
});
466+
const { content } = await executeJob('job123', jobParams, cancellationToken);
467+
468+
expect(content).toEqual("one,two\n\"'=cmd|' /C calc'!A0\",bar\n");
469+
});
470+
471+
it('does not escapes values with formulas', async () => {
472+
configGetStub.withArgs('csv', 'escapeFormulaValues').returns(false);
473+
callAsCurrentUserStub.onFirstCall().returns({
474+
hits: {
475+
hits: [{ _source: { one: `=cmd|' /C calc'!A0`, two: 'bar' } }],
476+
},
477+
_scroll_id: 'scrollId',
478+
});
479+
480+
const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger);
481+
const jobParams = getJobDocPayload({
482+
headers: encryptedHeaders,
483+
fields: ['one', 'two'],
484+
conflictedTypesFields: [],
485+
searchRequest: { index: null, body: null },
486+
});
487+
const { content } = await executeJob('job123', jobParams, cancellationToken);
488+
489+
expect(content).toEqual('one,two\n"=cmd|\' /C calc\'!A0",bar\n');
490+
});
491+
});
492+
449493
describe('Elasticsearch call errors', function() {
450494
it('should reject Promise if search call errors out', async function() {
451495
callAsCurrentUserStub.rejects(new Error());

x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ export const executeJobFactory: ExecuteJobFactory<ESQueueWorkerExecuteFn<
136136
checkForFormulas: config.get('csv', 'checkForFormulas'),
137137
maxSizeBytes: config.get('csv', 'maxSizeBytes'),
138138
scroll: config.get('csv', 'scroll'),
139+
escapeFormulaValues: config.get('csv', 'escapeFormulaValues'),
139140
},
140141
});
141142

x-pack/legacy/plugins/reporting/export_types/csv/server/lib/escape_value.test.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ describe('escapeValue', function() {
1111
describe('quoteValues is true', function() {
1212
let escapeValue: (val: string) => string;
1313
beforeEach(function() {
14-
escapeValue = createEscapeValue(true);
14+
escapeValue = createEscapeValue(true, false);
1515
});
1616

1717
it('should escape value with spaces', function() {
@@ -46,12 +46,42 @@ describe('escapeValue', function() {
4646
describe('quoteValues is false', function() {
4747
let escapeValue: (val: string) => string;
4848
beforeEach(function() {
49-
escapeValue = createEscapeValue(false);
49+
escapeValue = createEscapeValue(false, false);
5050
});
5151

5252
it('should return the value unescaped', function() {
5353
const value = '"foo, bar & baz-qux"';
5454
expect(escapeValue(value)).to.be(value);
5555
});
5656
});
57+
58+
describe('escapeValues', () => {
59+
describe('when true', () => {
60+
let escapeValue: (val: string) => string;
61+
beforeEach(function() {
62+
escapeValue = createEscapeValue(true, true);
63+
});
64+
65+
['@', '+', '-', '='].forEach(badChar => {
66+
it(`should escape ${badChar} injection values`, function() {
67+
expect(escapeValue(`${badChar}cmd|' /C calc'!A0`)).to.be(
68+
`"'${badChar}cmd|' /C calc'!A0"`
69+
);
70+
});
71+
});
72+
});
73+
74+
describe('when false', () => {
75+
let escapeValue: (val: string) => string;
76+
beforeEach(function() {
77+
escapeValue = createEscapeValue(true, false);
78+
});
79+
80+
['@', '+', '-', '='].forEach(badChar => {
81+
it(`should not escape ${badChar} injection values`, function() {
82+
expect(escapeValue(`${badChar}cmd|' /C calc'!A0`)).to.be(`"${badChar}cmd|' /C calc'!A0"`);
83+
});
84+
});
85+
});
86+
});
5787
});

x-pack/legacy/plugins/reporting/export_types/csv/server/lib/escape_value.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,23 @@
55
*/
66

77
import { RawValue } from './types';
8+
import { CSV_FORMULA_CHARS } from '../../../../common/constants';
89

910
const nonAlphaNumRE = /[^a-zA-Z0-9]/;
1011
const allDoubleQuoteRE = /"/g;
1112

12-
export function createEscapeValue(quoteValues: boolean): (val: RawValue) => string {
13+
const valHasFormulas = (val: string) =>
14+
CSV_FORMULA_CHARS.some(formulaChar => val.startsWith(formulaChar));
15+
16+
export function createEscapeValue(
17+
quoteValues: boolean,
18+
escapeFormulas: boolean
19+
): (val: RawValue) => string {
1320
return function escapeValue(val: RawValue) {
1421
if (val && typeof val === 'string') {
15-
if (quoteValues && nonAlphaNumRE.test(val)) {
16-
return `"${val.replace(allDoubleQuoteRE, '""')}"`;
22+
const formulasEscaped = escapeFormulas && valHasFormulas(val) ? "'" + val : val;
23+
if (quoteValues && nonAlphaNumRE.test(formulasEscaped)) {
24+
return `"${formulasEscaped.replace(allDoubleQuoteRE, '""')}"`;
1725
}
1826
}
1927

x-pack/legacy/plugins/reporting/export_types/csv/server/lib/generate_csv.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export function createGenerateCsv(logger: Logger) {
2626
cancellationToken,
2727
settings,
2828
}: GenerateCsvParams): Promise<SavedSearchGeneratorResult> {
29-
const escapeValue = createEscapeValue(settings.quoteValues);
29+
const escapeValue = createEscapeValue(settings.quoteValues, settings.escapeFormulaValues);
3030
const builder = new MaxSizeStringBuilder(settings.maxSizeBytes);
3131
const header = `${fields.map(escapeValue).join(settings.separator)}\n`;
3232
if (!builder.tryAppend(header)) {

x-pack/legacy/plugins/reporting/export_types/csv/types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,5 +109,6 @@ export interface GenerateCsvParams {
109109
maxSizeBytes: number;
110110
scroll: ScrollConfig;
111111
checkForFormulas?: boolean;
112+
escapeFormulaValues: boolean;
112113
};
113114
}

x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ export async function generateCsvSearch(
173173
...uiSettings,
174174
maxSizeBytes: config.get('csv', 'maxSizeBytes'),
175175
scroll: config.get('csv', 'scroll'),
176+
escapeFormulaValues: config.get('csv', 'escapeFormulaValues'),
176177
timezone,
177178
},
178179
};

x-pack/legacy/plugins/reporting/server/config/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export interface ReportingConfigType {
109109
checkForFormulas: boolean;
110110
maxSizeBytes: number;
111111
useByteOrderMarkEncoding: boolean;
112+
escapeFormulaValues: boolean;
112113
};
113114
encryptionKey: string;
114115
kibanaServer: any;

0 commit comments

Comments
 (0)