|
@@ -0,0 +1,529 @@
|
|
|
|
|
+# Frontend Security Audit Report
|
|
|
|
|
+
|
|
|
|
|
+**Date:** 2025-12-12
|
|
|
|
|
+**Scope:** shopcall.ai-main/src/ (React/TypeScript frontend)
|
|
|
|
|
+**Auditor:** Claude Code Security Analysis
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## Executive Summary
|
|
|
|
|
+
|
|
|
|
|
+A comprehensive security audit of the ShopCall.ai frontend application identified **17 security vulnerabilities** across the codebase. The findings include 2 critical, 6 high, 5 medium, and 4 low severity issues.
|
|
|
|
|
+
|
|
|
|
|
+| Severity | Count |
|
|
|
|
|
+|----------|-------|
|
|
|
|
|
+| Critical | 2 |
|
|
|
|
|
+| High | 6 |
|
|
|
|
|
+| Medium | 5 |
|
|
|
|
|
+| Low | 4 |
|
|
|
|
|
+| **Total** | **17** |
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## Critical Vulnerabilities
|
|
|
|
|
+
|
|
|
|
|
+### 1. Unvalidated Open Redirect in OAuth Flow
|
|
|
|
|
+
|
|
|
|
|
+**File:** `src/pages/Index.tsx:8-12`
|
|
|
|
|
+**CVSS Score:** 9.1 (Critical)
|
|
|
|
|
+
|
|
|
|
|
+**Vulnerable Code:**
|
|
|
|
|
+```tsx
|
|
|
|
|
+const Index = () => {
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ const params = new URLSearchParams(window.location.search);
|
|
|
|
|
+ const shop = params.get('shop'); // Unsanitized user input
|
|
|
|
|
+ if (shop) {
|
|
|
|
|
+ window.location.href = `${API_URL}/shopify-oauth/init?shop=${shop}`; // No validation
|
|
|
|
|
+ }
|
|
|
|
|
+ }, []);
|
|
|
|
|
+ return <LandingPage />;
|
|
|
|
|
+};
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Risk:** Attackers can craft malicious URLs that redirect users to phishing sites. For example: `https://shopcall.ai/?shop=malicious-site.com` could be used in phishing campaigns.
|
|
|
|
|
+
|
|
|
|
|
+**Remediation:**
|
|
|
|
|
+```tsx
|
|
|
|
|
+const isValidShopifyDomain = (shop: string): boolean => {
|
|
|
|
|
+ const pattern = /^[a-zA-Z0-9][a-zA-Z0-9-]*\.myshopify\.com$/;
|
|
|
|
|
+ return pattern.test(shop);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+if (shop && isValidShopifyDomain(shop)) {
|
|
|
|
|
+ window.location.href = `${API_URL}/shopify-oauth/init?shop=${encodeURIComponent(shop)}`;
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 2. Missing Dependency in Authentication Check
|
|
|
|
|
+
|
|
|
|
|
+**File:** `src/components/PrivateRoute.tsx:13-15`
|
|
|
|
|
+**CVSS Score:** 8.5 (High/Critical)
|
|
|
|
|
+
|
|
|
|
|
+**Vulnerable Code:**
|
|
|
|
|
+```tsx
|
|
|
|
|
+useEffect(() => {
|
|
|
|
|
+ check_auth(null);
|
|
|
|
|
+}, []); // Missing 'check_auth' dependency
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Risk:** The `check_auth` function reference could become stale if AuthContext re-renders, potentially causing authentication checks to be skipped.
|
|
|
|
|
+
|
|
|
|
|
+**Remediation:**
|
|
|
|
|
+```tsx
|
|
|
|
|
+useEffect(() => {
|
|
|
|
|
+ check_auth(null);
|
|
|
|
|
+}, [check_auth]);
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## High Severity Vulnerabilities
|
|
|
|
|
+
|
|
|
|
|
+### 3. Insecure JSON.parse Without Error Handling
|
|
|
|
|
+
|
|
|
|
|
+**Files:**
|
|
|
|
|
+- `src/pages/IntegrationsRedirect.tsx:90, 467`
|
|
|
|
|
+- `src/components/context/AuthContext.tsx` (multiple locations)
|
|
|
|
|
+
|
|
|
|
|
+**Vulnerable Code:**
|
|
|
|
|
+```tsx
|
|
|
|
|
+const session = JSON.parse(sessionData); // No try-catch - will crash on invalid JSON
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Risk:** If localStorage/sessionStorage is corrupted (by browser extensions, cache poisoning, or malicious scripts), the application will crash with an unhandled exception, causing denial of service.
|
|
|
|
|
+
|
|
|
|
|
+**Remediation:**
|
|
|
|
|
+```tsx
|
|
|
|
|
+const safeJsonParse = <T,>(data: string | null, fallback: T): T => {
|
|
|
|
|
+ if (!data) return fallback;
|
|
|
|
|
+ try {
|
|
|
|
|
+ return JSON.parse(data) as T;
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ console.warn('Failed to parse JSON data');
|
|
|
|
|
+ return fallback;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const session = safeJsonParse(sessionData, null);
|
|
|
|
|
+if (!session) {
|
|
|
|
|
+ // Handle invalid session
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 4. Sensitive Data in sessionStorage
|
|
|
|
|
+
|
|
|
|
|
+**File:** `src/pages/IntegrationsRedirect.tsx:201, 513, 517, 525, 536, 540`
|
|
|
|
|
+
|
|
|
|
|
+**Vulnerable Code:**
|
|
|
|
|
+```tsx
|
|
|
|
|
+sessionStorage.setItem('pending_install', JSON.stringify(pendingInstall));
|
|
|
|
|
+sessionStorage.setItem('pending_integration_redirect', redirectUrl);
|
|
|
|
|
+const cachedShopInfo = sessionStorage.getItem(`shop_info_${srInstall}`);
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Risk:** sessionStorage is accessible to any JavaScript running on the page. An XSS vulnerability would allow attackers to steal installation IDs, OAuth tokens, and redirect URLs.
|
|
|
|
|
+
|
|
|
|
|
+**Remediation:**
|
|
|
|
|
+- Use httpOnly cookies for sensitive tokens (requires backend changes)
|
|
|
|
|
+- Minimize what's stored in sessionStorage
|
|
|
|
|
+- Implement Content Security Policy to mitigate XSS
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 5. No CSRF Token Validation
|
|
|
|
|
+
|
|
|
|
|
+**Files:** All API calls throughout the codebase
|
|
|
|
|
+**Example:** `src/components/WooCommerceConnect.tsx:303-318`
|
|
|
|
|
+
|
|
|
|
|
+**Vulnerable Code:**
|
|
|
|
|
+```tsx
|
|
|
|
|
+const response = await fetch(`${API_URL}/oauth-woocommerce?action=connect_manual`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Authorization': `Bearer ${session.session.access_token}`,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ // No CSRF token
|
|
|
|
|
+ },
|
|
|
|
|
+ body: JSON.stringify({...})
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Risk:** Cross-Site Request Forgery attacks could allow attackers to perform actions on behalf of authenticated users by tricking them into visiting malicious websites.
|
|
|
|
|
+
|
|
|
|
|
+**Remediation:**
|
|
|
|
|
+- Implement CSRF tokens in backend and include in all state-changing requests
|
|
|
|
|
+- Use SameSite=Strict cookie attribute for session cookies
|
|
|
|
|
+- Add custom headers that cannot be set cross-origin
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 6. XSS via dangerouslySetInnerHTML
|
|
|
|
|
+
|
|
|
|
|
+**File:** `src/components/WooCommerceConnect.tsx:591, 595, 609, 613`
|
|
|
|
|
+
|
|
|
|
|
+**Vulnerable Code:**
|
|
|
|
|
+```tsx
|
|
|
|
|
+<span dangerouslySetInnerHTML={{ __html: t('integrations.woocommerceConnect.keyStep2') }} />
|
|
|
|
|
+<span dangerouslySetInnerHTML={{ __html: t('integrations.woocommerceConnect.keyStep3') }} />
|
|
|
|
|
+<span dangerouslySetInnerHTML={{ __html: t('integrations.woocommerceConnect.keyStep5') }} />
|
|
|
|
|
+<span dangerouslySetInnerHTML={{ __html: t('integrations.woocommerceConnect.keyStep6') }} />
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Risk:** If translation files are compromised or if any user-controlled data ends up in translation strings, XSS attacks become possible.
|
|
|
|
|
+
|
|
|
|
|
+**Remediation:**
|
|
|
|
|
+```tsx
|
|
|
|
|
+// Option 1: Use text content (preferred)
|
|
|
|
|
+<span>{t('integrations.woocommerceConnect.keyStep2')}</span>
|
|
|
|
|
+
|
|
|
|
|
+// Option 2: Use a sanitizer library like DOMPurify
|
|
|
|
|
+import DOMPurify from 'dompurify';
|
|
|
|
|
+<span dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(t('...')) }} />
|
|
|
|
|
+
|
|
|
|
|
+// Option 3: Use Trans component from react-i18next for rich text
|
|
|
|
|
+import { Trans } from 'react-i18next';
|
|
|
|
|
+<Trans i18nKey="integrations.woocommerceConnect.keyStep2">
|
|
|
|
|
+ Go to <strong>Settings</strong>
|
|
|
|
|
+</Trans>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 7. Missing Input Validation on API Responses
|
|
|
|
|
+
|
|
|
|
|
+**File:** `src/components/WooCommerceConnect.tsx:170, 213`
|
|
|
|
|
+
|
|
|
|
|
+**Vulnerable Code:**
|
|
|
|
|
+```tsx
|
|
|
|
|
+const response = await fetch(`${API_URL}/api/phone-numbers?group_by=cities&country=${selectedCountry}`);
|
|
|
|
|
+if (response.ok) {
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+ setCities(data.cities || []); // No validation of cities array contents
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Risk:** If the backend is compromised, malicious data could be injected into dropdown menus and subsequently used in API calls.
|
|
|
|
|
+
|
|
|
|
|
+**Remediation:**
|
|
|
|
|
+```tsx
|
|
|
|
|
+const isValidCity = (city: unknown): city is string =>
|
|
|
|
|
+ typeof city === 'string' && city.length > 0 && city.length < 100;
|
|
|
|
|
+
|
|
|
|
|
+const data = await response.json();
|
|
|
|
|
+const validCities = Array.isArray(data.cities)
|
|
|
|
|
+ ? data.cities.filter(isValidCity)
|
|
|
|
|
+ : [];
|
|
|
|
|
+setCities(validCities);
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 8. Inconsistent URL Encoding
|
|
|
|
|
+
|
|
|
|
|
+**Files:** Various API calls throughout codebase
|
|
|
|
|
+
|
|
|
|
|
+**Issue:** Some URL parameters are encoded with `encodeURIComponent`, others are not, creating inconsistent security posture.
|
|
|
|
|
+
|
|
|
|
|
+**Remediation:** Create a utility function for building API URLs:
|
|
|
|
|
+```tsx
|
|
|
|
|
+const buildApiUrl = (endpoint: string, params: Record<string, string>): string => {
|
|
|
|
|
+ const url = new URL(endpoint, API_URL);
|
|
|
|
|
+ Object.entries(params).forEach(([key, value]) => {
|
|
|
|
|
+ url.searchParams.set(key, value); // Automatically encodes
|
|
|
|
|
+ });
|
|
|
|
|
+ return url.toString();
|
|
|
|
|
+};
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## Medium Severity Vulnerabilities
|
|
|
|
|
+
|
|
|
|
|
+### 9. JWT Tokens in localStorage
|
|
|
|
|
+
|
|
|
|
|
+**Files:**
|
|
|
|
|
+- `src/components/context/AuthContext.tsx:102, 145`
|
|
|
|
|
+- `src/pages/IntegrationsRedirect.tsx:596`
|
|
|
|
|
+
|
|
|
|
|
+**Vulnerable Code:**
|
|
|
|
|
+```tsx
|
|
|
|
|
+localStorage.setItem("session_data", JSON.stringify(session_data));
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Risk:** localStorage is persistent and accessible to any XSS payload. Unlike sessionStorage, data persists across browser sessions, increasing the window of opportunity for token theft.
|
|
|
|
|
+
|
|
|
|
|
+**Remediation:**
|
|
|
|
|
+- Store tokens in httpOnly cookies (backend implementation required)
|
|
|
|
|
+- Use Supabase's built-in session management
|
|
|
|
|
+- If localStorage must be used, implement token rotation and short expiration times
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 10. Unvalidated Redirect from sessionStorage
|
|
|
|
|
+
|
|
|
|
|
+**File:** `src/components/context/AuthContext.tsx:148-151`
|
|
|
|
|
+
|
|
|
|
|
+**Vulnerable Code:**
|
|
|
|
|
+```tsx
|
|
|
|
|
+const pendingRedirect = sessionStorage.getItem('pending_integration_redirect');
|
|
|
|
|
+if (pendingRedirect) {
|
|
|
|
|
+ sessionStorage.removeItem('pending_integration_redirect');
|
|
|
|
|
+ check_auth(pendingRedirect); // Passed directly to navigation
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Risk:** If sessionStorage is compromised, attackers could redirect users to arbitrary paths after authentication.
|
|
|
|
|
+
|
|
|
|
|
+**Remediation:**
|
|
|
|
|
+```tsx
|
|
|
|
|
+const ALLOWED_REDIRECT_PATHS = ['/dashboard', '/integrations', '/webshops', '/call-logs'];
|
|
|
|
|
+
|
|
|
|
|
+const isAllowedRedirect = (path: string): boolean => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const url = new URL(path, window.location.origin);
|
|
|
|
|
+ return url.origin === window.location.origin &&
|
|
|
|
|
+ ALLOWED_REDIRECT_PATHS.some(allowed => url.pathname.startsWith(allowed));
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const pendingRedirect = sessionStorage.getItem('pending_integration_redirect');
|
|
|
|
|
+if (pendingRedirect && isAllowedRedirect(pendingRedirect)) {
|
|
|
|
|
+ sessionStorage.removeItem('pending_integration_redirect');
|
|
|
|
|
+ check_auth(pendingRedirect);
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 11. Incomplete URL Validation (SSRF Risk)
|
|
|
|
|
+
|
|
|
|
|
+**File:** `src/components/WooCommerceConnect.tsx:267-287`
|
|
|
|
|
+
|
|
|
|
|
+**Vulnerable Code:**
|
|
|
|
|
+```tsx
|
|
|
|
|
+try {
|
|
|
|
|
+ const url = new URL(normalizedUrl);
|
|
|
|
|
+ if (url.protocol !== 'https:') {
|
|
|
|
|
+ setError(t('integrations.woocommerceConnect.errors.httpsRequired'));
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+} catch (e) {
|
|
|
|
|
+ setError(t('integrations.woocommerceConnect.errors.invalidUrl'));
|
|
|
|
|
+ return;
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Risk:** Only validates protocol. Doesn't prevent:
|
|
|
|
|
+- Internal endpoints: `https://localhost/`, `https://127.0.0.1/`
|
|
|
|
|
+- Private IP ranges: `https://192.168.1.1/`, `https://10.0.0.1/`
|
|
|
|
|
+- Cloud metadata endpoints: `https://169.254.169.254/`
|
|
|
|
|
+
|
|
|
|
|
+**Remediation:**
|
|
|
|
|
+```tsx
|
|
|
|
|
+const isPublicUrl = (urlString: string): boolean => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const url = new URL(urlString);
|
|
|
|
|
+ const hostname = url.hostname.toLowerCase();
|
|
|
|
|
+
|
|
|
|
|
+ // Block localhost
|
|
|
|
|
+ if (hostname === 'localhost' || hostname === '127.0.0.1') return false;
|
|
|
|
|
+
|
|
|
|
|
+ // Block private IP ranges
|
|
|
|
|
+ const ipPattern = /^(\d{1,3}\.){3}\d{1,3}$/;
|
|
|
|
|
+ if (ipPattern.test(hostname)) {
|
|
|
|
|
+ const parts = hostname.split('.').map(Number);
|
|
|
|
|
+ if (parts[0] === 10) return false; // 10.x.x.x
|
|
|
|
|
+ if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return false; // 172.16-31.x.x
|
|
|
|
|
+ if (parts[0] === 192 && parts[1] === 168) return false; // 192.168.x.x
|
|
|
|
|
+ if (parts[0] === 169 && parts[1] === 254) return false; // Link-local
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return url.protocol === 'https:';
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 12. Full Query String Stored Without Validation
|
|
|
|
|
+
|
|
|
|
|
+**File:** `src/pages/IntegrationsRedirect.tsx:516-517`
|
|
|
|
|
+
|
|
|
|
|
+**Vulnerable Code:**
|
|
|
|
|
+```tsx
|
|
|
|
|
+const redirectUrl = `/integrations?${searchParams.toString()}`;
|
|
|
|
|
+sessionStorage.setItem('pending_integration_redirect', redirectUrl);
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Risk:** Arbitrary query parameters are preserved and could be used for parameter pollution or injection after authentication.
|
|
|
|
|
+
|
|
|
|
|
+**Remediation:** Only preserve known, safe parameters:
|
|
|
|
|
+```tsx
|
|
|
|
|
+const SAFE_PARAMS = ['platform', 'store_id', 'action'];
|
|
|
|
|
+const safeParams = new URLSearchParams();
|
|
|
|
|
+SAFE_PARAMS.forEach(param => {
|
|
|
|
|
+ const value = searchParams.get(param);
|
|
|
|
|
+ if (value) safeParams.set(param, value);
|
|
|
|
|
+});
|
|
|
|
|
+const redirectUrl = `/integrations?${safeParams.toString()}`;
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 13. React useEffect Dependency Issues
|
|
|
|
|
+
|
|
|
|
|
+**File:** `src/components/PrivateRoute.tsx:13-15`
|
|
|
|
|
+
|
|
|
|
|
+**Issue:** Missing dependencies in useEffect hooks can cause stale closures and unpredictable behavior.
|
|
|
|
|
+
|
|
|
|
|
+**Remediation:** Use ESLint rule `react-hooks/exhaustive-deps` and fix all warnings.
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## Low Severity Vulnerabilities
|
|
|
|
|
+
|
|
|
|
|
+### 14. Hardcoded Fallback API URL
|
|
|
|
|
+
|
|
|
|
|
+**File:** `src/lib/config.ts:2`
|
|
|
|
|
+
|
|
|
|
|
+**Vulnerable Code:**
|
|
|
|
|
+```tsx
|
|
|
|
|
+export const API_URL = import.meta.env.VITE_API_URL || 'https://ztklqodcdjeqpsvhlpud.supabase.co/functions/v1';
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Risk:** Exposes Supabase project reference. If environment variable is not set, production credentials are used.
|
|
|
|
|
+
|
|
|
|
|
+**Remediation:**
|
|
|
|
|
+```tsx
|
|
|
|
|
+const API_URL = import.meta.env.VITE_API_URL;
|
|
|
|
|
+if (!API_URL) {
|
|
|
|
|
+ throw new Error('VITE_API_URL environment variable is required');
|
|
|
|
|
+}
|
|
|
|
|
+export { API_URL };
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 15. Console Logging of Sensitive Data
|
|
|
|
|
+
|
|
|
|
|
+**Files:** Multiple locations throughout codebase
|
|
|
|
|
+
|
|
|
|
|
+**Example:**
|
|
|
|
|
+```tsx
|
|
|
|
|
+console.error('Auth check failed:', err); // Could log sensitive info
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Risk:** In production, console logs may be captured by monitoring tools or accessible via browser developer tools.
|
|
|
|
|
+
|
|
|
|
|
+**Remediation:**
|
|
|
|
|
+- Use a logging library that can be disabled in production
|
|
|
|
|
+- Sanitize error messages before logging
|
|
|
|
|
+- Implement structured logging that excludes sensitive fields
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 16. No Client-Side Rate Limiting
|
|
|
|
|
+
|
|
|
|
|
+**Issue:** All API calls lack debouncing or throttling.
|
|
|
|
|
+
|
|
|
|
|
+**Risk:** Users could accidentally or intentionally spam API endpoints.
|
|
|
|
|
+
|
|
|
|
|
+**Remediation:**
|
|
|
|
|
+```tsx
|
|
|
|
|
+import { useMemo } from 'react';
|
|
|
|
|
+import { debounce } from 'lodash';
|
|
|
|
|
+
|
|
|
|
|
+const debouncedSync = useMemo(
|
|
|
|
|
+ () => debounce(handleSync, 1000, { leading: true, trailing: false }),
|
|
|
|
|
+ [handleSync]
|
|
|
|
|
+);
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 17. Missing Security Headers Recommendation
|
|
|
|
|
+
|
|
|
|
|
+**Issue:** No Content Security Policy or other security headers configured.
|
|
|
|
|
+
|
|
|
|
|
+**Remediation:** Add to `index.html` or configure via server:
|
|
|
|
|
+```html
|
|
|
|
|
+<meta http-equiv="Content-Security-Policy" content="
|
|
|
|
|
+ default-src 'self';
|
|
|
|
|
+ script-src 'self' 'unsafe-inline';
|
|
|
|
|
+ style-src 'self' 'unsafe-inline';
|
|
|
|
|
+ img-src 'self' data: https:;
|
|
|
|
|
+ connect-src 'self' https://*.supabase.co;
|
|
|
|
|
+">
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## Remediation Priority
|
|
|
|
|
+
|
|
|
|
|
+| Priority | Issue | Effort | Impact |
|
|
|
|
|
+|----------|-------|--------|--------|
|
|
|
|
|
+| 1 | Open Redirect in Index.tsx | Low | Critical |
|
|
|
|
|
+| 2 | dangerouslySetInnerHTML XSS | Low | High |
|
|
|
|
|
+| 3 | JSON.parse error handling | Medium | High |
|
|
|
|
|
+| 4 | SSRF URL validation | Low | Medium |
|
|
|
|
|
+| 5 | Redirect validation | Low | Medium |
|
|
|
|
|
+| 6 | CSRF tokens | High | High |
|
|
|
|
|
+| 7 | httpOnly cookies | High | Medium |
|
|
|
|
|
+| 8 | useEffect dependencies | Low | Medium |
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## Appendix: Files Requiring Changes
|
|
|
|
|
+
|
|
|
|
|
+| File | Issues |
|
|
|
|
|
+|------|--------|
|
|
|
|
|
+| `src/pages/Index.tsx` | #1 Open Redirect |
|
|
|
|
|
+| `src/components/PrivateRoute.tsx` | #2, #13 Auth check, useEffect deps |
|
|
|
|
|
+| `src/pages/IntegrationsRedirect.tsx` | #3, #4, #10, #12 JSON.parse, sessionStorage, redirects |
|
|
|
|
|
+| `src/components/context/AuthContext.tsx` | #3, #9, #10 JSON.parse, localStorage, redirects |
|
|
|
|
|
+| `src/components/WooCommerceConnect.tsx` | #5, #6, #7, #8, #11 CSRF, XSS, validation |
|
|
|
|
|
+| `src/lib/config.ts` | #14 Hardcoded URL |
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## Fixes Applied (2025-12-12)
|
|
|
|
|
+
|
|
|
|
|
+The following vulnerabilities have been remediated:
|
|
|
|
|
+
|
|
|
|
|
+| Issue | Status | File Changed |
|
|
|
|
|
+|-------|--------|--------------|
|
|
|
|
|
+| #1 Open Redirect in Index.tsx | **FIXED** | `src/pages/Index.tsx` - Added `isValidShopifyDomain()` validation |
|
|
|
|
|
+| #2 Auth check dependency | **FIXED** | `src/components/PrivateRoute.tsx` - Added `useCallback` wrapper |
|
|
|
|
|
+| #3 JSON.parse crashes | **FIXED** | `src/lib/utils.ts` - Added `safeJsonParse()` utility |
|
|
|
|
|
+| #6 dangerouslySetInnerHTML XSS | **FIXED** | `src/components/WooCommerceConnect.tsx` - Replaced with `Trans` component |
|
|
|
|
|
+| #10 Redirect validation | **FIXED** | `src/components/context/AuthContext.tsx` - Added `isAllowedRedirectPath()` |
|
|
|
|
|
+| #11 SSRF URL validation | **FIXED** | `src/components/WooCommerceConnect.tsx` - Added private IP blocking |
|
|
|
|
|
+| #12 Query param injection | **FIXED** | `src/pages/IntegrationsRedirect.tsx` - Added `createSafeRedirectUrl()` |
|
|
|
|
|
+| #13 useEffect dependencies | **FIXED** | `src/components/PrivateRoute.tsx` - Proper dependency array |
|
|
|
|
|
+| #14 Hardcoded URL | **FIXED** | `src/lib/config.ts` - Now throws if env var not set |
|
|
|
|
|
+
|
|
|
|
|
+### Remaining Items (Require Backend Changes)
|
|
|
|
|
+
|
|
|
|
|
+| Issue | Status | Notes |
|
|
|
|
|
+|-------|--------|-------|
|
|
|
|
|
+| #5 CSRF tokens | NOT FIXED | Requires backend implementation |
|
|
|
|
|
+| #9 httpOnly cookies | NOT FIXED | Requires backend to set cookies |
|
|
|
|
|
+| #15-17 Low priority | NOT FIXED | Console logging, rate limiting, CSP headers |
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## References
|
|
|
|
|
+
|
|
|
|
|
+- [OWASP Top 10 2021](https://owasp.org/Top10/)
|
|
|
|
|
+- [React Security Best Practices](https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml)
|
|
|
|
|
+- [OWASP CSRF Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html)
|
|
|
|
|
+- [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)
|