Skip to content

[BUG] Security: Map Analysis "Trails" exposes GPS history to users lacking viewOnMap/nodes_private permissions #3365

@TheWISPRer

Description

@TheWISPRer

Describe the bug

The Map Analysis "Trails" view exposes full GPS location history — including node identifier (nodeNum), latitude, longitude, altitude, and timestamps — to users who should not have access to that data. This occurs because the /api/analysis/positions endpoint (which powers the Trails visualization) only enforces a coarse source-level nodes:read permission check and skips the two finer-grained permission gates that protect every other location-exposing surface in the application:

  1. Per-channel viewOnMap permission is not checked. An admin can restrict a user role (including anonymous/guest) from seeing node positions on the map by setting channel_N.viewOnMap = false. The main map and the v1 position-history API both enforce this. The analysis Trails view does not — nodes on those channels still appear in their entirety.

  2. Private position override (positionOverrideIsPrivate) is not checked. Nodes marked with a private position override require the nodes_private:read permission before any location data is disclosed. The analysis endpoint queries the raw telemetry table directly (bypassing the override logic entirely) and never inspects this flag, so unredacted historical GPS telemetry is returned regardless of that setting.

Because nodeNum is included in every returned record, an observer can convert it to the standard !xxxxxxxx Meshtastic hex identifier and cross-reference it against public mesh maps (e.g., meshmap.net) to correlate trails with real-world node owners — even without any node name or callsign being present in the response.

This also means nodes on encrypted channels (where the user has no viewOnMap access) are exposed. The problem affects anonymous users and any authenticated user whose permissions have been scoped to deny map visibility on specific channels.


To Reproduce

  1. As an admin, configure a source such that the anonymous (guest) user has nodes:read access but channel_0.viewOnMap is not granted — or mark any node as having a private position override (positionOverrideIsPrivate = true).
  2. Open MeshMonitor in a private/incognito browser window (unauthenticated/anonymous session).
  3. Navigate to MapMap Analysis.
  4. Select the Trails view.
  5. Observe that GPS trails are rendered for nodes and channels that should be hidden from anonymous users under the configured permissions.

The raw data can also be observed directly:

GET /api/analysis/positions

The response will include paginated records of nodeNum, latitude, longitude, altitude, sourceId, and timestamp regardless of channel or private-position settings.


Expected behavior

The Trails view and the /api/analysis/positions endpoint should enforce the same permission model as the main map and the v1 position-history API:

  • Positions for nodes on channels where the requesting user lacks viewOnMap permission should be excluded from the response.
  • Positions for nodes with positionOverrideIsPrivate = true should require nodes_private:read permission; users lacking that permission should receive no location data for those nodes.

The v1 per-node endpoint (GET /api/v1/nodes/:nodeId/position-history) already implements both of these checks correctly and can serve as the reference implementation.


Screenshots

N/A — the vulnerability is observable via the Trails UI rendering location data that should be hidden, or directly via the API response JSON.


Environment (please complete the following information):

  • Host OS: Linux (Debian)
  • Deployment type: Docker
    • Container version: 4.9.3
  • Networking customizations: Caddy reverse proxy, Authentik OIDC authentication provider
  • Database backend: PostgreSQL
  • Node type: N/A (server-side permission enforcement bug, not node-specific)
  • Node Connection Type: N/A
  • VirtualNode client: N/A

Additional context

Root cause is in server/routes/analysisRoutes.js. The resolvePermittedSourceIds() helper used by all analysis endpoints only calls checkPermissionAsync(userId, 'nodes', 'read', sourceId). It never calls checkNodeChannelAccess() (from server/utils/nodeEnhancer.js) or inspects positionOverrideIsPrivate on individual nodes.

Comparison of what each surface checks:

Check Main map v1 /position-history Analysis /positions
Source-level nodes:read
Per-channel channel_N.viewOnMap
positionOverrideIsPrivatenodes_private:read

The /api/analysis/coverage-grid endpoint shares the same root cause (it calls getPositions() internally in db/repositories/analysis.js), though its output is binned grid cells rather than precise coordinates, so the direct location exposure risk is lower.

Suggested fix: in analysisRoutes.js, after fetching the raw page of positions from getPositions(), apply a per-record post-filter that (a) calls checkNodeChannelAccess() for each nodeNum to enforce viewOnMap, and (b) checks node.positionOverrideIsPrivate and gates on nodes_private:read — mirroring what v1/positionHistory.js already does. The same filter should propagate to the coverage-grid handler.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions