diff --git a/README.md b/README.md index 0bc2338..6676884 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ csv({ ##### Parameters -**header** _String_ The current column header.
+**header** _String | Buffer_ The current column header (a `Buffer` when the `raw` option is `true`).
**index** _Number_ The current column index. #### mapValues @@ -217,7 +217,7 @@ Specifies a single-character string to denote a quoted string. Type: `Boolean`
-If `true`, instructs the parser not to decode UTF-8 strings. +If `true`, instructs the parser not to decode UTF-8 strings. This also affects headers: each header (and the `header` passed to `mapHeaders`) is a `Buffer` rather than a `String`. Row keys are still plain strings (JavaScript coerces object keys), but row values remain `Buffer`s. #### separator diff --git a/index.d.ts b/index.d.ts index cd3b718..90565dc 100644 --- a/index.d.ts +++ b/index.d.ts @@ -43,7 +43,7 @@ Bugs Bunny,22 * mapHeaders: ({ header, index }) => header.toLowerCase() * }); */ - readonly mapHeaders?: (args: { header: string; index: number }) => string | null; + readonly mapHeaders?: (args: { header: string | Buffer; index: number }) => string | Buffer | null; /** * A function that can be used to modify the value of each column value. diff --git a/index.js b/index.js index 545536f..3d336a5 100644 --- a/index.js +++ b/index.js @@ -20,12 +20,16 @@ const defaults = { const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']) -function sanitizeHeader(header) { - if (typeof header !== 'string') { +function sanitizeHeader (header) { + let key = header + + if (Buffer.isBuffer(header)) { + key = header.toString('utf-8') + } else if (typeof header !== 'string') { return null } - if (DANGEROUS_KEYS.has(header)) { + if (DANGEROUS_KEYS.has(key)) { return null } diff --git a/index.test-d.ts b/index.test-d.ts index 703330e..91966ea 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -10,13 +10,23 @@ expectType(csvParser({ headers: false })); expectType( csvParser({ mapHeaders: ({ header, index }) => { - expectType(header); + expectType(header); expectType(index); - return header.toLowerCase(); + return typeof header === 'string' ? header.toLowerCase() : header; }, }) ); expectType(csvParser({ mapHeaders: ({ header, index }) => null })); +expectType( + csvParser({ + raw: true, + mapHeaders: ({ header, index }) => { + expectType(header); + expectType(index); + return header; + }, + }) +); expectType( csvParser({ mapValues: ({ header, index, value }) => { diff --git a/test/fixtures/prototype-pollution.csv b/test/fixtures/prototype-pollution.csv new file mode 100644 index 0000000..c32247a --- /dev/null +++ b/test/fixtures/prototype-pollution.csv @@ -0,0 +1,2 @@ +a,__proto__,constructor,prototype,b +1,2,3,4,5 diff --git a/test/fixtures/raw-headers.csv b/test/fixtures/raw-headers.csv new file mode 100644 index 0000000..0099ae9 --- /dev/null +++ b/test/fixtures/raw-headers.csv @@ -0,0 +1,3 @@ +a,b +1,2 +3,4 diff --git a/test/issues.test.js b/test/issues.test.js index 7c06ee6..53368ae 100644 --- a/test/issues.test.js +++ b/test/issues.test.js @@ -23,3 +23,44 @@ test.cb('strict + skipLines (#136)', (t) => { collect('strict+skipLines', { strict: true, skipLines: 1 }, verify) }) + +test.cb('raw headers and rows are preserved (#251)', (t) => { + const verify = (err, lines) => { + t.false(err, 'no err') + t.is(lines.length, 2, '2 rows') + t.true(Buffer.isBuffer(lines[0].a), 'header a is a Buffer') + t.true(Buffer.isBuffer(lines[0].b), 'header b is a Buffer') + t.is(lines[0].a.toString(), '1') + t.is(lines[0].b.toString(), '2') + t.is(lines[1].a.toString(), '3') + t.is(lines[1].b.toString(), '4') + t.end() + } + + collect('raw-headers', { raw: true }, verify) +}) + +test.cb('prototype pollution keys are stripped from headers (#250)', (t) => { + const verify = (err, lines) => { + t.false(err, 'no err') + t.deepEqual(lines[0], { a: '1', b: '5' }, 'dangerous keys stripped') + t.false(Object.prototype.hasOwnProperty.call(lines[0], '__proto__'), 'no __proto__ own-property') + t.false(Object.prototype.hasOwnProperty.call(lines[0], 'constructor'), 'no constructor own-property') + t.false(Object.prototype.hasOwnProperty.call(lines[0], 'prototype'), 'no prototype own-property') + t.end() + } + + collect('prototype-pollution', verify) +}) + +test.cb('prototype pollution keys are stripped from raw Buffer headers (#250 + #251)', (t) => { + const verify = (err, lines) => { + t.false(err, 'no err') + t.is(Object.keys(lines[0]).length, 2, 'only safe headers survive') + t.true(Buffer.isBuffer(lines[0].a), 'header a is a Buffer') + t.true(Buffer.isBuffer(lines[0].b), 'header b is a Buffer') + t.end() + } + + collect('prototype-pollution', { raw: true }, verify) +})