# SweetHome — Full Security Audit & Pentest Report **Project:** SweetHome Next.js Real Estate Application **Date:** June 16, 2026 **Scope:** Client-side source code analysis (full white-box) --- ## Executive Summary | Severity | Count | Key Issues | |----------|-------|------------| | 🔴 **CRITICAL** | 8 | JWT in localStorage, XSS via Leaflet, OTP in console, no middleware, passwords in URL params, IDOR on edit APIs | | 🟠 **HIGH** | 12 | Client-only role checks, HTTP endpoints, credential logging, mass assignment, error leakage, no CSP | | 🟡 **MEDIUM** | 15 | Hardcoded IPs, stale localStorage keys, weak validation, no security headers, FCM token exposure | | 🟢 **LOW** | 6 | Missing maxLength, base64 avatar in storage, weak email regex | --- ## 🔴 CRITICAL FINDINGS ### C1. JWT Stored in `localStorage` (No httpOnly Cookie) **Files:** `app/services/AuthService.js:23-38`, `app/settings/page.js:335-339`, `app/payments/page.js:185-189` ```javascript const TOKEN_KEY = 'auth_token'; const USER_KEY = 'cached_user'; const AuthService = Object.freeze({ addToken(token) { localStorage.setItem(TOKEN_KEY, token); // <-- XSS-accessible }, getToken() { return localStorage.getItem(TOKEN_KEY); // <-- XSS-accessible }, deleteToken() { localStorage.removeItem(TOKEN_KEY); localStorage.removeItem(USER_KEY); }, ``` **Impact:** Any XSS vulnerability gives attackers permanent token theft. No httpOnly, Secure, or SameSite protection. Token persists across tabs with no client-side expiration enforcement. **Multiple fallback keys** (fragmented storage): - `auth_token` (primary) - `token`, `accessToken`, `authToken` (fallbacks in settings, payments, reservations pages) - `cached_user` (user profile data) - `userProfile`, `userAvatar` (profile page - base64 images!) --- ### C2. Stored XSS via Leaflet Popup HTML Construction **File:** `app/components/PropertyMapWithMarkers.js:51-65` ```javascript const popupContent = `

${property.title || 'عقار'}

${property.address || property.location?.address || ''}

