Skip to content

fix(security): cross-tenant FK ownership in reservation/room/housekeeping (audit #4, #5, #6)#119

Merged
telivity-otaip merged 6 commits into
mainfrom
fix/fk-ownership-checks
Jun 17, 2026
Merged

fix(security): cross-tenant FK ownership in reservation/room/housekeeping (audit #4, #5, #6)#119
telivity-otaip merged 6 commits into
mainfrom
fix/fk-ownership-checks

Conversation

@telivity-otaip

Copy link
Copy Markdown
Collaborator

What

Closes three HIGH findings from the independent Codex security audit (~/Desktop/HAIP-codex-security-review.md). The DB schema FKs only constrain the referenced row's id, not its property — so a caller at property A could pass an FK id belonging to property B and the row would insert/update without complaint, leaking that row's details back via subsequent JOIN reads.

Findings closed

Pattern is uniform: select id from <fk_table> where id = AND property_id = → if empty, BadRequestException('… not found in this property').

Tests (TDD)

  • NEW reservation-fk-ownership.spec.ts — 3 cases: foreign roomTypeId on create, foreign ratePlanId on create, foreign ratePlanId on modify.
  • NEW room-fk-ownership.spec.ts — foreign roomTypeId denied; same-property allowed.
  • NEW housekeeping-fk-ownership.spec.ts — foreign roomId denied.
  • Updated existing housekeeping.service.spec.ts + housekeeping-dashboard.spec.ts mock sequences to account for the new leading FK select call.

826/826 api specs pass.

Out of scope (next PRs)

This PR is cluster A of 4 follow-on audit clusters:

🤖 Generated with Claude Code

telivity-otaip and others added 6 commits June 17, 2026 10:01
…ping (audit #4,#5,#6)

Closes three HIGH findings from the independent Codex security audit. The DB
schema FKs only constrain the referenced row id, NOT same-property — so a caller
at property A could pass an FK id belonging to property B and the row would
insert/update happily. Each service now verifies the FK belongs to the caller's
propertyId BEFORE the write.

- reservation.service.ts (#4):
  * create(): assertSamePropertyFk for dto.roomTypeId + dto.ratePlanId before insert.
  * modify(): same checks when dto.roomTypeId or dto.ratePlanId is supplied.
  * Shared private helper assertSamePropertyFk(table, id, propertyId, label).
- room.service.ts (#5): createRoom verifies dto.roomTypeId is same-property.
- housekeeping.service.ts (#6): create() verifies dto.roomId is same-property
  before generating the checklist or inserting. (Inner generateChecklist room
  + reservation lookups already scoped on main from PR #117 — defense in depth.)

Tests (TDD):
- New reservation-fk-ownership.spec.ts — 3 cross-tenant denial cases.
- New room-fk-ownership.spec.ts — foreign roomTypeId denied; same-property allowed.
- New housekeeping-fk-ownership.spec.ts — foreign roomId denied.
- Updated existing housekeeping.service.spec.ts + housekeeping-dashboard.spec.ts
  mock sequences to account for the new leading FK select call.

826/826 api specs pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…llotment (audit follow-on)

The Codex re-audit of PR #119 verified that #4, #5, #6 are RESOLVED but
adversarially found the SAME anti-pattern in four more service create() methods
the original audit missed:

- FolioService.create — caller-supplied reservationId / bookingId not scoped
- DepositService.recordDeposit — reservationId / paymentId not scoped
- RatePlanService.create — roomTypeId not scoped (derived-parent check already was)
- AllotmentService.createBlock — groupProfileId / ratePlanId not scoped

All four now run the same property-scoped SELECT before the insert. Pattern is
uniform: select id where id=$1 AND property_id=$2 → BadRequestException if empty.

Also closes test-quality gaps Codex flagged on the original PR:
- New modify() test for foreign roomTypeId (only ratePlanId was covered).
- New housekeeping auto-checklist branch test that asserts the FK guard trips
  BEFORE generateChecklist runs (asserts db.select called exactly once).

Plus 4 new TDD specs covering the new cross-tenant denials and one existing
rate-plan spec updated to provide the FK select mock so it can still reach the
derived-without-parent BadRequest path.

835/835 api specs pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…inbound mapping

Two more live cross-tenant FK writes Codex found in the adversarial re-audit:

1. AllotmentService.setInventory — accepted caller-supplied roomTypeId without
   verifying same-property. The availability lookup would return 0 sellable for a
   foreign roomTypeId, but a roomsAllotted=0 request still passed validation and
   inserted a cross-property allotmentBlockInventory row.
2. InboundReservationService — handleNewReservation and handleModification both
   resolved roomTypeId/ratePlanId from channelConnections.config.{room,rate}Mapping
   (operator-supplied JSON) and wrote them straight into reservations. A
   misconfigured or maliciously edited mapping could point at a foreign-tenant
   id, producing cross-tenant reservation rows on every inbound OTA push.

Both now verify the resolved id belongs to the connection's / request's propertyId
before any write (private assertSamePropertyFk helper mirroring the reservation
service's pattern).

Tests: new setInventory cross-tenant denial; new inbound-reservation-fk-ownership
spec (2 cases: foreign roomTypeId and foreign ratePlanId via mapping). Updated 6
existing inbound-reservation specs + 2 allotment setInventory specs to account
for the new leading FK selects.

838/838 api specs pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…-audit

Codex round-3 adversarial sweep of the whole codebase found 4 more places with
the same caller-supplied-FK-without-same-property-check anti-pattern:

1. ConnectBookingService.modify — newRoomTypeId / newRatePlanId from the OTAIP
   agent could point at a foreign tenant. Now verified before re-calc / update.
   Also tightened the rate plan re-read to filter by propertyId (was bare id).
2. AllotmentService.updateBlock — dto.ratePlanId could redirect a block to a
   foreign tenant's plan. Now verified before update.
3. CashierService.recordMovement — dto.reservationId could attribute the cash
   movement (and downstream reporting) to a foreign tenant. Now verified.
4. ChannelService.create / update — operator-supplied roomTypeMapping and
   ratePlanMapping JSON could embed foreign-tenant ids. New
   assertMappingOwnership() bulk-verifies every id with an inArray lookup before
   insert/update. (Inbound-reservation also has a read-time guard from the prior
   commit — this is defense in depth so the row never lands in the DB.)

Also closes the spec-coverage gap Codex flagged: new test asserts handleModification
denies a foreign-tenant mapping (the code was already correct, just untested).

845/845 api specs pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…ent.createReview)

Codex round-4 adversarial sweep found two more FK write paths:

1. RoomingListService.importRoomingList — caller-supplied entry.roomTypeId was
   inserted into rooming_list_entries without verifying same-property. Now the
   service checks each entry's roomTypeId; a foreign id is recorded as an
   'error' row (matches the existing per-row error handling — does not abort
   the whole batch).
2. AgentController.createReview — caller-supplied dto.reservationId was
   inserted into guest_reviews without verifying same-property. Now rejects
   with BadRequestException.

847/847 api specs pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The last FK ownership hole Codex found in round-5: RatePlanService.update()
spreads dto into the update, and UpdateRatePlanDto omits propertyId/roomTypeId
but NOT parentRatePlanId. A caller could re-parent a rate plan onto another
tenant's chain.

Now verifies dto.parentRatePlanId belongs to the request's propertyId before
the update, mirroring the create() path's derived-parent check.

Codex round-5 verdict on the rest: 'Beyond the rate-plan.update gap above, I
did not find another remaining caller- or mapping-supplied FK insert/update in
modules/* services that lacks same-property scoping.' This closes that gap.

848/848 api specs pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@telivity-otaip telivity-otaip merged commit da70684 into main Jun 17, 2026
4 checks passed
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.

1 participant