Skip to content

fix(auth): fail closed in PropertyScopeGuard + scope property reads#120

Merged
telivity-otaip merged 1 commit into
mainfrom
claude/harden-property-scope-guard
Jun 17, 2026
Merged

fix(auth): fail closed in PropertyScopeGuard + scope property reads#120
telivity-otaip merged 1 commit into
mainfrom
claude/harden-property-scope-guard

Conversation

@telivity-otaip

Copy link
Copy Markdown
Collaborator

Ports the genuinely-better hardening from the parallel #116 onto the already-merged guard (from my #115), instead of merging #116 — which is a conflicted duplicate built on the pre-#115 base.

The bug this closes (was live in main)

The merged PropertyScopeGuard did if (!propertyId || typeof propertyId !== 'string') return true. A duplicated ?propertyId=A&propertyId=B parses to an array, so typeof !== 'string' → the membership check was skipped entirely (fail-open IDOR bypass). Same for a missing req.user.

Changes

  • property-scope.guard.ts — fails closed: non-string/array propertyIdForbiddenException; missing user on a non-public scoped route → ForbiddenException. Skips @Public routes via Reflector (so connect/webhook routes carrying a propertyId aren't broken). Bypass on AUTH_ENABLED=false unchanged.
  • property.controller.tsGET /properties and GET /properties/:id are now membership-scoped. The row is the tenant (param is :id, not propertyId), so the global guard can't bind it — this was the gap fix(auth): enforce property membership on HTTP routes (CRITICAL #1) #116 also caught. Demo (auth off) returns everything.

Tests

New guard specs: array-bypass → 403, missing-user → 403, plus the existing cases. 822/822 api tests green, typecheck + lint clean.

Supersedes

Once this lands, #116 should be closed (its value is here, without the merge conflict).

🤖 Generated with Claude Code


Generated by Claude Code

Ports the hardening from the parallel PR #116 onto the merged guard instead
of merging that conflicting duplicate:

- PropertyScopeGuard now fails CLOSED when a non-public, property-scoped route
  has a non-string propertyId (a duplicated ?propertyId=A&propertyId=B parses
  to an array — previously `typeof !== 'string' -> return true` skipped the
  membership check entirely) or no authenticated user. @public routes are
  skipped via Reflector so connect/webhook routes that carry a propertyId
  aren't broken.
- property.controller GET list/detail are now membership-scoped: the row IS
  the tenant (param is `:id`, not `propertyId`), so the global guard can't
  bind it. Demo (AUTH off) returns everything.

New guard specs prove the array-bypass -> 403 and missing-user -> 403.
822/822 api tests green; typecheck + lint clean.
@telivity-otaip telivity-otaip merged commit c90e29a 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.

2 participants