Skip to content

Malcolm NSM wave (6 GHSAs): the Lua RBAC gate reads bytes nginx never routes on

Wave in one line: The Lua RBAC gate reads bytes nginx never routes on, one fall-through location is unauthenticated with a trusted identity header, and the shipped upload server fetches any URL for you

Source: hourly offensive-security scan of GitHub Security Advisories, 2026-10-08 (wave published 2026-10-08T18:32Z, six advisories for the Malcolm network-security-monitoring stack — idapyramid/Malcolm).

Malcolm is a widely deployed open NSM appliance (Arkime + Zeek + OpenSearch + a custom nginx reverse proxy with a Lua RBAC layer). The wave is a compact catalogue of reverse-proxy composition bugs — every layer of the shipped proxy makes a different decision from different input, and each mismatch is an authorization bypass. For an operator, NSM/monitoring appliances are high-value targets: they hold full packet captures, auth interfaces, and management scripts, usually on a flatter network segment than production.

Advisory table

Leg GHSA CVE Sev Boundary
Lua RBAC pattern-matches the raw percent-encoded URI; nginx routes on the decoded, normalized URI GHSA-rgrh-q687-4gc8 CVE-2026-107334 medium /%68tadmin.php matches no restriction pattern in the gate but nginx still routes it to the restricted location. Affects every RBAC-protected path family: upload, PHP server, htadmin, auth management.
"Dashboards → Arkime shortcut" location: case-insensitive regex matcher, case-sensitive rewrite; miss falls through to proxy_pass — the one proxied location with no per-location auth include — and X-Forwarded-User passes un-overwritten while Arkime trusts it as the authenticated username GHSA-c366-pv2g-9685 CVE-2026-107336 medium /IDDASH2ARK/... skips the redirect rewrite, reaches Arkime unauthenticated with a forged, auto-provisioned full-access identity.
Kiosk Flask app: POST /script_call/<script> with zero authentication + wildcard CORS GHSA-hfc4-x7vw-3m49 CVE-2026-107337 high Operator-browser CSRF executes arbitrary management commands — control.py --wipe permanently destroys all captured traffic/forensic logs, --stop blinds the monitoring. Also directly reachable if the kiosk port is exposed.
Ships upstream FilePond PHP server nearly unmodified; index.php fetch route downloads any URL via curl (FOLLOWLOCATION), and the HEAD variant stores the fetched body in the transfer dir and returns the transfer ID GHSA-6x93-f29r-jwj6 CVE-2026-107362 high Full-readback SSRF from the upload container — internal services, cloud metadata, and (via file://-class scheme support in the route) local content, then retrieved by transfer ID.
arkime-live runs network_mode: host, port 8005 on all interfaces, Arkime trusts X-Forwarded-User from any IP (userAuthIps=::,0.0.0.0/0), auto-creates users with full access, passwordSecret hardcoded to the public value "Malcolm" GHSA-9jvx-84p5-48xr CVE-2026-107361 medium A network-adjacent attacker bypasses the entire nginx+RBAC stack by connecting to 8005 directly with a forged identity header.
Upload pipeline safe-extract.py applies entry-count/depth/byte limits to zip/tar/rar/7z via libarchive but not to single-stream compressed files (.gz/.bz2/.xz/.lzma/.lz) GHSA-ffq8-c9hx-7wvf CVE-2026-107335 medium Any permitted uploader: small gzip bomb → unbounded decompress onto the shared OpenSearch/Logstash/Arkime/Zeek volume.

Why this is an operator page

  1. A reverse proxy is a multi-parser system — diff each layer's view of the URI before trusting the gate. Two legs here are the same bug family: the authorization decision consumes one representation of the path (raw encoded, or case-matched by a different function than the rewrite) and routing consumes another (decoded/normalized, case-sensitive rewrite). Probe battery for any nginx-fronted appliance with Lua/auth-by-path logic: percent-encode one character of a restricted path (%68, %2e%2e, %5c), vary case of every segment, add trailing /., ;params, duplicate slashes — then diff the gate decision (allow/deny response) against the served content (did the restricted app answer?). A 200 from the restricted app while the gate stayed silent is the finding. This is the same parser-differential family as the Oct 8 Coraza ARGS legs and the Brocade Host-header leg: the check's input is not the router's input.
  2. Enumerate per-location auth coverage, not proxy presence. The fall-through leg's root is config hygiene: every proxied location includes an auth file except one, picked because its matcher and its rewrite use different case rules. Recon rule on any multi-location proxy config you can read (appliance GitHub repo = pre-engagement reading): list all proxy_pass blocks and diff which include the auth snippet — the odd one out plus a routing quirk (case, encoding, alias-vs-root) is an unauthenticated path with no exploit needed.
  3. X-Forwarded-User-style identity headers are credentials when the backend can't tell proxies from callers. Two independent legs (fall-through proxy + direct-to-8005) both land on forged-identity full access. Audit rule for every Arkime/Dashboards/Kibana-style product fronted by a proxy: which ports answer without the proxy, and does the backend's trustedAuthIp/userAuthIps range cover those paths' peers? Decision-table probe from a host on the same segment: (no header, valid user header, garbage user header) × (proxied path, direct backend port). Auto-provisioning-on-first-sight (auto-creates users with full access) turns any forged name into an instant admin session.
  4. Appliances inherit their vendored components' routes. The FilePond leg is the vendor-ship surface: the Dockerfile copies upstream PHP files and the appliance only overwrote two of them. Enumerate upload endpoints' full route family (?[fetch|process|revert|head]-style query params on the uploader endpoint), not just the one the UI calls. Full-readback-by-transfer-ID turns a blind SSRF into a reading one in a second request — grep any upload/fetch API for "return the ID of what I stored."
  5. Unauthenticated management-script endpoints + wildcard CORS = destruction from a phishing link. script_call/<script> is the "kiosk convenience" class: a local helper endpoint that assumes its caller is the operator's browser. The wildcard CORS makes drive-by CSRF-to-wipe work from any page the operator visits. Evidence on authorized scope: OPTIONS preflight from an arbitrary origin + one read-only harmless script call shape (enumerate the script allowlist from the app's own source in the repo); never run --wipe/--stop on a live sensor, lab instance only.
  6. Decompression guards are per-format — sweep every accepted container spelling. Limits enforced "when extracting archives" but not "when the file is a single compressed stream" is the guard-coverage shape (same rule as Heimdall's one-controller SafeUrlFetcher, folded the same day). Upload fuzzing matrix: same payload wrapped as .zip, .tar.gz, and bare .gz/.xz/.lzma — asymmetric resource behavior across wrapper formats is the fingerprint.

Replayable validation boundaries (lab/authorized scope only)

  • RBAC differentials: authenticated low-privilege lab user, restricted paths are your own lab's htadmin/auth pages; encode/case variants; evidence = gate-vs-route decision table. No admin actions beyond read.
  • Fall-through + header forgery: own lab instance, X-Forwarded-User: canary-operator marker user; prove session provisioning + read of the Arkime UI index; stop before querying real PCAP indices.
  • Direct-port leg: connect to 8005 from a second lab host with your own marker identity; the hardcoded "Malcolm" secret is a config-fingerprint claim from shipped defaults, do not use it against non-lab hosts.
  • FilePond fetch: own HTTP callback listener + owned metadata-mock, prove fetch and transfer-ID readback of the canary body only.
  • Kiosk leg: enumerate script names from source; CSRF proof limited to an OPTIONS/shape test in your own browser against your own kiosk.
  • Bomb leg: lab instance, modest ratio (e.g. 1 GB expandable), monitor volume, delete marker after; never fill a shared production sensor volume.

Cross-references