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)
+})