${property.images && property.images.length > 0 ? `${property.title}` : '' }
`; marker.bindPopup(popupContent); ``` **Impact:** Property data (title, address, image URLs) from API is interpolated directly into HTML without sanitization. A malicious owner or compromised API can execute arbitrary JavaScript in every viewer's session. `onerror` handler on `` provides an additional execution vector. **Also affected:** `app/components/home/PropertyMap.js:227` — `property.priceUSD` interpolated into `L.divIcon({ html: ... })`. --- ### C3. OTP Verification Code Logged to Browser Console **File:** `app/login/page.js:200-203` ```javascript console.log("[OTP] Verifying code:", otpCode); // <-- OTP in plaintext console.log("[OTP] Verify response status:", result.status); ``` **Impact:** OTP codes are one-time passwords. Browser extensions, devtools monitors, or error-logging services can capture them. Full account takeover via credential + OTP. --- ### C4. No Server-Side Route Middleware **No `middleware.js` file exists** in the project. Every protected route relies on **client-side only** checks: ```javascript // app/ClientLayout.js:74-86 — Role read from JWT, no server verification useEffect(() => { const authUser = AuthService.getUser(); if (authUser) { setUser({ role: AuthService.isOwner() ? UserRole.OWNER : UserRole.CUSTOMER }); } }, [pathname]); ``` **Impact:** Routes like `/owner/*`, `/settings`, `/profile`, `/payments`, `/reservations` have zero server-side protection. If JS fails to load, pages render unauthenticated. --- ### C5. Passwords Sent as URL Query Parameters **File:** `app/utils/api.js:1082-1088` ```javascript export async function changePassword(oldPassword, newPassword) { return apiFetch( `/User/ChangePassword?oldPassword=${encodeURIComponent(oldPassword)}&newPassword=${encodeURIComponent(newPassword)}`, { method: "PUT" }, ); } export async function deleteMyAccount(password) { return apiFetch( `/User/DeleteMyAccount?password=${encodeURIComponent(password)}`, { method: "DELETE" }, ); } ``` **Impact:** Passwords in URL query parameters are logged by web servers, proxies, browser history, and the `apiFetch` `console.log("API Body:", ...)` at `api.js:445`. Also leaked via `Referer` header. --- ### C6. IDOR — No Ownership Verification on Property Edit/Status APIs **File:** `app/utils/api.js:691-732` ```javascript editRentProperty(id, data) // PUT /RentProperties/EditRentProperty/${id} editSaleProperty(id, data) // PUT /SaleProperties/EditSaleProperty/${id} updateRentPropertyStatus(id, status) // PUT /RentProperties/UpdateStatus/${id} updateSalePropertyStatus(id, status) // PUT /SaleProperties/UpdateStatus/${id} ``` **Impact:** Any authenticated user can edit or deactivate **any** property by guessing/iterating IDs. The `GetMyRentListings` fetch filters to "my" properties, but the edit API has no server-side ownership check. --- ### C7. IDOR — Reservation Confirmation Without Ownership Check **File:** `app/owner/reservations/page.js:1309-1332` ```javascript const handleConfirm = async (r) => { setActionLoadingId(r.id); const res = await API( AuthService.getToken(), 'PUT', `/Reservations/OwnerConfirmReservation/owner-confirm/${r.id}` ); ``` **Impact:** Any owner can confirm/reject **any** reservation by ID, regardless of whether they own the property. Combined with C6, an attacker could spam-confirm reservations across the platform. --- ### C8. IDOR — Profile APIs Expose Any User's Data **File:** `app/utils/api.js:670-676` ```javascript getCustomerByUserId(userId) // GET /Customer/GetByUserId/${userId} getOwnerByUserId(userId) // GET /Owner/GetByUserId/${userId} ``` **Impact:** If the backend doesn't verify the requesting user matches the target `userId`, this allows enumerating all user profiles (name, email, phone, WhatsApp, national number). --- ## 🟠 HIGH FINDINGS ### H1. Client-Side Only Role Checks (JWT Decoded Without Signature Verification) **File:** `app/services/AuthService.js:66-75` ```javascript decodeToken() { const token = this.getToken(); if (!token) return null; try { const payload = token.split('.')[1]; return JSON.parse(atob(payload)); // <-- No signature verification, just base64 } catch { return null; } }, isOwner() { return this.getRoles().includes('Owner'); }, ``` **Impact:** A user can trivially craft/modify a JWT in localStorage to add the `Owner` role. While server-side validation should catch this, the client-side UI fully trusts the decoded token. **All owner pages use this pattern:** ```javascript if (!AuthService.isOwner()) { router.push('/'); return; } ``` --- ### H2. HTTP (Unencrypted) API Endpoints Used in Production **File:** Multiple locations | File | URL | |------|-----| | `app/utils/api.js:380` | `http://45.93.137.91/api` (REPORT_API_BASE) | | `app/settings/page.js:351` | `http://45.93.137.91/api/Reports/SendGeneralReport` | | `app/privacy/page.js:114` | `http://45.93.137.91/api` | | `app/payments/page.js:198` | `http://45.93.137.91/api/Customer/GetMyTransaction` | | `app/property/[id]/page.js:71` | `http://45.93.137.91${p.image}` | **Impact:** Credentials, tokens, and personal data sent over HTTP are visible in plaintext to anyone on the network (MITM). The main API uses HTTPS (`*.nip.io`), but the Report API and several image fetches use raw HTTP. --- ### H3. User Credentials Logged to Console **File:** `app/login/page.js:87-92` ```javascript console.log("[Login] Attempting login via", loginMethod, ":", formData.credential); ``` **File:** `app/utils/api.js:443-445` — Full API request bodies logged: ```javascript console.log("API Request:", url); console.log("API Method:", options.method || "GET"); console.log("API Body:", hasBody ? options.body : null); // <-- may contain passwords ``` **Impact:** Login credentials, password change payloads, and any API body is visible in browser console. Any extension or error-logging service can capture this. --- ### H4. No CSP or Security Headers **File:** `next.config.mjs` ```javascript const nextConfig = { reactCompiler: true, images: { remotePatterns: [ ... ] }, }; export default nextConfig; ``` **Missing headers:** - `Content-Security-Policy` — would prevent XSS (C2) - `Strict-Transport-Security` (HSTS) — would enforce HTTPS - `X-Content-Type-Options: nosniff` - `X-Frame-Options: DENY` — clickjacking protection - `Referrer-Policy` --- ### H5. Mass Assignment via Spread Operator **File:** `app/owner/properties/page.js:1592` ```javascript const updatedProperty = { ...property, ...formData }; ``` **Impact:** User-controlled `formData` is spread directly into the property object. Unexpected fields (`status`, `ownerId`, `role`) could overwrite protected properties if the backend doesn't sanitize. --- ### H6. Error Messages Leak Internal Details to Users **12 affected files.** Pattern: ```javascript toast.error(err.message || 'فشل تحميل البيانات'); ``` **Files:** `account-verification/page.js`, `change-password/page.js`, `notifications/page.js`, `reports/page.js`, `payments/page.js`, `reservations/page.js`, `PropertyDetail.js`, `owner/*/page.js` **Impact:** Raw `err.message` may contain API paths, stack traces, or database errors. Shows internal implementation details to end users. --- ### H7. Firebase & VAPID Keys Hardcoded Client-Side **File:** `app/utils/firebase.js:5-11` ```javascript const firebaseConfig = { apiKey: "AIzaSyBZV7KBLRJSTApahfrO8lBesmIM3zNRSaY", authDomain: "sweet-home-b2766.firebaseapp.com", projectId: "sweet-home-b2766", // ... vapidKey: "BGZ4Fo8rRhoTdStLGlCySDZOnAX4ekCA0e3HDWXL5uEi2kOnXynYjbaDbY15002phUrFqxBpPPFHgfH2VhrmFDU", }; ``` **Duplicated in:** `public/firebase-messaging-sw.js:8-14`, `app/components/NotificationHandler.js:70` **Impact:** While Firebase API keys are semi-public, the `projectId` and `storageBucket` exposure enables abuse if Firebase Security Rules are misconfigured. Push notifications can be sent without server authorization. --- ## 🟡 MEDIUM FINDINGS ### M1. Hardcoded Server IP in 10+ Files The IP `45.93.137.91` is hardcoded across the codebase with no environment variable abstraction for many URLs. ### M2. No CSRF Protection No CSRF tokens, no `SameSite` cookie attributes (no cookies used at all), no custom headers for state-changing operations. ### M3. No Rate Limiting or Brute-Force Protection Login functions (`loginWithEmail`, `loginWithPhone`) pass credentials directly without client-side rate limiting or exponential backoff. ### M4. CI/CD Server URL Exposed in Source **File:** `.gitea/workflows/deployer.yaml:14` ```yaml github-server-url: http://45.93.137.91:3000 ``` ### M5. FCM Push Token Logged to Console **File:** `app/utils/firebase.js:45` ```javascript console.log("[FCM] Token:", token); // persistent device identifier ``` ### M6. Owner Properties Cached in localStorage **File:** `app/owner/properties/page.js:1450-1453` ```javascript localStorage.setItem("ownerProperties", JSON.stringify(newProperties)); ``` ### M7. Weak Email Validation Regex **Pattern:** `/^[^\s@]+@[^\s@]+\.[^\s@]+$/` — allows ````, `|`, and special characters in local part. ### M8. Report API Sends Token Over HTTP **File:** `app/settings/page.js:351` — `fetch('http://45.93.137.91/api/Reports/SendGeneralReport', ...)` with Bearer token header. ### M9. No `.env` Files in Repository All environment variables fall back to hardcoded values (IPs, Firebase config). No `.env` or `.env.local` exists. ### M10. User Credential Reflected in OTP UI **File:** `app/login/page.js:612` — `{formData.credential}` rendered in DOM (JSX-escaped, but could aid social engineering). ### M11. Client-Side Only File Upload Validation `file.type.startsWith('image/')` — bypassable. Size limits (2MB/5MB) enforced client-side only. ### M12. All Reservations Loaded Then Filtered Client-Side **File:** `app/reservations/page.js:871-899` — Fetches ALL rent properties via `getRentProperties()` for enrichment; if backend doesn't scope properly, sensitive data leaks. ### M13. Reservation Report Allows Reporting Any Reservation **File:** `app/reservations/page.js:498-543` — No verification the reporter owns the reservation before filing a report. ### M14. Multiple Inconsistent localStorage Keys At least 5 different keys store user data: `auth_token`, `token`, `accessToken`, `authToken`, `user`, `currentUser`, `authUser`, `profile`, `cached_user`. ### M15. i18next Configured to Cache in localStorage **File:** `app/i18n/config.js:373-374` — Language detection data cached in localStorage. --- ## 🟢 LOW FINDINGS | # | Finding | File(s) | |---|---------|---------| | L1 | Missing `maxLength` on text inputs | `reports/page.js`, `profile/page.js`, `login/page.js` | | L2 | Base64 avatar images stored in localStorage | `profile/page.js:163` | | L3 | Weak WhatsApp number sanitization | `PropertyDetail.js:1682` — only `[^0-9]` stripped | | L4 | Profuse console.log throughout codebase | ~30+ files with active console.log statements | | L5 | Business data cached in localStorage | `owner/bookings/page.js`, `owner/profits/page.js`, `owner/calendar/page.js` | | L6 | No JWT library in dependencies (manual `atob` decode) | `package.json` | --- ## Dependencies Analysis **File:** `package.json` | Dependency | Risk | |------------|------| | `next@16.1.6` | Very bleeding-edge; verify official release | | `firebase@^12.11.0` | FCM only; correctly scoped | | `xlsx@^0.18.5` | Used for admin export; no user data flow | | `jspdf@^4.2.1` | Client-side PDF generation; safe | | `html2canvas@^1.4.1` | Screenshot capture; safe | | No `jsonwebtoken`/`jose` | Manual JWT decode with `atob()` — no signature verification | | No `helmet`/`cors`/`csurf` | No security middleware | | No `axios`/`ky` | Raw `fetch` used throughout | --- ## Attack Scenarios ### Scenario 1: Full Account Takeover via XSS + OTP Logging 1. Attacker stores a malicious property with `