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
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
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:227 — property.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 HTTPSX-Content-Type-Options: nosniffX-Frame-Options: DENY— clickjacking protectionReferrer-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: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
- Attacker stores a malicious property with
<script>in the title - Any user viewing the map (
PropertyMapWithMarkers.js) executes the script - Script steals JWT from
localStorage→ full account access - If the victim is logging in, OTP is visible in console → complete takeover
Scenario 2: Mass Property Manipulation via IDOR
- Attacker enumerates property IDs via
GetRentProperties(public) - Calls
editRentProperty(id, { price: 1 })orupdateRentPropertyStatus(id, 5)on any property - No server-side ownership check — all properties editable
Scenario 3: Credential Harvesting via MITM
- Attacker on same network intercepts HTTP traffic to
http://45.93.137.91/api - Captures Bearer tokens, passwords in URL query params, and personal data
- 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.