From 95dca9d810ea706c66a13ffe34f6f4c0c3a3a1b6 Mon Sep 17 00:00:00 2001 From: Banh Date: Mon, 20 Jul 2026 21:35:05 -0400 Subject: [PATCH] fix: preserve raw:true Buffer headers instead of nulling them (#251) The #250 prototype-pollution fix (783a7ab) added sanitizeHeader() to reject any non-string header, but raw:true mode produces Buffer cells for every column including headers, so every header was silently nulled and writeRow() dropped every column, yielding {} for all rows. sanitizeHeader() now special-cases Buffer: compares the decoded utf-8 string against DANGEROUS_KEYS but returns the original Buffer when safe. Buffer.prototype.toString() is a fixed, non-overridable decode of the same bytes, so there's no check-time/use-time gap for it to disagree with itself the way an arbitrary object's toString() could. Other non-string types still return null, preserving #250's security posture. Also adds regression tests for #250 itself, which shipped with none, and updates index.d.ts/README.md to document Buffer headers. --- README.md | 4 +-- index.d.ts | 2 +- index.js | 10 +++++-- index.test-d.ts | 14 +++++++-- test/fixtures/prototype-pollution.csv | 2 ++ test/fixtures/raw-headers.csv | 3 ++ test/issues.test.js | 41 +++++++++++++++++++++++++++ 7 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 test/fixtures/prototype-pollution.csv create mode 100644 test/fixtures/raw-headers.csv 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) +})