jsonguard 0.1.0

Secure output sanitization for JSON/JSONL, CSV, and TSV — guards against injection, formula, bidi-override, and control-character attacks.
Documentation

jsonguard

Crates.io License: MIT CI Sponsor

Secure output sanitization for JSON/JSONL, CSV, and TSV. Guards against injection, formula, bidi-override, and control-character attacks — with a secure-by-default API that makes the safe path the only path.

The Problem

Emitting untrusted data into structured text formats is a minefield:

Attack Format Example
Formula injection CSV/TSV =HYPERLINK("https://evil.example","click me")
JSON string injection JSON hello"\n,"injected_key":"injected_value
Bidi override All U+202E reverses displayed text
Control characters All TAB/CR/LF breaks field boundaries
CJKV encoding hazard JSON Big5 has 0x5C as second byte — triggers JSON \ escape
Unbalanced quotes CSV Raw " in a field breaks RFC 4180 parsers

Most crates handle one or two of these. jsonguard handles all of them, in every function, by default.

Secure by Default

Every sanitizer in jsonguard accepts &str or &[u8]. Byte input is decoded through a safe UTF-8 lossy path before sanitization. There is no "call decode() first" footgun — the decode step is invisible and mandatory.

use jsonguard::{csv_field, tsv_safe, jsonl_safe};

// Both compile. Both are safe. The &[u8] path decodes first.
let safe = csv_field("O'Brien, \"quoted\"");
let safe = csv_field(b"\xB3\x5C raw bytes from Big5 source");
//                    ^^ 許 in Big5; 0x5C second byte is handled correctly

assert!(!safe.lossy);   // true only when bytes couldn't decode
println!("{}", safe);   // always a valid CSV field

Features

Feature Default Description
alloc Cow<'_, str> / String return types
std std::error::Error impl

Install

[dependencies]
jsonguard = "0.1"

No-std compatible:

[dependencies]
jsonguard = { version = "0.1", default-features = false, features = ["alloc"] }

Usage

CSV

use jsonguard::csv_field;

// Wraps in quotes and escapes internal quotes per RFC 4180
let field = csv_field("=SUM(A1:A10)");  // formula injection blocked
assert_eq!(field.to_string(), r#"'=SUM(A1:A10)"#);

let field = csv_field("name with \"quotes\"");
assert_eq!(field.to_string(), r#""name with ""quotes"""#);

TSV

use jsonguard::tsv_safe;

// Tabs, CR, LF replaced; bidi stripped; formula-prefixed fields escaped
let cell = tsv_safe("value\twith\ttabs");
assert_eq!(cell.to_string(), "value with tabs");

JSON Lines

use jsonguard::jsonl_safe;

// Produces a valid JSON string value (with surrounding quotes)
let val = jsonl_safe("line1\nline2");
assert_eq!(val.to_string(), r#""line1\nline2""#);

Display-safe strings

use jsonguard::{display_safe, cap_display};

// Strip control chars and bidi overrides for any display context
let s = display_safe("normal \x00 text \u{202E} reversed");
assert_eq!(s.to_string(), "normal  text  reversed");

// Truncate with a safe sentinel, never mid-character
let s = cap_display("hello world", 5);
assert_eq!(s.to_string(), "hello…");

Raw bytes

use jsonguard::bytes_to_utf8_lossy_safe;

let decoded = bytes_to_utf8_lossy_safe(b"\xFF\xFE valid utf-8 \xC0\x80 not");
assert!(decoded.lossy);  // replacement chars inserted
println!("{}", decoded); // safe for display

The Guarded type

Every sanitizer returns Guarded:

pub struct Guarded {
    pub value: String,
    pub lossy: bool,   // true when input had undecodable bytes
}

Guarded implements Display (emits value) so it drops straight into format strings. Check lossy when you care about data fidelity — e.g., log a warning or reject the record.

Attack Coverage

Attack class Handled
CSV formula injection (= + - @ prefix) '-prefix escape
RFC 4180 quote handling ✓ double-quote escape
TSV column shift (embedded TAB) ✓ replace with space
JSON string escape (backslash, quote, control chars)
CJKV Big5/GBK/EUC-KR byte collision with \ and " ✓ decode-first
Bidi override (U+202E, U+200F, U+2069, etc.) ✓ strip
C0/C1 control characters ✓ strip
Null bytes ✓ strip
UTF-8 overlong sequences and surrogates ✓ lossy decode

Acknowledgements


Privacy Policy · Terms of Service · © 2026 Security Ronin Ltd