#4051·arkime

Enforce forced expressions on viewer APIs that use sessionIds

Author: geo-chenCreated Jun 19, 2026Updated Jun 19, 2026
Labelsenhancementviewersome day

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#localSessionDetailprocessSessionId) — decoded packet payloads.
  • GET /api/session/:nodeName/:id.pcap / .pcapng / entire/...pcap (getPCAPFromNode, getPCAPNGFromNode, getEntirePCAP).
  • GET /api/session/raw/:nodeName/:id and /...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:

javascript
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:

javascript
app.get( // session detail (SPI) endpoint
  ['/api/session/:nodeName/:id/detail'],
  [logAction()],                       // no permission, no expression scoping
  SessionAPIs.getDetail
);

viewer/apiSessions.js getDetail:

javascript
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:

javascript
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 expression tags == 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.

  1. The boundary works on search — lowpriv cannot see the session:
GET /api/sessions   (as lowpriv)
-> recordsFiltered: 0   (session hidden by forced expression)

GET /api/sessions   (as admin)
-> recordsFiltered: 1   (session visible)
  1. The boundary is bypassed by-id — lowpriv reads 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.com

The 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.