Security #8633 » SU-UPDATE-01.md
SU-UPDATE-01 — suricata-update: rule-archive path traversal → arbitrary file write → RCE
Severity: HIGH [CONDITIONAL] — code execution as the user running suricata-update. Conditions: the attacker controls a fetched archive source, and the updater's privilege.
Reach: ruleset feed (0.3). Requires archive (tar/zip) delivery — the standard format for rule feeds (a single plain .rules source is inert).
Status: ✅ lab-confirmed end-to-end against suricata-update 1.3.7 (current release); the flaw is in the Python updater, independent of the Suricata engine version it is paired with.
Novelty: NOVEL — no CVE/GHSA/redmine; present at OISF/suricata-update master HEAD 210e0b6 (tag 1.3.7). Mechanism shipped for filemd5/filesha* in 1.2.0rc1 (2020), widened to lua in 1.3.6 (2025-07).
Description
When suricata-update installs a downloaded rule archive, a rule keyword (lua, filemd5, filesha1, filesha256) whose value contains ../, plus a matching archive member, writes that member's
contents outside the rules output directory — an arbitrary file write → RCE (e.g. via /etc/cron.d/).
Mechanism
Three behaviours combine (all in current source):
-
Extractor keeps
..—extract.pyextract_tar/extract_ziponlymember.name.lstrip("/");../survives into thedep_filesdict (main.py:1259-1260, every non-.rulesarchive member). -
Keyword value stored verbatim —
file_kw = ["filemd5","filesha1","filesha256","dataset","lua"](main.py:100). -
handle_embedded_file()writes unconfined (main.py:504-531):dest_filename = source_filename[len(prefix)+len(os.path.sep):] # keeps "../" filepath = os.path.join(config.get_output_dir(), os.path.dirname(dest_filename)) # "../" escapes os.makedirs(filepath) open(os.path.join(filepath, os.path.basename(embedded_filename)), "w").write(...) # attacker bytesbasename()guards only the leaf; the directory part keeps../, and there is norealpath/commonpathconfinement (contrast thedataset-save path, which rejects../absolute — CVE-2023-35852).
Reached by both--merge(:575) and--no-merge(:636).
Reachability & preconditions
Exploitation requires:
-
An archive (tar/zip) source. A single plain
.rulesURL is inert (dep_filesis filled only from non-.rulesarchive members). Archives are the standard format for the feedssuricata-update
manages: default ET/Open, ET/Pro, Positive Technologies, abuse.ch, malsilo and pawpatrules are all.tar.gz. - Attacker control of that archive's bytes — via a hostile or breached rule source (a compromised feed/mirror/CDN, or a third-party source the operator added).
-
A UTF-8 text payload — fine for cron,
profile.d,authorized_keys, configs.
These conditions are realistic: archives are the default delivery format, and a compromised or hostile rule feed is the standard supply-chain threat for an auto-updater.
Impact bypasses Suricata's rule containment
Suricata is designed to contain rule content — rule Lua is sandboxed by default, rule-load is hardened, and dataset save is confined — so a hostile feed's intended worst case is degraded detection /
file read / DoS, not code execution.
SU-UPDATE-01 bypasses all of it, and doesn't rely on that containment being airtight (it isn't — cf. the Lua sandbox bypass CVE-2026-45770, fixed 8.0.5): the write/exec happens at update time on the
filesystem, before the engine ever runs the rule. The decisive case is a trusted feed (e.g. ET/Open) compromised at the vendor or a mirror — the operator never consented to running attacker code;
path-confinement restores the guarantee.
Lab
docker compose (builder → Caddy → a real Suricata + the suricata-update 1.3.7 it ships).
Two demonstrations:
- cron: the poisoned archive in the configured source list + a root cron running
suricata-updateevery minute: the update-cron plants a distinct/etc/cron.d/suri_autocron, whichcrondthen executes — autonomous root RCE, no manual trigger (uid=0, ~100 s). - manual:
--url— tar+lua→/etc/cron.d, zip+filemd5→/etc/profile.dwrite outside the rules dir (both keywords/formats)
Remediation
Confine the write:
- require
realpath(output_filename)to lie withinrealpath(get_output_dir()) - reject
../absolute inembedded_filenameand in extracted member names (use Python'starfiledata filter; explicit zip checks). Mirror thedataset-save guard.
Systemic: sign rule archives so a hostile source cannot ship arbitrary content.
- « Previous
- 1
- 2
- Next »