Skip to content

fix(security): enforce channel viewOnMap and private-position gates on analysis positions endpoint#3366

Merged
Yeraze merged 1 commit into
mainfrom
claude/great-dijkstra-ZR1qj
Jun 8, 2026
Merged

fix(security): enforce channel viewOnMap and private-position gates on analysis positions endpoint#3366
Yeraze merged 1 commit into
mainfrom
claude/great-dijkstra-ZR1qj

Conversation

@Yeraze

@Yeraze Yeraze commented Jun 8, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes #3365 — the /api/analysis/positions (Trails view) and /api/analysis/coverage-grid endpoints were returning GPS position data without enforcing the two finer-grained permission checks that the main map and v1/position-history already apply:

  • Per-channel viewOnMap — nodes on channels the user lacks viewOnMap permission for were still included in the response.
  • positionOverrideIsPrivate — nodes marked with a private position override were returned without requiring nodes_private:read.

Changes

src/server/routes/analysisRoutes.ts

  • Added buildPositionFilter(user, sourceIds) helper: pre-fetches the user's channel permission set (once per source), virtual-channel DB permissions, and node metadata (channel + positionOverrideIsPrivate) in bulk, then returns a cheap synchronous predicate.
  • /positions handler: applies the predicate to result.items after getPositions() returns.
  • /coverage-grid handler: passes the predicate as postFilter into getCoverageGrid(); also skips the shared in-memory cache for non-admin users since results are now user-specific.
  • Admin requests bypass the filter entirely (no behaviour change).

src/db/repositories/analysis.ts

  • Added optional postFilter?: (pos: PositionRow) => boolean to GetCoverageGridArgs.
  • getCoverageGrid() applies the filter inside its internal pagination loop.

src/server/routes/analysisRoutes.test.ts

  • Updated mock setup to include nodes.getAllNodes, getChannelDatabasePermissionsForUserAsSetAsync.
  • 3 new tests: channel-denied positions filtered out; private-position nodes blocked without nodes_private:read; admin bypass (filter never invoked).

Permission check comparison (after fix)

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

Test plan

  • Confirm existing analysis route tests still pass (19/19)
  • As a non-admin user with nodes:read but channel_0.viewOnMap = false: verify /api/analysis/positions returns no items for nodes on channel 0
  • As a non-admin user without nodes_private:read: verify nodes with positionOverrideIsPrivate = true are excluded from /api/analysis/positions
  • As admin: verify all positions are still returned (filter bypassed)
  • Verify Trails UI renders correctly for permitted nodes and omits restricted ones

https://claude.ai/code/session_011GB1VoJXTVbFh2wdaUoArj


Generated by Claude Code

…n analysis positions endpoint

The /api/analysis/positions endpoint was returning raw GPS position
history without checking whether the requesting user had viewOnMap
permission on the node's channel, or nodes_private:read permission
for nodes with positionOverrideIsPrivate=true.  This allowed any user
with source-level nodes:read access — including anonymous/guest — to
enumerate precise historical coordinates for nodes they should not be
able to see on the map.

The /api/analysis/coverage-grid endpoint shared the same root cause
since it calls getPositions() internally.

Fix: introduce buildPositionFilter() which pre-fetches the user's
channel permission set (once per source), virtual-channel DB
permissions, and node metadata (channel + positionOverrideIsPrivate)
in bulk.  The resulting synchronous predicate is applied:
  - after getPositions() returns in the /positions handler
  - inside getCoverageGrid()'s pagination loop via a new optional
    postFilter arg on GetCoverageGridArgs

Admin requests bypass filtering (no change in behaviour).
Non-admin requests to /coverage-grid skip the shared in-memory cache
because results vary per user.

Mirrors the checks already enforced by the main map and the v1
/nodes/:id/position-history endpoint.

Fixes #3365

https://claude.ai/code/session_011GB1VoJXTVbFh2wdaUoArj
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


Pull Request Review

I'll provide a comprehensive review of this security fix PR. Here's my analysis plan:

Review Tasks

  • Read and understand the security vulnerability being fixed
  • Review the implementation in analysisRoutes.ts
  • Analyze the repository changes in analysis.ts
  • Evaluate test coverage and quality
  • Check for potential security concerns
  • Assess performance implications
  • Provide final feedback and recommendations

🔍 Overall Assessment

This is an excellent security fix that addresses a significant privilege escalation vulnerability. The implementation is well-designed, thoroughly tested, and follows security best practices.

