Auditing CSV and JSON Exports for Sensitive Data

Auditing CSV and JSON Exports for Sensitive Data

pr0h0
data-securitycsvjsonprivacy
AI Usage (85%)

Exports are one of the easiest places for teams to leak data by accident. The app may have authorization, validation, and masking in the UI, so people assume a CSV or JSON export will follow the same rules. It often does not.

I have seen exports include far more than the page showed: internal IDs, recovery tokens, staff-only notes, debug fields, and nested objects nobody meant to ship to customers. Once that file leaves the app, the leak is hard to pull back. It gets emailed, uploaded to ticketing systems, or opened in a spreadsheet with no audit trail.

Why exports are a bigger leak surface than they look

Exports skip a lot of the browser-side safety net. The UI may hide a field, but the export code can still serialize it. The page may trim text, but the backend can still return the full record. And once the data is flattened into CSV, the receiver rarely asks whether every column belongs there.

The practical risk is not just privacy. A bad export can expose:

  • credentials or API tokens
  • internal URLs and hostnames
  • account recovery data
  • support notes and incident details
  • row-level identifiers that enable enumeration

If a lower-privileged user can generate the export, the bug is really an authorization problem, not just a formatting problem.

What to look for in CSV and JSON exports

Obvious fields: names, emails, tokens, IDs, and notes

Start with the obvious stuff. Look for direct personal data, access material, and anything that can link records back to a real person or tenant.

Typical risky columns:

  • email, phone, address
  • accessToken, refreshToken, apiKey
  • userId, accountId, orgId
  • notes, comment, internalMemo

The mistake is assuming “only metadata” is harmless. In many systems, the metadata is the record.

Hidden fields: metadata, nested objects, and debug values

JSON is where hidden data tends to hide. A developer may export a full object because it was convenient, then forget that the object contains more than the table view.

Watch for:

  • nested objects like customer.profile or billing.source
  • debug fields like isAdmin, riskScore, internalFlags
  • timestamps that reveal workflow timing
  • internal links like /admin/users/1234

CSV has its own trap: a field can contain serialized JSON, base64, or comma-separated subfields that look harmless at first glance. Open the raw file and inspect the actual bytes, not the spreadsheet rendering.

A practical review workflow

Open the file as data, not as a spreadsheet preview

Spreadsheet previews help, but they hide the parts that matter. I usually open the export in a text editor first, then run a structured parse.

You want to answer three questions:

  1. What fields are present?
  2. What values look sensitive?
  3. Does the export contain more rows, columns, or nesting than expected?

If the file is JSON, pretty-print it. If it is CSV, inspect the header and a sample of rows. Do not trust the UI preview alone.

Search for secrets, internal URLs, and high-risk identifiers

Use simple pattern checks before you do anything fancy. Search for:

  • bearer tokens
  • JWT-looking strings
  • long hex or base64 values
  • localhost, .internal, private IPs
  • file paths and admin routes

A quick grep-style pass catches a lot of bad exports before a human reviewer misses them.

Check for schema drift and unexpected columns

One of the most useful tests is boring: compare the current export schema to a known allowlist. If a new column appears, ask why.

Schema drift often introduces leaks slowly. A developer adds a debugging field, a support note, or a nested object “just for this release,” and it never gets removed.

CheckWhat it catchesWhy it matters
Extra columnsNew sensitive fieldsPrevents silent expansion of exported data
Nested objectsHidden attributesCSV/JSON often carry more than the UI shows
Unexpected IDsCross-tenant linkingEnables enumeration and correlation
Internal URLsEnvironment leakageCan expose admin or staging surfaces

Code examples for safe inspection

Quick Node.js scan for suspicious keys and values

const fs = require("fs");

const file = process.argv[2];
const raw = fs.readFileSync(file, "utf8");

const suspiciousKey = /(token|secret|password|apikey|api_key|session|cookie|refresh|internal|admin|memo|note)/i;
const suspiciousValue = /(bearer\s+[A-Za-z0-9._-]+|eyJ[a-zA-Z0-9_-]+\.|localhost|10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+)/i;

function walk(value, path = "") {
  if (Array.isArray(value)) {
    value.forEach((item, i) => walk(item, `${path}[${i}]`));
    return;
  }

  if (value && typeof value === "object") {
    for (const [key, child] of Object.entries(value)) {
      const nextPath = path ? `${path}.${key}` : key;
      if (suspiciousKey.test(key)) {
        console.log("suspicious key:", nextPath);
      }
      walk(child, nextPath);
    }
    return;
  }

  if (typeof value === "string" && suspiciousValue.test(value)) {
    console.log("suspicious value at", path, "=>", value.slice(0, 120));
  }
}

try {
  walk(JSON.parse(raw));
} catch {
  const lines = raw.split("\n");
  console.log("CSV/flat text sample:");
  lines.slice(0, 20).forEach((line, i) => {
    if (suspiciousValue.test(line)) console.log("line", i + 1, line.slice(0, 160));
  });
}

Comparing an export against an allowlist

const allowedCsvHeaders = new Set([
  "id",
  "name",
  "email",
  "createdAt"
]);

function validateHeaders(headers) {
  const unexpected = headers.filter((h) => !allowedCsvHeaders.has(h));
  if (unexpected.length) {
    throw new Error(`Unexpected columns: ${unexpected.join(", ")}`);
  }
}

This kind of check is simple, but it stops a lot of accidental leakage. The key is to fail closed. If the schema changes, the export should not quietly continue.

Common mistakes that cause real exposure

Assuming JSON is safer because it is structured

JSON is not safer. It is just easier to parse. If the backend serializes a rich object, JSON can expose more sensitive state than a CSV ever would.

The classic mistake is exporting an ORM object or API response directly. That usually includes fields nobody intended to make public.

Trusting spreadsheet redaction and truncated views

A spreadsheet may show masked values, truncated cells, or formatted dates, but the underlying file still contains the full content. If the export is shared outside the team, the raw data is what matters.

I do not count a field as redacted unless the file itself is redacted.

How to reduce the risk before export

Minimize fields at the source

The best defense is to construct export rows from an allowlist, not from a live object. Pull only the fields the export needs, and do that in the backend layer that owns the data.

Redact, hash, or split sensitive values

Not every value has to disappear completely. Sometimes you can:

  • redact the last part of a token
  • hash identifiers when the exact value is not needed
  • split sensitive details into separate, permissioned exports

If a field is only useful for support, do not put it in the general export.

Conclusion

Exports are a data boundary, not a convenience feature. Treat them like an API that can leak secrets, internal metadata, and access paths if you do not inspect them carefully.

My default workflow is simple: open the raw file, compare the schema to an allowlist, search for high-risk values, and reject anything that drifts. If the export is safe, that process is boring. If it is not, it usually becomes obvious fast.

Share this post

More posts

Comments