v1.3.10 — Detect mode now actually detects OWASP attacks¶
The third installment in the AppSec detection saga (v1.3.8 fixed the log-spam, v1.3.9 wired SendAlert), and the one that closes the loop end-to-end: detect mode now produces alerts for the broad OWASP attack classes operators expect a WAF to catch, not just for the narrow set of vendored CVE vpatches.
Bug¶
Smoke test against v1.3.9 with 10 standard OWASP payloads:
| Payload | Detected? |
|---|---|
| SQLi UNION-based | NO |
SQLi tautology (' OR '1'='1) | NO |
XSS <script> tag | NO |
XSS <img onerror=> | NO |
Path traversal ?file=../../etc/passwd | NO |
Command injection ?cmd=;cat /etc/passwd | NO |
RCE eval ?x=system("id") | NO |
Log4Shell ${jndi:ldap://...} | NO |
SSTI ?name={{7*7}} | NO |
WordPress recon /wp-config.php.bak | NO |
The only rule that ever fired in v1.3.9 was crowdsecurity/experimental-no-user-agent, triggered by an empty User-Agent header on the panel's own pre-v1.3.9 probes. Real attacks from real attackers would hit zero rules.
Root cause¶
crowdsec/appsec-configs/argos-appsec-detect.yaml:
inband_rules:
- crowdsecurity/base-config
- crowdsecurity/vpatch-* # CVE-specific virtual patches
- crowdsecurity/generic-* # tiny set: no-UA, scanner-UA, freemarker-SSTI
What's NOT here:
crowdsecurity/crs # OWASP Core Rule Set
# (SQLi, XSS, RCE, LFI, command injection,
# PHP injection, ~187 SecLang rules)
cscli appsec-rules list | grep crs confirms the CRS rule package is installed in the local rule pool (shipped via cscli collections install crowdsecurity/appsec-crs in setup-appsec.sh). But because no acquisition referenced it, no AppSec listener loaded it -- the rules sat there unused.
The vendor crowdsecurity/crs config (different file, separate acquisition) does load CRS, but as outofband_rules -- and argos's listener layout collapses everything into its own single-acquisition-per-mode shape that never references that vendor file.
Fix¶
inband_rules:
- crowdsecurity/base-config
+ - crowdsecurity/crs
- crowdsecurity/vpatch-*
- crowdsecurity/generic-*
CRS is added INBAND in detect mode (matching the user-supplied fix). Vendor convention puts CRS in OUTOFBAND because CRS in INBAND on production traffic carries non-trivial false-positive risk in BLOCK mode. Argos detect mode is default_remediation: allow + the v1.3.9 SendAlert hook -- a CRS match produces a panel-AppSec entry but the request flows through to the backend unchanged, so the false-positive cost is reduced to a log-volume question, never a user-visible 403.
Block mode (crowdsecurity/appsec-default, vendor) stays unchanged on purpose: the vendor explicitly keeps CRS out of inband block mode and we follow that.
Verification (prod stack argos-prod-argos:v1.3.10)¶
Cleared alerts, drove the same 10 OWASP payloads:
$ docker exec argos-prod-crowdsec cscli alerts list --since 2m
+----+---------------+-------------------------------------------------------+
| ID | value | reason |
+----+---------------+-------------------------------------------------------+
| 20 | Ip:172.18.0.1 | anomaly score block: lfi: 5, anomaly: 10 |
| 19 | Ip:172.18.0.1 | anomaly score block: rce: 5, anomaly: 5 |
| 18 | Ip:172.18.0.1 | anomaly score block: php_injection: 10, anomaly: 10 |
| 17 | Ip:172.18.0.1 | anomaly score block: lfi: 10, rce: 20, anomaly: 30 |
| 16 | Ip:172.18.0.1 | anomaly score block: lfi: 55, rce: 10, anomaly: 65 |
| 15 | Ip:172.18.0.1 | anomaly score block: xss: 30, anomaly: 30 |
| 14 | Ip:172.18.0.1 | anomaly score block: xss: 40, anomaly: 40 |
| 13 | Ip:172.18.0.1 | anomaly score block: sql_injection: 10, anomaly: 10 |
| 12 | Ip:172.18.0.1 | anomaly score block: sql_injection: 30, anomaly: 30 |
+----+---------------+-------------------------------------------------------+
9 alerts spanning every OWASP category from 10 payloads:
- SQL injection: 2 alerts (the UNION + tautology variants; CRS scored them at 10 and 30 anomaly points respectively reflecting confidence)
- XSS: 2 alerts (
<script>+<img onerror>-- 30 and 40 anomaly points) - LFI / path traversal: 1 alert at 55 points (the most confident match -- classic
..%2ftraversal) - Command injection: 1 alert combining LFI 10 + RCE 20 (CRS recognises the shell metacharacter chain on multiple axes)
- RCE eval: 1 alert with
php_injection: 10(CRS recognisessystem()/eval()as PHP-shaped) - Log4Shell JNDI: 1 alert with
rce: 5(the${jndi:...}shape gets matched by the generic remote-code-include pattern) - SSTI
{{7*7}}: 1 alert withlfi: 5(CRS picks up the template-expression brackets as a generic injection signal)
The 10th payload (/wp-config.php.bak recon) doesn't match a CRS signature -- recon paths are best caught by access-log buckets at the LAPI layer (crowdsecurity/http-probing, crowdsecurity/http-bad-user-agent etc), not by AppSec. That's working as designed.
Panel /api/appsec/metrics mirrors the cscli view:
{
"total_hits": 9,
"blocked": 0,
"logged": 9,
"top_rules": [
{"rule": "anomaly score block: rce: 5, anomaly: 5"},
{"rule": "anomaly score block: lfi: 55, rce: 10, anomaly: 65"},
{"rule": "anomaly score block: sql_injection: 10, anomaly: 10"},
...
]
}
logged: 9 matches detect-mode behaviour (alert recorded, request flowed through). blocked: 0 because the bouncer received {"action":"allow"} from the detect listener.
Known cosmetic issue (not blocking)¶
The panel UI categorises every CRS rule as "other" in the by_category chart (the categorize() function in backend/internal/appsec/metrics.go doesn't have prefix matchers for the anomaly score block: ... rule names CRS emits). That's a UI grouping concern, not a detection concern, and is left for a future release that adds an explicit CRS category mapping.
Files changed¶
crowdsec/appsec-configs/argos-appsec-detect.yaml-- one line added ininband_rulesplus a long comment explaining why CRS goes inband for detect (vs vendor convention putting it outofband).
Upgrade¶
cd argos-edge
git pull
docker compose build
docker compose up -d
docker compose exec crowdsec /setup-appsec.sh
The setup-appsec.sh step copies the new argos-appsec-detect.yaml over the stale one and SIGHUPs crowdsec; ~1 s of detect-listener downtime, no other impact. Alternatively a docker exec argos-prod-crowdsec kill -HUP 1 also works if the operator has already manually updated the config file.
Not changed¶
- v1.3.9's
on_match: SendAlert()directive stays in place; CRS matches now flow through it. - v1.3.8's
trusted_proxiesconfig; v1.3.7's target-health badges; v1.3.6's CrowdSec stale-creds detection -- all unaffected. - Block mode
crowdsecurity/appsec-defaultstill does NOT reference CRS, by design (vendor convention; false-positive risk on real traffic).
Related¶
- v1.3.9 -- where SendAlert() got wired but the rule set was still missing the OWASP coverage.
- Testing AppSec detection -- the 10-payload smoke that exposed this gap.
- Troubleshooting -> Detect mode emits no alerts -- now refers to the SendAlert fix; this release fixes a different "detection still narrow" follow-up.