✅ Strengths

Security Implementation

  • Proper privilege enforcement: Correctly implements the same permission checks used by main map and v1 position-history endpoints
  • Admin bypass logic: Clean separation allowing admins full access while applying filters to non-admin users
  • Defense in depth: Applies filtering at multiple layers (both endpoints affected)

Performance Optimization

  • Efficient bulk loading: buildPositionFilter pre-fetches all permissions and node metadata once, avoiding N+1 queries
  • Synchronous filtering: Returns a cheap predicate function for hot-path execution
  • Smart caching: Correctly disables shared cache for non-admin users since results are now user-specific

Code Quality

  • Clear documentation: Excellent JSDoc explaining the security purpose and performance rationale
  • Consistent patterns: Follows established permission patterns from other routes
  • Type safety: Proper TypeScript usage throughout

Test Coverage

  • Comprehensive scenarios: Tests both permission types (channel viewOnMap + private position override)
  • Admin bypass verification: Ensures admins still get full access
  • Edge cases covered: Handles missing permissions gracefully

🔧 Technical Details

Filter Implementation (analysisRoutes.ts:77-130)

The buildPositionFilter function is well-architected:

  • Bulk loads permissions for all sources once
  • Pre-fetches node metadata to avoid runtime DB calls
  • Returns null for admins (no filtering needed)
  • Creates efficient synchronous predicate for non-admins

Repository Changes (analysis.ts:145)

Minimal, surgical change adding optional postFilter parameter to getCoverageGrid:

postFilter?: (pos: PositionRow) => boolean;

Cache Strategy (analysisRoutes.ts:218-226)

Correctly handles caching implications:

  • Admin requests: cached (results are user-agnostic)
  • Non-admin requests: not cached (results vary by user permissions)

📊 Permission Check Consistency

The PR achieves the stated goal of consistency across endpoints:

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

🚀 Performance Analysis

Positive Impacts

  • Bulk permission loading: Single DB query per source vs N queries per position
  • Cached admin responses: No performance regression for admin users
  • Efficient filtering: O(1) lookup per position after initial setup

Minor Considerations

  • Memory usage: Loads all node metadata upfront (acceptable for typical deployments)
  • Non-admin cache miss: Non-admin requests can't leverage shared cache (necessary for security)

🧪 Test Quality Assessment

The test suite covers all critical scenarios:

  1. Channel permission filtering (analysisRoutes.test.ts:119-144)
  2. Private position override blocking (analysisRoutes.test.ts:146-166)
  3. Admin bypass verification (analysisRoutes.test.ts:168-186)
  4. Cache behavior differences (analysisRoutes.test.ts:287-298)

Tests use proper mocking and verify both positive and negative cases.

🔐 Security Analysis

Vulnerability Severity: HIGH

The original issue allowed unauthorized access to:

  • GPS coordinates of nodes on restricted channels
  • Position data marked as private

Fix Quality: EXCELLENT

  • Applies consistent permission model across all position endpoints
  • No bypass vectors identified
  • Proper error handling (fails closed)
  • Admin privileges preserved appropriately

💡 Minor Suggestions

Code Documentation

Consider adding a brief comment in getCoverageGrid explaining why postFilter is needed:

// Optional security filter for non-admin users to enforce channel permissions
postFilter?: (pos: PositionRow) => boolean;

Error Handling

Current implementation gracefully handles missing node data by defaulting to { channel: 0, positionOverrideIsPrivate: false }. This is secure (fails closed) but consider logging when node metadata is missing for debugging.

🎯 Recommendation

APPROVE - This is a high-quality security fix that should be merged promptly. The implementation is thorough, well-tested, and addresses the vulnerability completely without introducing performance regressions or breaking changes.

The code follows established patterns in the codebase and maintains backward compatibility for admin users while properly securing access for non-admin users.


Job Run

@Yeraze Yeraze merged commit becfb8b into main Jun 8, 2026
19 checks passed
Yeraze added a commit that referenced this pull request Jun 10, 2026
The [4.9.4] section captured only the entries present in [Unreleased] when
the release branch was cut. Add the remaining changes that shipped in
v4.9.4 but merged without their own CHANGELOG entries: {DATE}/{TIME}
tokens (#3382), MeshCore JSONL export (#3391), system theme (#3344),
Map Pin Style fix (#3364), MeshCore connecting state (#3380), the Map
Analysis security gate (#3365/#3366), and the 11 dependency bumps.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants