Enforce forced expressions on viewer APIs that use sessionIds
based on SECURITY.md, reporting here instead:
Summary
Arkime restricts which sessions a user may access with a Forced Expression — a search expression silently appended to that user's (or role's) queries (e.g. tags == corp, or an IP/VLAN/node restriction). This is a documented access-control boundary used to limit analysts to the subset of captured traffic they are authorized to see.
The Forced Expression is only applied in the query-builder used by the search endpoints (viewer/buildQuery.js, where req.user.getExpression() is added to the ES query). Every endpoint that fetches a session by its session id instead resolves the session directly by document id and never applies the user's expression. As a result, any authenticated user who knows or can obtain a session id can read the full contents of sessions that are outside their Forced-Expression scope — bypassing the restriction entirely.
Affected by-id endpoints (all resolve the raw :id with no expression check):
GET /api/session/:nodeName/:id/detail(SessionAPIs.getDetail) — full session metadata (SPI: IPs, ports, hostnames, HTTP/DNS/TLS fields, tags).GET /api/session/:nodeName/:id/packets(getPackets→#localSessionDetail→processSessionId) — decoded packet payloads.GET /api/session/:nodeName/:id.pcap/.pcapng/entire/...pcap(getPCAPFromNode,getPCAPNGFromNode,getEntirePCAP).GET /api/session/raw/:nodeName/:idand/...png(getRawPackets,getPacketPNG).GET /api/session/:nodeName/:id/body...,/bodyhash/:hash(getRawBody,getBodyHashFromNode).
These routes enforce the hidePcap / disablePcapDownload permission flags, but those flags are independent of the per-user Forced Expression data-scope and do not constrain which sessions are reachable.
Details
Search path (boundary enforced) — viewer/buildQuery.js:
if (!err && req.user.getExpression()) {
...
const userExpression = arkimeparser.parse(req.user.getExpression());
query.query.bool.filter.push(userExpression); // forced expression applied
}By-id path (boundary NOT enforced) — viewer/viewer.js:
app.get( // session detail (SPI) endpoint
['/api/session/:nodeName/:id/detail'],
[logAction()], // no permission, no expression scoping
SessionAPIs.getDetail
);viewer/apiSessions.js getDetail:
static getDetail (req, res) {
const options = ViewerUtils.addCluster(req.query.cluster);
options._source = ['cert', 'dns'];
options.fields = ['*'];
Db.getSession(req.params.id, options, (err, session) => { // raw id, no expression
...
res.send( <full session SPI> );
});
}viewer/db.js Db.getSession issues a plain ids query and never receives the user's expression:
const query = { query: { ids: { values: [Db.sid2Id(id)] } }, _source: options._source, fields: options.fields };getPackets, getRawPackets, getPCAPFromNode, getEntirePCAP, getBodyHashFromNode, etc. follow the same raw-:id pattern (processSessionId / #writePcap / processSessionIdAndDecode) with no expression filter.
Session ids are routinely shared between analysts (links pasted in tickets/chat, exported pcap filenames, the rootId that links related session segments, multi-viewer references). A restricted user needs only one out-of-scope id to dump that session; ids also encode the time-bucketed index plus the document id, so adjacent/related ids are frequently derivable from in-scope material.
PoC
Setup: Arkime viewer against OpenSearch, two users both with packetSearch:
admin— no forced expression.lowpriv— forced expressiontags == corp:node addUser.js -c config.ini -n NODE lowpriv "Restricted" pass --packetSearch --expression "tags == corp"
A session that does NOT match tags == corp exists (e.g. tags:["secret"], src 10.9.9.9:44321, dst 10.8.8.8:443, host secret-internal-banking.example.com), with Sid 3@260619:260619-POCSECRETSESSION001.
- The boundary works on search —
lowprivcannot see the session:
GET /api/sessions (as lowpriv)
-> recordsFiltered: 0 (session hidden by forced expression)
GET /api/sessions (as admin)
-> recordsFiltered: 1 (session visible)- The boundary is bypassed by-id —
lowprivreads the same session:
GET /api/session/poc/3%40260619%3A260619-POCSECRETSESSION001/detail (as lowpriv)
-> HTTP 200, 24525 bytes
-> response contains: 10.9.9.9, 10.8.8.8, 44321, secret-internal-banking.example.comThe restricted user obtains the full SPI metadata of a session it is explicitly forbidden from seeing via search. GET /api/session/poc/<Sid>/packets (and the pcap/raw/bodyhash siblings) behave identically.
Impact
The Forced Expression is the mechanism deployments use to compartmentalize captured network traffic among users/teams (by tag, IP range, VLAN, node, etc.). Because it is enforced only on search and not on any by-id session-data or pcap endpoint, any authenticated low-privilege user can read session metadata, decoded packets, and raw pcap for sessions outside their authorized scope given a session id — defeating the data-segmentation control on a full-packet-capture system. Confidentiality impact is high; the security scope/boundary is crossed.
Suggested fix: apply req.user.getExpression() to the by-id lookups (resolve the session through a query that includes the forced expression / view restriction, or re-check the fetched session against the parsed expression before returning), mirroring the search path, for getDetail, getPackets, getRawPackets, getPCAPFromNode, getPCAPNGFromNode, getEntirePCAP, getPacketPNG, getRawBody, getBodyHashFromNode.
Source: arkime/arkime