PageSourceSearch

https://pressthink.org/j/rosen-archive/frontend/utils/csvSafety.js?v=3.8.36

js pressthink.org collected 2026-10-02 04:26:59 UTC 2,186 bytes, 44 lines download raw bytes

1// CSV-cell escaping for the in-browser data export (#289).
2//
3// exportAsCSV builds a downloadable CSV from archive fields (title, summary,
4// url, publication, author) that originate in scraped page content and curator
5// submissions -- both untrusted. A cell whose first character is a spreadsheet
6// formula trigger is executed when the CSV is opened in Excel/Sheets/Numbers,
7// enabling =HYPERLINK(...) data exfiltration or =cmd|'/c ...'!A1 DDE attacks.
8//
9// The trigger set mirrors the canonical server-side neutraliser
10// (backend/src/rosen_scraper/csv_safety.py CSV_INJECTION_PREFIXES) so the
11// in-browser export and the canonical CSV neutralise the same characters:
12// the four OWASP formula prefixes plus the leading control characters
13// (tab/CR/LF) that importers strip or treat as formula starts.
14
15const CSV_FORMULA_TRIGGERS = new Set(['=', '+', '-', '@', '\t', '\r', '\n']);
16
17/**
18 * Escape one value for a CSV cell: neutralise formula injection, then apply
19 * RFC 4180 quoting.
20 *
21 * A leading formula trigger is prefixed with a single quote so spreadsheets
22 * treat the cell as text. A value already escaped on disk (e.g. "'@handle")
23 * begins with an apostrophe -- not itself a trigger -- so it is not
24 * double-escaped. The cell is then wrapped in double quotes (and any embedded
25 * quote doubled) when it contains a comma, quote, or newline.
26 *
27 * @param {*} value - raw cell value (coerced to string; null/undefined -> '')
28 * @returns {string} the escaped cell ready to join into a CSV row
29 */
30export const escapeCsvCell = (value) => {
31  if (value === null || value === undefined) return '';
32  let str = String(value);
33  if (str && CSV_FORMULA_TRIGGERS.has(str[0])) {
34    str = "'" + str;
35  }
36  // Quote on any character that can break CSV structure. Both \n and a bare \r
37  // are record terminators in RFC 4180, so a cell like "'\r=HYPERLINK(...)"
38  // must be wrapped — otherwise the \r starts a new row and the neutralised
39  // formula text leads the next line, defeating the leading-apostrophe escape.
40  if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {
41    return `"${str.replace(/"/g, '""')}"`;
42  }
43  return str;
44};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.