From 8a6acc911e99aa05e4eb0be2a77e29f56cbb901a Mon Sep 17 00:00:00 2001 From: Ashwin Date: Fri, 20 Feb 2026 21:14:24 +0530 Subject: [PATCH] feat: Add UUID validation for organization IDs in backend API endpoints and frontend logic. --- backend/common/views/auth_views.py | 16 ++++++++++++++++ frontend/src/hooks.server.js | 6 +++++- frontend/src/lib/api.js | 8 ++++++++ .../src/routes/(no-layout)/org/+page.server.js | 6 ++++-- 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/backend/common/views/auth_views.py b/backend/common/views/auth_views.py index f1ebb64b..0a38c086 100644 --- a/backend/common/views/auth_views.py +++ b/backend/common/views/auth_views.py @@ -1,6 +1,7 @@ import json import logging import secrets +import uuid import requests from django.conf import settings @@ -293,6 +294,13 @@ class LoginView(APIView): default_org = None if org_id: + try: + uuid.UUID(str(org_id)) + except (ValueError, AttributeError): + return Response( + {"error": "org_id must be a valid UUID"}, + status=status.HTTP_400_BAD_REQUEST, + ) # Use specified org if user has access try: profile = profiles.get(org_id=org_id) @@ -547,6 +555,14 @@ class OrgSwitchView(APIView): {"error": "org_id is required"}, status=status.HTTP_400_BAD_REQUEST ) + try: + uuid.UUID(str(org_id)) + except (ValueError, AttributeError): + return Response( + {"error": "org_id must be a valid UUID"}, + status=status.HTTP_400_BAD_REQUEST, + ) + # Get current org for audit logging from_org = getattr(request, "org", None) diff --git a/frontend/src/hooks.server.js b/frontend/src/hooks.server.js index 5d328bfb..b5c35a31 100644 --- a/frontend/src/hooks.server.js +++ b/frontend/src/hooks.server.js @@ -15,6 +15,7 @@ import axios from 'axios'; import { env } from '$env/dynamic/public'; const API_BASE_URL = `${env.PUBLIC_DJANGO_API_URL}/api`; +const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; /** * @typedef {{ default_currency?: string, currency_symbol?: string, default_country?: string|null }} OrgSettingsPayload @@ -169,7 +170,10 @@ export const handle = sequence(Sentry.sentryHandle(), async function _handle({ e }; // Check if org cookie is set and token has org context - if (orgId) { + if (orgId && !UUID_RE.test(orgId)) { + // Invalid org cookie value, clear it + event.cookies.delete('org', { path: '/' }); + } else if (orgId) { const token = /** @type {string} */ (accessToken); if (tokenHasOrgContext(token, orgId)) { diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js index 89bbbffe..3e514ac1 100644 --- a/frontend/src/lib/api.js +++ b/frontend/src/lib/api.js @@ -10,6 +10,8 @@ import { env } from '$env/dynamic/public'; import { goto } from '$app/navigation'; +const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + // API Base URL from environment variables // Note: VITE_ prefix is required for client-side env vars const API_BASE_URL = env.PUBLIC_DJANGO_API_URL @@ -231,6 +233,9 @@ export const auth = { /** @type {Record} */ const body = { email, password }; if (orgId) { + if (!UUID_RE.test(orgId)) { + throw new Error('Invalid organization ID format'); + } body.org_id = orgId; } @@ -276,6 +281,9 @@ export const auth = { * @returns {Promise} New tokens and org data */ async switchOrg(orgId) { + if (!orgId || !UUID_RE.test(orgId)) { + throw new Error('Invalid organization ID format'); + } /** @type {any} */ const data = await apiRequest('/auth/switch-org/', { method: 'POST', diff --git a/frontend/src/routes/(no-layout)/org/+page.server.js b/frontend/src/routes/(no-layout)/org/+page.server.js index 16bc7476..8fd70a9a 100644 --- a/frontend/src/routes/(no-layout)/org/+page.server.js +++ b/frontend/src/routes/(no-layout)/org/+page.server.js @@ -13,6 +13,8 @@ import { env as publicEnv } from '$env/dynamic/public'; import { redirect, fail } from '@sveltejs/kit'; import axios from 'axios'; +const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + /** @type {import('./$types').PageServerLoad} */ export async function load({ cookies, locals }) { const user = locals.user; @@ -65,8 +67,8 @@ export const actions = { const orgId = formData.get('org_id')?.toString(); const orgName = formData.get('org_name')?.toString(); - if (!orgId) { - return fail(400, { error: 'Organization ID is required' }); + if (!orgId || !UUID_RE.test(orgId)) { + return fail(400, { error: 'Invalid Organization ID' }); } const jwtAccess = cookies.get('jwt_access');