Files
SweetHome/security-audit-report.md
mouazkh 199478f237
All checks were successful
Build frontend / build (push) Successful in 1m27s
added the elmeent on the nav and fixed the payments transactions
2026-06-23 23:32:07 +03:00

18 KiB

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

Files: app/services/AuthService.js:23-38, app/settings/page.js:335-339, app/payments/page.js:185-189

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

const popupContent = `
  <div dir="rtl" style="text-align: right; padding: 12px; max-width: 250px;">
    <h3 style="font-weight: bold; font-size: 16px; margin-bottom: 8px; color: #111;">
      ${property.title || 'عقار'}
    </h3>
    <p style="font-size: 14px; color: #666; margin-bottom: 8px;">
      ${property.address || property.location?.address || ''}
    </p>
    ${property.images && property.images.length > 0
      ? `<img src="${property.images[0]}" alt="${property.title}"
           style="width:100%;height:120px;object-fit:cover;border-radius:8px;"
           onerror="this.src='/property-placeholder.jpg'" />`
      : ''
    }
  </div>
`;
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 <img> provides an additional execution vector.

Also affected: app/components/home/PropertyMap.js:227property.priceUSD interpolated into L.divIcon({ html: ... }).


C3. OTP Verification Code Logged to Browser Console

File: app/login/page.js:200-203

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:

// 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

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

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

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

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

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:

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

console.log("[Login] Attempting login via", loginMethod, ":", formData.credential);

File: app/utils/api.js:443-445 — Full API request bodies logged:

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

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

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:

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

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

github-server-url: http://45.93.137.91:3000

M5. FCM Push Token Logged to Console

File: app/utils/firebase.js:45

console.log("[FCM] Token:", token);   // persistent device identifier

M6. Owner Properties Cached in localStorage

File: app/owner/properties/page.js:1450-1453

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:351fetch('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 <script> in the title
  2. Any user viewing the map (PropertyMapWithMarkers.js) executes the script
  3. Script steals JWT from localStorage → full account access
  4. If the victim is logging in, OTP is visible in console → complete takeover

Scenario 2: Mass Property Manipulation via IDOR

  1. Attacker enumerates property IDs via GetRentProperties (public)
  2. Calls editRentProperty(id, { price: 1 }) or updateRentPropertyStatus(id, 5) on any property
  3. No server-side ownership check — all properties editable

Scenario 3: Credential Harvesting via MITM

  1. Attacker on same network intercepts HTTP traffic to http://45.93.137.91/api
  2. Captures Bearer tokens, passwords in URL query params, and personal data
  3. Credentials from changePassword?oldPassword=...&newPassword=... visible in plaintext

Key Remediation Priority Matrix

Priority Fix Effort Impact
P0 Move JWT to httpOnly Secure SameSite cookie High Eliminates XSS token theft
P0 Sanitize Leaflet popup HTML with DOMPurify Low Fixes stored XSS
P0 Remove all console.log statements (especially OTP) Medium Stops data leakage
P0 Add middleware.js for server-side route protection Medium Real auth enforcement
P0 Move passwords from URL params to POST body Low Stops credential leakage
P1 Add CSP and security headers in next.config.mjs Low Mitigates XSS, MITM, clickjacking
P1 Add ID validation on all API calls with path IDs Medium Defense-in-depth vs IDOR
P1 Replace HTTP endpoints with HTTPS Medium Stops MITM credential theft
P1 Add server-side ownership verification on edit APIs High (backend) Primary IDOR fix
P1 Remove err.message from toast.error calls Low Prevents info leakage
P2 Move all hardcoded IPs/secrets to env variables Medium Configurable, secure defaults
P2 Enforce single localStorage key for token Low Consistent, auditable
P2 Add rate limiting on login APIs High (backend) Brute-force protection
P2 Add input maxLength and server-side validation Low Defense-in-depth

Files Reviewed

File Lines Key Security Findings
app/utils/api.js 1088 Token logging, passwords in URL, IDOR on edit APIs, no CSRF
app/services/AuthService.js 151 localStorage JWT, no signature verification, role from decoded JWT
app/login/page.js 800+ OTP in console, credential logging, credential in UI
app/property/[id]/PropertyDetail.js 1700+ Error messages leak, WhatsApp link construction
app/components/PropertyMapWithMarkers.js 100+ Stored XSS via Leaflet popup HTML
app/components/home/PropertyMap.js 250+ HTML injection in DivIcon
app/owner/properties/page.js 1650+ Mass assignment, localStorage caching, console.log
app/owner/reservations/page.js 1350+ IDOR on confirm/reject, localStorage fallback keys
app/reservations/page.js 1000+ Load all rent properties, report any reservation, error leakage
app/settings/page.js 500+ Token over HTTP, multiple localStorage fallback keys
app/profile/page.js 250+ localStorage caching, base64 avatar, console.log
next.config.mjs 12 No security headers, no CSP
package.json No security libraries
app/utils/firebase.js 60 Hardcoded Firebase config, FCM token logging
.gitea/workflows/deployer.yaml 30 CI/CD internal URL exposed
app/payments/page.js 300+ Token from multiple localStorage keys
app/owner/properties/add/page.js 700+ Full payload logged to console
app/register/*/page.js 2000+ Client-only file upload validation
app/change-password/page.js 60+ Error leakage
app/account-verification/page.js 110+ Error leakage

Report generated June 16, 2026. All findings based on static analysis of client-side source code.