From de668e7950a019a204b2df0c84596ea0fa32cce6 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Tue, 2 Apr 2024 09:44:06 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=92=20Added=20escaping=20to=20member?= =?UTF-8?q?=20export=20CSV=20fields?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix https://linear.app/tryghost/issue/ENG-805/ refs https://owasp.org/www-community/attacks/CSV_Injection - it's possible for certain fields in a member CSV export to be executed by software that opens the CSVs - we can protect against this for the user by escaping any forumulae in the CSV fields - papaparse provides this option natively, so it's just a case of providing the field to the unparse method - credits to Harvey Spec (phulelouch) for reporting --- ghost/members-csv/lib/unparse.js | 1 + ghost/members-csv/test/unparse.test.js | 28 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/ghost/members-csv/lib/unparse.js b/ghost/members-csv/lib/unparse.js index 5cf4f1f046..983cff741f 100644 --- a/ghost/members-csv/lib/unparse.js +++ b/ghost/members-csv/lib/unparse.js @@ -61,6 +61,7 @@ const unparse = (rows, columns = DEFAULT_COLUMNS.slice()) => { }); return papaparse.unparse(mappedRows, { + escapeFormulae: true, columns }); }; diff --git a/ghost/members-csv/test/unparse.test.js b/ghost/members-csv/test/unparse.test.js index f139f10998..e04352d951 100644 --- a/ghost/members-csv/test/unparse.test.js +++ b/ghost/members-csv/test/unparse.test.js @@ -103,4 +103,32 @@ third-member-email@email.com,"banana, avocado"`; const expected = `email,tiers\r\nmember-email@email.com,Bronze Level`; assert.equal(result, expected); }); + + it('escapes fields starting with CSV injection characters', async function () { + const json = [{ + email: 'email@example.com', + name: '=1+2', + note: 'Early supporter' + }]; + + const result = unparse(json); + assert.ok(result); + + const expected = `id,email,name,note,subscribed_to_emails,complimentary_plan,stripe_customer_id,created_at,deleted_at,labels,tiers\r\n,email@example.com,"'=1+2",Early supporter,,,,,,,`; + assert.equal(result, expected); + }); + + it('escapes fields with CSV injection characters and quotes', async function () { + const json = [{ + email: 'email@example.com', + name: `=1+2'" `, + note: 'Early supporter' + }]; + + const result = unparse(json); + assert.ok(result); + + const expected = `id,email,name,note,subscribed_to_emails,complimentary_plan,stripe_customer_id,created_at,deleted_at,labels,tiers\r\n,email@example.com,"'=1+2'"" ",Early supporter,,,,,,,`; + assert.equal(result, expected); + }); });