Skip to content

Commit 1d7a947

Browse files
authored
Prevent CSV Injection (#210)
* Add preventCsvInjection option * Add tests for preventCsvInjection option * Fix indentation problem that caused the build to fail * Fix jsdoc asterix alignment that causes eslint to fail * Add unit test for csv injectable characters when preventCsvInjection is not specified
1 parent b7615ce commit 1d7a947

File tree

6 files changed

+160
-1
lines changed

6 files changed

+160
-1
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ Looking for examples? Check out the Wiki: [json-2-csv Wiki](https://github.com/m
140140
* Note: If selected, values will be converted using `toLocaleString()` rather than `toString()`
141141
* `wrapBooleans` - Boolean - Should boolean values be wrapped in wrap delimiters to prevent Excel from converting them to Excel's TRUE/FALSE Boolean values.
142142
* Default: `false`
143+
* `preventCsvInjection` - Boolean - Should CSV injection be prevented by left trimming these characters: Equals (=), Plus (+), Minus (-), At (@), Tab (0x09), Carriage return (0x0D).
144+
* Default: `false`
143145

144146

145147
For examples, please refer to the [json2csv API Documentation (Link)](https://github.com/mrodrig/json-2-csv/wiki/json2csv-Documentation)

lib/constants.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
"useDateIso8601Format": false,
3737
"useLocaleFormat": false,
3838
"parseValue": null,
39-
"wrapBooleans": false
39+
"wrapBooleans": false,
40+
"preventCsvInjection": false
4041
},
4142

4243
"values" : {

lib/converter.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ export interface ISharedOptions {
4747
* @default false
4848
*/
4949
trimFieldValues?: boolean;
50+
51+
/**
52+
* Should CSV injection be prevented by left trimming these characters:
53+
* Equals (=), Plus (+), Minus (-), At (@), Tab (0x09), Carriage return (0x0D).
54+
* @default false
55+
*/
56+
preventCsvInjection?: boolean;
5057
}
5158

5259
export interface IFullOptions extends ISharedOptions {

lib/json2csv.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ const Json2Csv = function(options) {
252252
processedRecordData = recordFieldData.map((fieldValue) => {
253253
fieldValue = trimRecordFieldValue(fieldValue);
254254
fieldValue = valueParserFn(fieldValue);
255+
fieldValue = preventCsvInjection(fieldValue);
255256
fieldValue = wrapFieldValueIfNecessary(fieldValue);
256257

257258
return fieldValue;
@@ -344,6 +345,26 @@ const Json2Csv = function(options) {
344345
return fieldValue;
345346
}
346347

348+
/**
349+
* Prevent CSV injection on strings if specified by the user's provided options.
350+
* Mitigation will be done by ensuring that the first character doesn't being with:
351+
* Equals (=), Plus (+), Minus (-), At (@), Tab (0x09), Carriage return (0x0D).
352+
* More info: https://owasp.org/www-community/attacks/CSV_Injection
353+
* @param fieldValue
354+
* @returns {*}
355+
*/
356+
function preventCsvInjection(fieldValue) {
357+
if (options.preventCsvInjection) {
358+
if (Array.isArray(fieldValue)) {
359+
return fieldValue.map(preventCsvInjection);
360+
} else if (utils.isString(fieldValue) && !utils.isNumber(fieldValue)) {
361+
return fieldValue.replace(/^[=+\-@\t\r]+/g, '');
362+
}
363+
return fieldValue;
364+
}
365+
return fieldValue;
366+
}
367+
347368
/**
348369
* Escapes quotation marks in the field value, if necessary, and appropriately
349370
* wraps the record field value if it contains a comma (field delimiter),

lib/utils.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ module.exports = {
1717
getNCharacters,
1818
unwind,
1919
isInvalid,
20+
isNumber,
2021

2122
// underscore replacements:
2223
isString,
@@ -248,6 +249,15 @@ function unwind(array, field) {
248249
return result;
249250
}
250251

252+
/**
253+
* Checks whether value can be converted to a number
254+
* @param value {String}
255+
* @returns {boolean}
256+
*/
257+
function isNumber(value) {
258+
return !isNaN(Number(value));
259+
}
260+
251261
/*
252262
* Helper functions which were created to remove underscorejs from this package.
253263
*/

test/json2csv.js

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,124 @@ function runTests(jsonTestData, csvTestData) {
656656
});
657657
});
658658

659+
// Test cases for https://github.com/mrodrig/json-2-csv/issues/209
660+
it('should left trim equals (=) if preventCsvInjection is specified', (done) => {
661+
converter.json2csv([{name: '=Bob'}], (err, csv) => {
662+
if (err) done(err);
663+
664+
let expectedCsv = 'name\nBob';
665+
666+
csv.should.equal(expectedCsv);
667+
done();
668+
}, {
669+
preventCsvInjection: true
670+
});
671+
});
672+
673+
it('should left trim plus (+) if preventCsvInjection is specified', (done) => {
674+
converter.json2csv([{name: '+Bob'}], (err, csv) => {
675+
if (err) done(err);
676+
677+
let expectedCsv = 'name\nBob';
678+
679+
csv.should.equal(expectedCsv);
680+
done();
681+
}, {
682+
preventCsvInjection: true
683+
});
684+
});
685+
686+
it('should left trim minus (-) if preventCsvInjection is specified', (done) => {
687+
converter.json2csv([{name: '-Bob'}], (err, csv) => {
688+
if (err) done(err);
689+
690+
let expectedCsv = 'name\nBob';
691+
692+
csv.should.equal(expectedCsv);
693+
done();
694+
}, {
695+
preventCsvInjection: true
696+
});
697+
});
698+
699+
it('should left trim at (@) if preventCsvInjection is specified', (done) => {
700+
converter.json2csv([{name: '@Bob'}], (err, csv) => {
701+
if (err) done(err);
702+
703+
let expectedCsv = 'name\nBob';
704+
705+
csv.should.equal(expectedCsv);
706+
done();
707+
}, {
708+
preventCsvInjection: true
709+
});
710+
});
711+
712+
it('should left trim tab (0x09) if preventCsvInjection is specified', (done) => {
713+
converter.json2csv([{name: String.fromCharCode(9) + 'Bob'}], (err, csv) => {
714+
if (err) done(err);
715+
716+
let expectedCsv = 'name\nBob';
717+
718+
csv.should.equal(expectedCsv);
719+
done();
720+
}, {
721+
preventCsvInjection: true
722+
});
723+
});
724+
725+
it('should left trim carriage return (0x0D) if preventCsvInjection is specified', (done) => {
726+
converter.json2csv([{name: String.fromCharCode(13) + 'Bob'}], (err, csv) => {
727+
if (err) done(err);
728+
729+
let expectedCsv = 'name\nBob';
730+
731+
csv.should.equal(expectedCsv);
732+
done();
733+
}, {
734+
preventCsvInjection: true
735+
});
736+
});
737+
738+
it('should left trim a combination of csv injection characters if preventCsvInjection is specified', (done) => {
739+
converter.json2csv([{name: String.fromCharCode(9) + String.fromCharCode(13) + '=+-@Bob'}], (err, csv) => {
740+
if (err) done(err);
741+
742+
let expectedCsv = 'name\nBob';
743+
744+
csv.should.equal(expectedCsv);
745+
done();
746+
}, {
747+
preventCsvInjection: true
748+
});
749+
});
750+
751+
it('should not alter numbers by removing minus (-) even if preventCsvInjection is specified', (done) => {
752+
converter.json2csv([{temperature: -10}], (err, csv) => {
753+
if (err) done(err);
754+
755+
let expectedCsv = 'temperature\n-10';
756+
757+
csv.should.equal(expectedCsv);
758+
done();
759+
}, {
760+
preventCsvInjection: true
761+
});
762+
});
763+
764+
it('should not left trim a combination of csv injection characters if preventCsvInjection is not specified', (done) => {
765+
let originalValue = String.fromCharCode(9) + String.fromCharCode(13) + '=+-@Bob';
766+
converter.json2csv([{name: originalValue}], (err, csv) => {
767+
if (err) done(err);
768+
769+
let expectedCsv = `name\n"${originalValue}"`;
770+
771+
csv.should.equal(expectedCsv);
772+
done();
773+
}, {
774+
});
775+
});
776+
659777
// Test case for #184
660778
it('should handle keys with nested dots when expanding and unwinding arrays', (done) => {
661779
converter.json2csv(jsonTestData.nestedDotKeysWithArrayExpandedUnwound, (err, csv) => {

0 commit comments

Comments
 (0)