TL;DR
-
What: Malcolm — the free network-traffic-analysis suite built by CISA and Idaho National Laboratory — shipped an nginx reverse-proxy config where one route's matcher was case-insensitive but its redirect was case-sensitive. Requesting that route in any non-lowercase casing (
/IDDASH2ARK/…) slipped past the redirect and fell through to an unauthenticatedproxy_pass, in one of two proxied locations that never got the auth include. -
Impact: an unauthenticated network caller reaches the Arkime full-packet-capture backend, and because that same location forwards a client-supplied
X-Forwarded-Userheader un-overwritten — the exact header Arkime is configured to trust as the logged-in user — the caller arrives with a forged, auto-provisioned identity. Rated Moderate 6.5 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N). - Fixed in v26.08.0, advisory GHSA-7j32-cf27-cp6h, credited to me as reporter. CVE-2026-107336 lists me as finder.
Why you should care
Malcolm is a free, full-packet-capture and network-traffic-analysis suite, developed by CISA in collaboration with Idaho National Laboratory for network security monitoring. Its front door is an analyst web UI on 443; behind that sits Arkime, the engine that stores and serves captured sessions and PCAP.
For a tool whose entire job is to hold captured network traffic, the auth gate on the front proxy is the security boundary. So when one route quietly falls off that gate, "unauthenticated" doesn't mean a marketing page is exposed — it means an anonymous caller is talking to the packet store.
The setup
Malcolm's front nginx gates its proxied routes by include-ing a runtime auth file (nginx_auth_rt.conf, which in the shipped default resolves to HTTP basic auth). Most proxied locations include it; two did not. That include does two jobs at once: it demands credentials, and it overwrites X-Forwarded-User with the authenticated user so a client can never forge it.
The /iddash2ark/ route is different by design. The "Dashboards → Arkime shortcut" at /iddash2ark/ was only ever meant to hand you a 302 redirect to the real, gated /arkime/sessions?expression=…. It didn't need its own auth, because a redirect leaks nothing. That assumption is where it goes wrong.
The bug
# nginx/nginx.conf (v26.07.1)
location ~* /iddash2ark/(.*) { # ~* = CASE-INSENSITIVE match
...
rewrite ^.*/iddash2ark/(.*) $forwarded_scheme://$host/arkime/sessions?expression=($1) redirect; # case-SENSITIVE (no (?i) modifier)
proxy_pass $arkime_protocol://arkime; # fall-through target: unauthenticated
...
include /etc/nginx/nginx_proxy_forward_headers.conf; # NOT the auth include
}
Two directives disagree about case. The location ~* matcher is case-insensitive, so /IDDASH2ARK/x enters the block. But the rewrite pattern ^.*/iddash2ark/(.*) has no (?i) modifier, so it is case-sensitive and does not match the uppercase path. No 302 is issued, and nginx falls through to the next thing in the block: a bare proxy_pass to Arkime. This location includes only the header-forwarding file, not the auth file. The request reaches the backend with no credentials checked. (/dashboards/app/extracted-files/ had the same case mismatch and the same missing include; the maintainer fixed both together.)
Then the confused deputy. The header file this location does include sets X-Forwarded-For/Host/Proto/... but never touches X-Forwarded-User. On every gated route, the auth include overwrites that header with the authenticated user; here nothing overwrites it, so whatever the client sends survives. And Arkime is configured to trust it:
# arkime/etc/config.ini (v26.07.1)
userAuthIps=::,0.0.0.0/0
userNameHeader=x-forwarded-user
userAutoCreateTmpl={"userId": "${this["x-forwarded-user"]}", ... "enabled": true, ... "webEnabled": true, "headerAuthEnabled": true, ... "packetSearch": true, ...}
userNameHeader=x-forwarded-user means Arkime reads the logged-in username straight out of that header; userAuthIps=::,0.0.0.0/0 trusts every IPv4 peer (and nginx is the peer); and the auto-create template provisions that user on the fly with packet search enabled. So the attacker doesn't merely reach the backend unauthenticated — they get to choose who they arrive as.
The "aha"
The location matcher was case-insensitive, but the redirect meant to neutralize the request was case-sensitive — so changing one letter to uppercase slipped the request into a route that never had an auth gate, carrying a username the backend had been told to trust.
Proof of concept (benign)
I didn't need the full suite for this — the bug lives entirely in Malcolm's own nginx config. I stood up a minimal nginx from the verbatim, unmodified shipped config fragments of tag v26.07.1, in front of a tiny stub that just echoes the method, path, and headers it receives (no Arkime logic reimplemented, nothing weaponized). Everything local, synthetic, no live or third-party system touched. The run went over plain HTTP on localhost; this is the recorded output:
# [1] positive control: GET /arkime/ (no creds)
HTTP/1.1 401 Authorization Required
# [2] inverse control: GET /iddash2ark/x (exact lowercase, no creds)
HTTP/1.1 302 Moved Temporarily
Location: http://127.0.0.1/arkime/sessions?expression=(x)
# [3] the bypass: GET /IDDASH2ARK/x (uppercase, no creds, X-Forwarded-User: forged-admin)
HTTP/1.1 200 OK
ARKIME_STUB_HIT method=GET path=/IDDASH2ARK/x x-forwarded-user='forged-admin' x-forwarded-roles=None authorization=None
The last request reached the backend unauthenticated, with the forged header passed through un-overwritten.
Mixed case (/IdDaSh2Ark/y) behaves identically, and the raw request URI (path + query) is forwarded verbatim, so the caller controls exactly what Arkime receives.
Honest boundary: I confirmed the bypass and the forged, un-overwritten identity reaching the backend. I did not stand up a real Arkime instance to confirm which session/PCAP responses it serves under that prefix. That's why the score sits at Moderate 6.5 (C:L) and not higher — the foothold is proven, the depth of the data read isn't, and I'd rather under-claim than over-claim. (Before the PoC I had it at 7.5, assuming a full data read. I filed it at 6.5 because that read wasn't confirmed.)
The fix
Shipped in v26.08.0 (2026-08-25), tracked in cisagov/Malcolm#1081. The maintainer's fix does three things. It makes the rewrite case-insensitive with the inline (?i) modifier, so the matcher and the rewrite agree. It deletes the dead proxy_pass and its forwarding headers, so the location can only ever redirect and nothing reaches Arkime through it. And it adds the auth include as a backstop. The location as shipped in v26.08.0:
location ~* /iddash2ark/(.*) {
include /etc/nginx/nginx_auth_rt.conf;
set $forwarded_scheme $scheme;
if ($http_x_forwarded_proto = 'https') {
set $forwarded_scheme https;
}
rewrite (?i)^.*/iddash2ark/(.*) $forwarded_scheme://$host/arkime/sessions?expression=($1) redirect;
}
The same change went into /dashboards/app/extracted-files/ and into the read-only profile (nginx_readonly.conf), where the /iddash2ark/ block was byte-identical. A week later the maintainer fixed a third location of the same shape, /dashboards/app/netbox/ (#1107).
Takeaways
-
A case-insensitive
location ~*paired with a case-sensitiverewriteis a bug, not a style choice. If a matcher and the directive meant to act on the match disagree about case, requests can enter the block and skip the action — here, skip the redirect that was the only thing standing between an anonymous caller and an unauthenticatedproxy_pass. -
Per-location auth is a checklist, and checklists have gaps. Here two "harmless redirect" locations also carried a
proxy_passand skipped the include. A redirect-only location shouldn't have aproxy_passat all, and a default-deny backstop — a server-levelauth_requestthat every location inherits — means one forgotten include isn't a total bypass. -
Never honor a forwarded identity header on a path that isn't behind the auth that sets it.
X-Forwarded-Useris only trustworthy if something upstream guarantees it. The moment a route forwards it without overwriting it, it's attacker input — a textbook confused deputy.
Disclosure timeline
- 2026-08-13 — found during a source review of Malcolm's nginx config.
- 2026-08-17 — reproduced locally against the shipped v26.07.1 config.
- 2026-08-19 — reported privately via GitHub's private vulnerability reporting.
- 2026-08-20 — advisory GHSA-7j32-cf27-cp6h published by the maintainer, crediting me as reporter.
- 2026-08-25 — fix released in v26.08.0.
- 2026-10-08 — CVE-2026-107336 published.
This is distinct from Malcolm's earlier nginx-RBAC advisories about URI normalization and percent-encoding, which have a different root cause. This one is the case-sensitivity mismatch on the /iddash2ark/ location plus an un-overwritten forwarded identity header.
Credit / CTA
If you run nginx with per-location auth, this is a five-minute grep worth doing today: look for ~* matchers paired with case-sensitive rewrites, and for any proxy_pass location missing your auth include. Two directives that disagree about case are enough to open a door.
Reported by me. Comments and corrections welcome.
Santosh Kumar Puppala — AI/ML Platform Architect and security researcher (multiple CVEs; creator of Norviq). GitHub: @Santoshkumarpuppala

Top comments (0)