Skip to content

fix: code review security and quality fixes#109

Merged
Kikolator merged 16 commits intodevfrom
fix/code-review-security-fixes
Mar 26, 2026
Merged

fix: code review security and quality fixes#109
Kikolator merged 16 commits intodevfrom
fix/code-review-security-fixes

Conversation

@Kikolator
Copy link
Copy Markdown
Owner

Summary

Comprehensive fixes from code review of the dev branch. Addresses 3 critical security issues, 6 high-severity bugs, and 4 medium-severity improvements.

Critical (security)

  • RLS JWT path bugspace_access_config policies used wrong JWT path, breaking the feature for all non-platform-admins
  • Member column escalationusers_update_own RLS policy allowed self-update of plan_id, status, has_twenty_four_seven etc. via direct Supabase client
  • API token exposurenuki_api_token was readable by any authenticated member via members_read SELECT policy

High

  • Added Zod validation to admin server actions (email, UUID, status inputs)
  • Fixed TOCTOU race in removePlatformAdmin with atomic RPC
  • Replaced Math.random() with crypto.randomInt() for door PIN generation
  • Added allowlist for updateFeatureFlag key parameter
  • Added null guard for spaceId in Stripe webhook routing
  • Added auth check to fetchNukiSmartlocks server action

Medium

  • Used Stripe auto-pagination (was capped at 100 records)
  • Added atomic conflict checks to create_booking_with_credits RPC
  • Documented two-layer auth model in admin app
  • Switched notification logging to admin client

Bonus

  • Renamed middleware.tsproxy.ts in both apps for Next.js 16

New migrations

  • 20260326081642_fix_access_config_rls_jwt_path.sql
  • 20260326082946_restrict_member_self_update_columns.sql
  • 20260326083158_restrict_access_config_member_view.sql
  • 20260326090126_atomic_remove_platform_admin.sql
  • 20260326091259_add_booking_conflict_check_to_rpc.sql

Test plan

  • Run supabase db reset to verify all migrations apply cleanly
  • Test member profile self-service — confirm protected columns (plan_id, status) cannot be modified via direct API
  • Test /access page as a member — should show access codes via RPC
  • Test admin dashboard — verify Stripe stats paginate correctly
  • Test desk booking — verify double-booking is prevented atomically
  • Test platform admin removal — verify last admin cannot be removed
  • Test Nuki PIN generation — verify codes are valid 6-digit non-zero
  • Verify both apps boot correctly with proxy.ts (no middleware.ts)

🤖 Generated with Claude Code

Kikolator and others added 15 commits March 26, 2026 09:19
The space_admins_manage and members_read policies on space_access_config
used (auth.jwt() ->> 'space_id')::uuid which reads from the JWT root.
This project stores space_id in app_metadata, so the correct accessor is
current_space_id(). This is the same bug that was fixed for import_jobs
in migration 20260320181450.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The users_update_own RLS policy allows members to UPDATE their own row
but has no column-level restriction. A malicious user could bypass
server actions and directly modify plan_id, status, has_twenty_four_seven,
and other security-sensitive fields via the Supabase client.

This trigger rejects changes to protected columns unless the caller is
service_role, a space admin, or a platform admin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The members_read SELECT policy on space_access_config exposed all
columns including nuki_api_token to any authenticated member. Replace
with a SECURITY DEFINER function that returns only safe columns, and
restrict direct table SELECT to space admins. Update the /access page
to use the new RPC.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add email, UUID, and status validation at API boundaries in platform
admin actions (addPlatformAdmin, removePlatformAdmin, updateTenantStatus,
updatePlatformFee) per project convention of Zod at API boundaries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous implementation checked admin count and deleted in separate
queries, allowing a race where two concurrent removals could both pass
the count check and leave zero admins. Replace with an atomic RPC that
deletes only if count > 1 in a single statement.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Math.random() is not cryptographically secure. For physical access PINs,
use Node's crypto.randomInt() which provides a CSPRNG.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
updateFeatureFlag accepted arbitrary keys from the client, allowing
injection of unexpected keys into the features JSONB column. Add an
allowlist check matching the FEATURE_FLAGS defined in the UI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Space-level webhook handlers used spaceId! (non-null assertion) but
spaceId can be null when the connected account lookup fails to find a
space. Add an explicit null check with error logging before dispatching
to handlers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The server action accepted a raw API token and made an external Nuki API
call without verifying the caller is authenticated. Add getSpaceId()
check to ensure only authenticated users with space context can invoke.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
getApplicationFeesSummary and getPaymentVolume were limited to 100
records, silently truncating results for active platforms. Use Stripe's
async iterator (for-await) to paginate through all records.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The booking overlap, desk conflict, fixed desk, and pass checks were
performed as separate queries before calling the RPC, creating a race
window where two concurrent bookings could both pass validation. Move
all conflict checks inside the SECURITY DEFINER function where they
run within the same transaction as the INSERT.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add comments clarifying that middleware handles authentication (layer 1)
while the (dashboard)/layout.tsx handles platform admin authorization
(layer 2). All protected routes must live under (dashboard).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The email notification logger used createClient() (user-scoped) which
could fail silently if RLS on notifications_log restricts inserts.
Infrastructure logging should use createAdminClient() to bypass RLS
and guarantee writes succeed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Next.js 16 renames middleware.ts to proxy.ts, which runs on Node.js
runtime instead of edge. Rename in both apps/web and apps/admin, remove
the config.matcher export (proxy runs on all requests), and add early
returns for static assets.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 26, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
cowork-admin Ignored Ignored Preview Mar 26, 2026 10:11am
cowork-platform Ignored Ignored Preview Mar 26, 2026 10:11am

Request Review

Add type definitions for remove_platform_admin and
get_member_access_config functions introduced in new migrations.
Required for TypeScript to recognize the RPC calls.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Kikolator Kikolator merged commit d91c652 into dev Mar 26, 2026
11 checks passed
@Kikolator Kikolator deleted the fix/code-review-security-fixes branch March 26, 2026 10:20
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