Skip to content

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 ..%2f traversal)
  • 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 recognises system() / 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 with lfi: 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 in inband_rules plus 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_proxies config; v1.3.7's target-health badges; v1.3.6's CrowdSec stale-creds detection -- all unaffected.
  • Block mode crowdsecurity/appsec-default still does NOT reference CRS, by design (vendor convention; false-positive risk on real traffic).