# DibaGuard — Independent Validation Report
> **Open-release copy** ([DibaAi/DibaGuard-Open](https://huggingface.co/DibaAi/DibaGuard-Open)). This document was written for the private build of DibaGuard. Numbers that involve the HTTP request model (`http_v2`) were measured with that model, which is **not** part of the open release; the open release ships the licence-clean network models described in `CLEAN_MODELS.md`. Licence statements inside this document are superseded by `DATA_LICENSES.md`, `CLEAN_MODELS.md` and the repository's `LICENSE`, `LICENSE-WEIGHTS` and `NOTICE` files. Sections of the original that were written in Persian were removed from this copy; nothing else was edited.
*Date: 2026-10-31. Validated tree: the working copy of `guard/` on this date. Versions: the HTTP-side measurements
(V-01…V-11, V-15, V-16, V-18, CRS, real traffic) do not depend on the engine changes made during the work. The
flow-side measurements were taken after the first change set (05:14 local: destination-flood tier acts on volume
alone only at ≥ 10 flows / 10 s; brute force by rate alone no longer on LDAP 389 / SMB 445) and **before** the second
(10:00 local: V-14 fixed with hash-set membership; GRU warm table capped; O(batch) buffer folding; `GlobalRate`
relative to each key's own 1-h baseline; campaign CUSUM capped at 2 h; destination flood no longer renames other
tiers' flows; slow 30-min port walking named `portscan`). V-12, V-13, V-17, V-20, V-21, V-22, the kill chains and the
harm runs should be re-measured on the 10:00 tree; V-22 in particular targets the old absolute `GlobalRate` floor.
Exception: the SIMARGL 2021 attack-window row was measured before both change sets.*
The validation team did not build DibaGuard. Every claim in the project's documents was treated as unverified until
measured here. Nothing under `dibaguard/`, `go/`, the existing `tests/*.py` or the existing `scripts/` was modified.
All traffic was synthesised in-process, sent over loopback, or replayed from datasets; nothing was sent to any external
host. Public test material (OWASP CRS regression tests, flow datasets) was used for evaluation only, never training.
---
## 1. Executive summary
DibaGuard's core ideas hold up in places the previous red team looked: the ten RT-xx fixes are in place (their 39
regression tests pass), Python↔Go parity fixtures pass, the Python canonicaliser defeats the classic obfuscation
layers, distributed credential stuffing is caught even with a rotating `Host` header, no benign Persian request of the
80,234-request zanbil.ir log is blocked, and a multi-stage intrusion is noticed at most stages. But the system can be
bypassed, and it harms legitimate users in ways the lab results do not show.
**Security correctness (bypass).** 17 of the 22 findings (V-01…V-17), 8 of them High; three of them (V-12, V-13, V-14) were fixed by the maintainers while this validation ran and are now regression tests:
* the canonicaliser *deletes live code*: `/* … */` folding is not quote-aware (V-01) and whitespace folding destroys
SQL line comments (V-02), so `?q=/*' union select password from users-- -*/` and `?id=1 union%23x%0aselect …`
reach the database while no signature or SQL-context tier sees them;
* fixed work caps silently drop data from judgement — JSON entries past the 200th, the first of two duplicate JSON
keys, a value past 2,048 characters, a multipart field after a 2 KB file part, the 65th field (V-05, V-06);
* the **Go fast path judges a different text** from the Python edge and sends that text to the sidecar, so the
Python-side defences (JSON `\u` escapes, HTML entities, NFKC, any-header RT-09 fix) do not exist in a Go deployment:
`{"q":"\u0024{jndi:ldap://…}"}` or a SQLi in `Accept-Language` passes inline *and* in the sidecar (V-08, V-09);
* **identity tricks defeat the behavioural tiers**: rotating eight user-agents turns any address into a "shared"
one that is never blocked (credential stuffing 300/300 served instead of 10/300; enumeration and scraping alarms
295 → 0), and rotating inside one IPv6 /64 makes every request a newborn source (V-10, V-11);
* **references can be poisoned**: one attack request among the first 1,000 sets the HTTP threshold (model-only
detection 87.5 % → 68.8 %; five requests → 6.2 %), a client-chosen `Host` header trains its own profile (→ 0 %),
and the sidecar's traffic feeds accept an unauthenticated re-calibration (→ 0 %) or an IP to frame (V-15, V-16);
* **state attacks**: re-calibration is O(n²) in distinct sources (81 s per epoch at 32k sources, slope 1.9 —
extrapolated hours at the default 500k), an out-of-order flow record rewinds a source's clock and lifts a block,
LRU eviction by fresh sources resets a scanner to "newborn", and a tracked source costs 3.6 KB (1.7 GB at the
default cap; 1.1 MB for one source that walked 512 ports) (V-12, V-13, V-14, V-17).
**Detection reliability.** Complex attacks: in a six-stage kill chain the recon, lateral movement and distributed
stuffing stages were caught within seconds and the SQL injection was caught by the database tier even when the
HTTP edge missed it; the slow exfiltration through the application's own connection was never seen by the database
tier, and a single source guessing SSH 15 times a minute for 30 minutes raised nothing (V-20, V-21). Harm: English
prose with a contraction and `;`, `--` or `#` is blocked as SQL injection (5.8 % [4.1, 8.2] of apostrophe-bearing
sentences of an independent English corpus; 10 of 15 ordinary review sentences). In a 26-hour simulated shop that
single cause blocks **146 of every 1,000 users per day [140, 153]** — whole NAT addresses at a time — while the same
day with that one review sentence reworded blocks 0.1 per 1,000 [0.01, 0.45] (V-18). Challenges stay at ≈ 150 per
1,000 users per day, and partner API clients and the search crawler are challenged — i.e. cut off (V-19).
**Independent validation.** On the 4,138 attack tests of the OWASP CRS regression suite (commit `ca679721`) the
Python edge's hard tiers fire on 15.1 % (PL1 in-scope families: 16.9 % [15.4, 18.6]; PL1 SQLi 42.9 % [37.6, 48.3],
XSS 34.0 % [27.6, 41.1], RCE 6.0 %), with 4.8 % alarms on the 1,054 negative tests; libinjection fires on 4.1 % /
13.8 % (SQLi/XSS) of all attack tests. The CRS tests are rule unit tests — many are fragments (`database(`,
`pg_catalog`) a context engine is not meant to flag — so these are lower bounds, but they are the first numbers on
material the authors did not write. On the GREEN public flow datasets with the engine's calibrate-then-score protocol
the DDoS of CSE-CIC-IDS2018 (AWS original) is separated almost perfectly (source-minute AUROC 0.9987 [0.997, 0.9998],
all 10 attack sources caught) but at a realised benign alarm rate of 49.6 % of source-minutes instead of the 0.1 %
budget — 2.9 % once the global-scan and campaign tiers are set aside — and the same tier pushes SIMARGL 2021's benign
traffic to 43.8 % (V-22). On LUFlow 2021, the temporal hold-out, 22 % of attacking sources are caught at a 10.5 %
benign source-minute alarm rate. The CIC-IDS2017 original could not be obtained from the publisher.
---
## 2. Method
### 2.1 What was exercised
| path | what judges a request / flow | how it was driven |
|---|---|---|
| Python edge | `parse.Request.model_text` → `canon` → `playbook.match_http` + `sqlctx.analyse` (A22, DbNet as soft second opinion) | `tests/validation/vharness.py::py_judge` |
| Layer (operator API) | `Layer` with `Policy({"mode": "enforce"})`, dry-run backend, shipped models from `D:/build-tmp/guard/dl/models` | `vharness.make_layer`, every script under `scripts/validate_*.py` |
| Go fast path inline | `go/dibaguard` `Decider.Inspect` (Canonical → MatchSigs + HTTP model, inline blocking on) | helper `tests/validation/gocanon` (imports the package through a `go.work`, reads stdin) |
| Go → sidecar | what `dibaguard-proxy` / `-authz` push to `guard_score_http`: the **Go** canonical text, judged by the Python playbook + A22 | `vharness.go_judge` |
| sidecar over loopback | `dibaguard.llm.make_server` on `127.0.0.1:0` | `scripts/validate_cascade.py feed`, `tests/validation/test_v_cascade.py` |
Enforcement was emulated as the fast path applies it: a blocked address's later requests/flows are refused and never
reach the engine; `rate_limit` is a 5 req/s token bucket; `challenge` is passed once per browser per address per day
and never by clients without JavaScript (API clients, crawlers); a hard-signature request is refused inline.
### 2.2 Statistics
Proportions carry 95 % Wilson intervals; flow AUROCs on (source, minute) units carry a 95 % cluster-bootstrap
interval over sources (100 resamples). Long-run harm uses three seeds (min–max and the pooled Wilson interval).
### 2.3 Reproduce
```
python -m pytest tests/validation/test_v_bypass.py -q -rxX # 38 passed, 36 xfailed (Go cases skip without a Go toolchain)
python -m pytest tests/validation/test_v_cascade.py -q -rxX # 7 passed, 3 xfailed, 4 XPASS(strict) = fixes that landed at the end (V-10, V-11, V-15a, V-15c)
python -m pytest tests/validation/test_v_reliability.py -q -rxX # 9 passed, 12 xfailed (~60 s)
python scripts/validate_canon_fuzz.py --n 3000 # placement matrix, transforms, Python-vs-Go differential fuzz
python scripts/validate_cascade.py all # identity, stale, lru, recal, memory, bootstrap, feed, campaign
python scripts/validate_killchain.py # multi-stage kill chains
python scripts/validate_harm.py --seeds 3 # 26 h benign site (add --no-prose-fp for the control)
python scripts/validate_realtraffic.py # zanbil.ir, SR-BH 2020, ModSecurity 2025 on both paths
python scripts/validate_crs_local.py # OWASP CRS tests (fetched/parsed on Kaggle) on every path
python kag.py run --job validate_crs --run-name … --cpu # CRS fetch + parse + libinjection baseline (Kaggle)
python kag.py run --job validate_flows --run-name … --cpu # GREEN flow datasets, calibrate-then-score (Kaggle)
```
Raw results: `D:/build-tmp/guard/validation/*.json` (one file per script; the Kaggle results as `*_status.json`).
---
## 3. Findings
| id | property | severity | measured | one-line fix |
|---|---|---|---|---|
| V-01 | bypass | **High** | 4/4 quote-wrapped payloads (SQLi string, SQLi tautology, XSS handler, cmdi) invisible to signatures + A22; the 2 SQLi forms raise no alarm at all, XSS/cmdi only a model challenge | fold comments only outside quoted literals, and judge the unfolded text too |
| V-02 | bypass | **High** | `union#x\nselect`, `union--\nselect`: 3/3 evade Python, Go inline and sidecar | strip `#…\n` / `--…\n` line comments *before* folding newlines (canon and `database.views`) |
| V-03 | bypass | Medium | `java\tscript:` / `java\nscript:` 3/3 evade A19 on all paths | remove ASCII tab/LF/CR inside URL schemes before matching (as browsers do) |
| V-04 | bypass | Medium | `
` and 3 more slash-separated handlers evade A19 | accept `/` as attribute separator in the RT-02 handler branch |
| V-05 | bypass | Medium | first of two duplicate JSON keys never judged: 11/11 payloads, Layer incl. model 0/11 | judge every occurrence of a key (`object_pairs_hook`) |
| V-06 | bypass | **High** | JSON entry 201+: 11/11 bypass incl. model; multipart field after a 2.1 KB file part, value > 2,048 chars, field 65+, char 513+: hard tiers miss, model only challenges | never truncate silently: judge head+tail, raise a "too large to judge" verdict |
| V-07 | bypass | Low–Med | 2nd value of a repeated header never judged (11/11, all paths); A22 skips `Accept-Language` | judge every header value; drop `accept-language` from `SKIP_HEADERS` |
| V-08 | bypass | **High** | Go path misses what Python catches: JNDI/XSS/cmdi in JSON `\u` escapes, SQLi in `Accept-Language` or a custom header, fullwidth SQL (6/6); fuzz: Python-blocked but Go-path-missed 10.4 % of 3,000 cases | share one canonicaliser (port `parse.py` to Go, or have the sidecar re-canonicalise raw requests) |
| V-09 | bypass | Medium | Go inline allows A22-only injections (3/3); the source is blocked only after the next sidecar poll | port the A22 SQL-context tier to Go or make the proxy wait for it on SQL-looking values |
| V-10 | cascade | **High** | stuffing from one address with 8 rotating UAs: 300/300 served, never blocked (fixed UA: blocked after 10); enumeration/scraping alarms 295 → 0 | "shared" only from network evidence (CGNAT range, operator list), never from client-chosen headers; keep per-address evidence too |
| V-11 | cascade | **High** | a fresh address per request inside one IPv6 /64: 300/300 served, never blocked | key detection state by /64 (as blocks already are) |
| V-12 | state | Medium (fixed, regression test) | one stale flow record every 30 s (a long-lived connection's late export) turns a blocked SSH brute force into monitor-only | never move a source's clock backwards (`last_t = max(last_t, t)`) |
| V-13 | state | Medium (fixed at unit level, regression test) | fresh one-flow sources > table size between probes: scanner age stays 1, 0/150 probes alarmed (17 alarmed and blocked without eviction) | evict by low evidence first; keep evidence/age of suspicious sources in a protected segment |
| V-14 | availability | **High** (fixed 10:00, now a regression test) | one re-calibration epoch: 1.6 s / 5.9 s / 23 s / 81 s at 4k / 8k / 16k / 32k sources (slope 1.9) | replace `np.isin` on object arrays with set lookups / factorised integer keys |
| V-15 | poisoning | **High** | 1 attack request in the bootstrap: threshold 4.35 → 6.16, model-only detection 87.5 % → 68.8 %; 5 requests → 6.2 %; attacker `Host` profile → 0 % | hygiene for the bootstrap (exclude alarmed requests, cap per-source share), robust quantile; profiles only for configured hosts |
| V-16 | poisoning | Medium | without a token: `guard_score_http` frames any address (blocked), `guard_calibrate` drops model-only detection 87.5 % → 0 % | require the token for `guard_calibrate`; authenticate the proxy's pushes (shared secret / mTLS) |
| V-17 | availability | Medium | 3,564 B per tracked one-flow source (1.7 GB at the 500k default; DESIGN budget 300 MB / 1M); 1.14 MB for one source with 512 keys; 7.1 KB per distinct `Host` value, unbounded | compact per-source records; cap keys per source by bytes; bound per-host state |
| V-18 | harm | **High** | English prose with an apostrophe and `;`/`--`/`#` blocked as SQLi: 5.8 % [4.1, 8.2] of 531 such sentences of the Python stdlib docs, 10/15 ordinary review sentences; simulated shop: 146 users blocked per 1,000 per day [140, 153] vs 0.1 without that sentence | require an SQL token after the break-out in the "unbalanced quote + terminator" fallback |
| V-19 | harm | Medium | ≈ 150 users challenged per 1,000 per day on a benign day (typos, flash crowd, deploy 404s); partner API clients (5/5) and the search crawler challenged — which cuts off any client without JavaScript | exempt verified crawlers / API keys; rate-limit rather than challenge machine clients |
| V-20 | reliability | Medium | single-source SSH guessing at 15/min for 30 min: 0/450 alarms (1/s is caught) | count failed-auth sessions per (source, service) on a slower horizon with an absolute floor |
| V-21 | reliability | Medium | paged read of `customers` by the app server, 400 rows/min for 1 h (24k rows): 0 DB alarms | new query shape on a sensitive table + cumulative rows per shape |
| V-22 | reliability | **High** | realised benign source-minute alarm rate at α = 10⁻³: 49.6 % (CSE-CIC-IDS2018 AWS) and 43.8 % (SIMARGL 2021), 2.9 % / 4.9 % without the global-scan / campaign tiers; LUFlow 2021 10.5 % | calibrate the global tiers' floors on the site's own benign traffic like every other tier |
Severity scale: **High** = a complete bypass of the hard tiers or of enforcement for a whole attack class, a
reference poisoning that disables a tier, an availability failure, or measurable harm to many legitimate users;
Medium = a bypass with a narrower precondition or a partial loss; Low = coverage gaps with limited reach.
**Status now (2026-10-31, afternoon): every finding is fixed and every repro is a regression test; the
remediation and the re-measurement are in §8.** Status at hand-over (tree of 2026-10-31 ~09:21 local): fixed during the validation and now regression tests:
V-12, V-13, V-14. Fixes that landed in the last minutes and turn their strict-xfail tests red (XPASS — flip them to
regression tests): V-10, V-11, V-15a (bootstrap threshold), V-15c (unbounded host buffers). V-15b (attacker-chosen
`Host` profile) still fails. Everything else in the table is open.
Every finding has `xfail(strict=True, raises=AssertionError)` tests with its id in `tests/validation/`
(`test_v_bypass.py`, `test_v_cascade.py`, `test_v_reliability.py`), each next to a passing control test where one
makes sense. V-12, V-13 and V-14 were fixed while this validation ran; their tests are now plain regression tests.
V-17 (`test_v17_*`) and V-19 (`test_v19_*`, a 4-hour opening of the simulated shop, ~30 s) have strict-xfail tests
too. V-22 has no local minimal repro: its evidence is the Kaggle runs (`guard_jobs/validate_flows.py --testbeds
cic2018aws,simargl2021`), taken on the pre-10:00 `GlobalRate`; `test_v22_*` is only a synthetic guard, which the
current tree passes.
---
## 4. Details per property
### 4.1 Property 1 — security correctness
#### V-01 Comment folding deletes live code — High
*Repro:* `test_v_bypass.py::test_v01_*`.
```
GET /item?q=/*'%20union%20select%20password%20from%20users--%20-*/ -> canonical text "q:q= "
GET /item?q= -> ""
GET /item?host=127.0.0.1;echo '/*';cat /etc/passwd;echo '*/' -> "127.0.0.1;echo ' '"
```
In a string context the database reads `'/*'` as a literal, then `union select …`, then a `--` comment; the browser
runs the `onerror` between two quoted attribute values; the shell runs `cat`. The canonical text every detector reads
has the payload removed. Through a calibrated Layer the two **SQL forms raise nothing at all** (event score 1: the
model reads the same emptied text); the XSS and command forms get a model *challenge* (score 85) — a challenge, not a
block, so any client that runs JavaScript passes. The Go fast path does not fold comments and blocks these
(A15/A19) — the reverse divergence.
*Root cause:* `dibaguard/http/canon.py:18-19` (`MYSQL_EXEC`, `C_COMMENT`) applied by `normalize` (`canon.py:62-64`) to
every value regardless of quoting.
*Fix:* fold comments only outside quoted literals (a small quote-aware scanner), and run the signatures on the
unfolded decoded value as well.
#### V-02 SQL line comments + newline — High
*Repro:* `test_v02_*`. `?id=1 union%23x%0aselect password from users` (MySQL `#`) and
`?id=1 union--%0aselect …` (every dialect). The canonicaliser folds the newline into a space (`canon.py:20,66`), so the
`#…`/`--…` comment now swallows the rest; `database.views` strips `/* */` but no line comments (`database.py:219-220`),
so A22 sees one comment too. The Go text keeps the newline but its A15 regex does not allow a comment between
`union` and `select`: inline `allow`/`challenge`, sidecar miss. Measured in the transform matrix: 2/2 `union`
variants missed on every path (Layer: challenge only).
*Fix:* remove line comments up to the newline *before* whitespace folding, in `canon.normalize` and `database.views`.
#### V-03 `javascript:` with tab / newline — Medium
The WHATWG URL parser strips ASCII tab and newline anywhere in a URL; `canon` turns them into a space and A19 needs
`javascript\s*:` unbroken (`playbook.py:49`). `?url=java%09script:alert(document.domain)` and the same inside an
`href` evade all paths (Layer: challenge). *Fix:* delete `[\t\n\r]` inside a URL scheme before matching.
#### V-04 Slash-separated event handlers — Medium
The RT-02 fix (`playbook.py:49-51`) requires whitespace or a quote before `on…=`; the HTML tokenizer also accepts `/`.
``, ``, `