Skip to content

Trip members can read, modify, clone and launder other members' private packing items

Moderate
mauriceboe published GHSA-vh2h-288v-ggch Aug 27, 2026

Software

liketrek/TREK

Affected versions

< 4.0.0

Patched versions

4.0.0

Description

Summary

Packing items in TREK have three visibility tiers: Common (is_private = 0, everyone on the trip), Personal (is_private = 1, the owner only) and Shared (is_private = 1 plus rows in packing_item_recipients).

The read path enforces this. PackingService.listItems() filters on is_private = 0 OR owner_id = ? OR EXISTS (SELECT 1 FROM packing_item_recipients ...), so a member's own list correctly omits items they may not see.

Every other path enforces only trip-level access. TripAccessGuard plus @RequirePermission('packing_edit') establish that the caller is a member of the trip who may edit packing, and the service methods then load the item scoped to trip_id alone. Knowing (or guessing) an item id is therefore enough for any trip member to reach an item that is not visible to them.

Impact

An authenticated user who is already a member of a shared trip can, for packing items belonging to other members:

  • Disclose them. The update response returns the full enriched item, including name, category, quantity, weight_grams, owner_username and the recipient list.
  • Modify them (rename, tick, re-weigh, move to a bag, and flip is_private).
  • Delete them.
  • Clone them into an item owned by the attacker, which makes the copy permanently visible to them.
  • Launder them. Copying the trip recreates every Personal and Shared item as a Common item in the new trip, visible to everyone there.
  • Persist them into a packing template, name and category included.

Packing-list contents can be sensitive (medication, medical aids, gifts, intimate items), and the owner's identity is disclosed alongside the item.

This is not remotely exploitable by an anonymous user: the attacker must hold an account that is already a member of the trip. Membership in a shared trip is the only privilege required, and the default member role carries packing_edit.

Affected code paths

All of these reach a service method that filters by trip_id only:

Entry point Gate Service call
PUT /api/trips/:tripId/packing/:id TripAccessGuard + packing_edit updateItem()
DELETE /api/trips/:tripId/packing/:id TripAccessGuard + packing_edit deleteItem()
POST /api/trips/:tripId/packing/:id/clone TripAccessGuard + packing_edit cloneItem() via getItemInTrip()
POST /api/trips/:tripId/packing/save-as-template TripAccessGuard + packing_edit saveAsTemplate(), which runs SELECT name, category FROM packing_items WHERE trip_id = ? with no visibility filter at all
POST /api/trips/:id/copy trip_create + trip access only, not packing_edit TripsService.copy(), which runs SELECT * FROM packing_items WHERE trip_id = ? and re-inserts without is_private or owner_id, so both fall back to the column defaults (0 / NULL)
MCP packing tools trip access + packing_edit same updateItem() / deleteItem()
Plugin RPC packing operations trip access + packing_edit same updateItem() / deleteItem()

Proof of concept

Two accounts, owner and member, on one shared trip. member is added as an ordinary member.

  1. owner creates a Personal item: POST /api/trips/6/packing with {"name":"Owner private medication","category":"Health","visibility":"personal"} returns is_private=1, owner_id=1.
  2. Control: member calls GET /api/trips/6/packing and receives 0 items. The read filter works.
  3. member calls PUT /api/trips/6/packing/1 with {"name":"pwned by member"}. Response HTTP 200, body contains the whole item including "owner_username":"admin".
  4. member calls POST /api/trips/6/packing/1/clone. Response HTTP 201, the clone is created with owner_id=2 (the attacker).
  5. member calls POST /api/trips/6/copy. In the copied trip, GET /api/trips/7/packing returns the item with is_private=0, owner_id=null, i.e. Common and visible to every member of the copy.

Suggested remediation

Centralise object-level authorization in the packing service and make it fail closed:

  • A single visibility predicate: Common, or the actor is owner_id, or the actor has a row in packing_item_recipients. Everything that reads or mutates a single item resolves the item through it.
  • Thread the authenticated actor through the HTTP, MCP and plugin-RPC entry points, so no path can call a mutation without one.
  • An item the actor may not see must behave exactly like a missing item (404), so the endpoints cannot be used as an existence oracle.
  • saveAsTemplate and TripsService.copy() must apply the same filter, and the trip copy must not silently drop is_private / owner_id on insert.

Credit

Reported privately by Chichi (@vickyzer027-hash), with a detailed breakdown of the affected paths and a proposed remediation. Reported responsibly, without a public issue, discussion or pull request.

Notes for triage

  • The reporter also described gaps in the contributor and shared-recipient constraints. That part has not been independently verified yet and is not covered by the proof of concept above.
  • The reporter states they briefly pushed their fix to a branch on a public fork before reading SECURITY.md, then deleted the branch. Commits pushed to a fork can remain reachable through the fork network after branch deletion, so partial public exposure should be assumed when planning the disclosure timeline.

Fix

Fixed in #1941, shipping with 4.0.0.

One visibility rule in the packing service now backs every single-item lookup, the actor is threaded through the HTTP, MCP and plugin-RPC paths, an item the actor may not see is reported as missing rather than forbidden, and neither the template nor the trip copy carries someone else's restricted item any more.

Verified against a running instance: the update, clone and trip-copy paths from the report all answer 404 or carry nothing over afterwards, while the owner, the recipients and the Common list keep working.

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
Low
Integrity
Low
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N

CVE ID

No known CVE

Weaknesses

Authorization Bypass Through User-Controlled Key

The system's authorization functionality does not prevent one user from gaining access to another user's data or record by modifying the key value identifying the data. Learn more on MITRE.

Incorrect Authorization

The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. Learn more on MITRE.