bulltrackers-module 1.0.615 → 1.0.616
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -529,8 +529,11 @@ const fetchUserVerificationData = async (firestore, userId) => {
|
|
|
529
529
|
* Lookup CID by email (server-side only for security)
|
|
530
530
|
* Searches through all SignedInUsers verification documents to find matching email
|
|
531
531
|
* Returns only the CID and basic verification status - no email exposure
|
|
532
|
+
*
|
|
533
|
+
* NOTE: Multiple Firebase accounts (different UIDs) can access the same CID
|
|
534
|
+
* if their emails are in the verification document's email array.
|
|
532
535
|
*/
|
|
533
|
-
const lookupCidByEmail = async (firestore, userEmail) => {
|
|
536
|
+
const lookupCidByEmail = async (firestore, userEmail, firebaseUid = null) => {
|
|
534
537
|
if (!userEmail) {
|
|
535
538
|
throw new Error('Email is required for lookup');
|
|
536
539
|
}
|
|
@@ -556,6 +559,15 @@ const lookupCidByEmail = async (firestore, userEmail) => {
|
|
|
556
559
|
// Case-insensitive email comparison
|
|
557
560
|
const normalizedEmails = emails.map(e => String(e).toLowerCase().trim());
|
|
558
561
|
if (normalizedEmails.includes(normalizedEmail)) {
|
|
562
|
+
// Optional: If Firebase UID is provided, validate it's in the firebaseUids array (if it exists)
|
|
563
|
+
// This provides additional security but doesn't block access if UID tracking isn't set up yet
|
|
564
|
+
if (firebaseUid && verificationData.firebaseUids && Array.isArray(verificationData.firebaseUids)) {
|
|
565
|
+
if (!verificationData.firebaseUids.includes(firebaseUid)) {
|
|
566
|
+
// UID not in list - log warning but still allow access (email is the source of truth)
|
|
567
|
+
console.warn(`[lookupCidByEmail] Email ${userEmail} matches CID ${userDoc.id} but Firebase UID ${firebaseUid} not in firebaseUids array`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
559
571
|
// Found match - return only CID and verification status (no email)
|
|
560
572
|
return {
|
|
561
573
|
cid: verificationData.etoroCID || Number(userDoc.id),
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
* Middleware to resolve the effective user ID, handling Developer Impersonation.
|
|
3
3
|
* Sets req.targetUserId, req.isImpersonating, and req.actualUserId.
|
|
4
4
|
*
|
|
5
|
+
* SECURITY: This middleware validates that the authenticated Firebase user owns the requested CID.
|
|
6
|
+
* It prevents IDOR (Insecure Direct Object Reference) attacks by:
|
|
7
|
+
* 1. Looking up the CID associated with the Firebase authenticated user's email
|
|
8
|
+
* 2. Forcing req.targetUserId to be the authenticated user's CID (unless developer impersonation)
|
|
9
|
+
* 3. Ignoring any userCid provided in headers/query/body if it doesn't match the authenticated user
|
|
10
|
+
*
|
|
5
11
|
* Public routes (that don't require authentication):
|
|
6
12
|
* - /watchlists/public
|
|
7
13
|
* - /popular-investors/trending
|
|
@@ -9,7 +15,7 @@
|
|
|
9
15
|
* - /popular-investors/master-list
|
|
10
16
|
* - /popular-investors/search
|
|
11
17
|
*/
|
|
12
|
-
const { isDeveloper } = require('../helpers/data-fetchers/firestore.js');
|
|
18
|
+
const { isDeveloper, lookupCidByEmail } = require('../helpers/data-fetchers/firestore.js');
|
|
13
19
|
|
|
14
20
|
// List of public routes that don't require userCid
|
|
15
21
|
// Also includes routes that use Firebase Auth token authentication (like /verification/lookup)
|
|
@@ -60,6 +66,12 @@ const isPublicRoute = (path, originalUrl) => {
|
|
|
60
66
|
|
|
61
67
|
const resolveUserIdentity = async (req, res, next) => {
|
|
62
68
|
try {
|
|
69
|
+
const db = req.app.locals?.db || req.dependencies?.db;
|
|
70
|
+
if (!db) {
|
|
71
|
+
console.error('[IdentityMiddleware] Database not available');
|
|
72
|
+
return res.status(500).json({ error: "Internal server error" });
|
|
73
|
+
}
|
|
74
|
+
|
|
63
75
|
// Check if this is a public route (check both path and originalUrl for Express routing)
|
|
64
76
|
const isPublic = isPublicRoute(req.path, req.originalUrl);
|
|
65
77
|
|
|
@@ -68,45 +80,103 @@ const resolveUserIdentity = async (req, res, next) => {
|
|
|
68
80
|
const hasFirebaseAuth = req.headers.authorization &&
|
|
69
81
|
req.headers.authorization.startsWith('Bearer ');
|
|
70
82
|
|
|
71
|
-
//
|
|
72
|
-
|
|
83
|
+
// SECURITY FIX: If Firebase Auth is present, validate the user owns the requested CID
|
|
84
|
+
// NOTE: Multiple Firebase accounts (different UIDs) can access the same CID
|
|
85
|
+
// if their emails are in the verification document's email array
|
|
86
|
+
let authenticatedUserCid = null;
|
|
87
|
+
if (req.firebaseUser && req.firebaseUser.email) {
|
|
88
|
+
try {
|
|
89
|
+
// Look up the CID associated with the authenticated Firebase user's email
|
|
90
|
+
// Pass Firebase UID for optional validation (email is the source of truth)
|
|
91
|
+
const userData = await lookupCidByEmail(db, req.firebaseUser.email, req.firebaseUser.uid);
|
|
92
|
+
if (userData && userData.cid) {
|
|
93
|
+
authenticatedUserCid = String(userData.cid);
|
|
94
|
+
console.log(`[Identity] Authenticated user ${req.firebaseUser.email} (UID: ${req.firebaseUser.uid}) owns CID ${authenticatedUserCid}`);
|
|
95
|
+
} else {
|
|
96
|
+
console.warn(`[Identity] No CID found for authenticated user ${req.firebaseUser.email}`);
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error(`[Identity] Error looking up CID for ${req.firebaseUser.email}:`, error);
|
|
100
|
+
// Continue - will fail validation below if CID is required
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 1. Identify the requested user ID (from params/headers/body)
|
|
105
|
+
const requestedUserId = req.query.userCid || req.body.userCid || req.headers['x-user-cid'];
|
|
73
106
|
|
|
74
|
-
// For public routes
|
|
75
|
-
if (!
|
|
107
|
+
// For public routes, userCid is optional
|
|
108
|
+
if (!requestedUserId && !isPublic && !hasFirebaseAuth) {
|
|
76
109
|
return res.status(400).json({ error: "Missing user identification (userCid)" });
|
|
77
110
|
}
|
|
78
111
|
|
|
79
|
-
// If no user ID provided and it's a public route
|
|
80
|
-
if (!
|
|
112
|
+
// If no user ID provided and it's a public route, skip identity resolution
|
|
113
|
+
if (!requestedUserId && isPublic) {
|
|
81
114
|
req.actualUserId = null;
|
|
82
115
|
req.targetUserId = null;
|
|
83
116
|
req.isImpersonating = false;
|
|
84
117
|
return next();
|
|
85
118
|
}
|
|
86
119
|
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
120
|
+
// SECURITY FIX: If Firebase Auth is present, enforce ownership
|
|
121
|
+
// The authenticated user can only access their own CID (unless developer impersonation)
|
|
122
|
+
if (authenticatedUserCid) {
|
|
123
|
+
// If a CID was requested but doesn't match the authenticated user's CID, reject (unless developer impersonation)
|
|
124
|
+
if (requestedUserId && requestedUserId !== authenticatedUserCid) {
|
|
125
|
+
// Check if this is a developer impersonation request
|
|
126
|
+
const impersonateId = req.headers['x-impersonate-cid'] || req.query.impersonateCid;
|
|
127
|
+
|
|
128
|
+
if (impersonateId && impersonateId === requestedUserId) {
|
|
129
|
+
// This is an explicit impersonation request - verify developer status
|
|
130
|
+
const isDev = await isDeveloper(db, authenticatedUserCid);
|
|
131
|
+
if (isDev) {
|
|
132
|
+
req.actualUserId = authenticatedUserCid;
|
|
133
|
+
req.targetUserId = impersonateId;
|
|
134
|
+
req.isImpersonating = true;
|
|
135
|
+
console.log(`[Identity] Dev ${authenticatedUserCid} is impersonating ${impersonateId}`);
|
|
136
|
+
return next();
|
|
137
|
+
} else {
|
|
138
|
+
console.warn(`[Identity] Unauthorized impersonation attempt by ${authenticatedUserCid}`);
|
|
139
|
+
return res.status(403).json({ error: "Unauthorized: Developer privileges required for impersonation" });
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
// User is trying to access another user's CID without impersonation header
|
|
143
|
+
console.warn(`[Identity] Security violation: User ${authenticatedUserCid} attempted to access CID ${requestedUserId}`);
|
|
144
|
+
return res.status(403).json({ error: "Unauthorized: You can only access your own data" });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Authenticated user owns the requested CID (or no CID requested) - use authenticated CID
|
|
149
|
+
req.actualUserId = authenticatedUserCid;
|
|
150
|
+
req.targetUserId = authenticatedUserCid;
|
|
151
|
+
req.isImpersonating = false;
|
|
152
|
+
} else if (requestedUserId) {
|
|
153
|
+
// No Firebase Auth but CID provided - legacy behavior (for backward compatibility)
|
|
154
|
+
// WARNING: This is less secure but may be needed for some legacy clients
|
|
155
|
+
// Consider requiring Firebase Auth for all authenticated routes
|
|
156
|
+
if (!isPublic) {
|
|
157
|
+
console.warn(`[Identity] No Firebase Auth but CID ${requestedUserId} provided - using legacy mode`);
|
|
158
|
+
}
|
|
159
|
+
req.actualUserId = requestedUserId;
|
|
160
|
+
req.targetUserId = requestedUserId;
|
|
161
|
+
req.isImpersonating = false;
|
|
162
|
+
|
|
163
|
+
// Check for developer impersonation (legacy mode)
|
|
164
|
+
const impersonateId = req.headers['x-impersonate-cid'] || req.query.impersonateCid;
|
|
165
|
+
if (impersonateId && impersonateId !== requestedUserId) {
|
|
166
|
+
const isDev = await isDeveloper(db, requestedUserId);
|
|
167
|
+
if (isDev) {
|
|
168
|
+
req.targetUserId = impersonateId;
|
|
169
|
+
req.isImpersonating = true;
|
|
170
|
+
console.log(`[Identity] Dev ${requestedUserId} is impersonating ${impersonateId}`);
|
|
171
|
+
} else {
|
|
172
|
+
console.warn(`[Identity] Unauthorized impersonation attempt by ${requestedUserId}`);
|
|
173
|
+
}
|
|
109
174
|
}
|
|
175
|
+
} else {
|
|
176
|
+
// No CID and not public - this shouldn't happen due to check above, but handle gracefully
|
|
177
|
+
req.actualUserId = null;
|
|
178
|
+
req.targetUserId = null;
|
|
179
|
+
req.isImpersonating = false;
|
|
110
180
|
}
|
|
111
181
|
|
|
112
182
|
next();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
2
|
const { resolveUserIdentity } = require('../middleware/identity_middleware.js');
|
|
3
|
+
const { verifyFirebaseToken } = require('../middleware/firebase_auth_middleware.js');
|
|
3
4
|
|
|
4
5
|
const notificationRoutes = require('./notifications.js');
|
|
5
6
|
const alertsRoutes = require('./alerts.js'); // <--- NEW
|
|
@@ -19,6 +20,11 @@ module.exports = (dependencies) => {
|
|
|
19
20
|
next();
|
|
20
21
|
});
|
|
21
22
|
|
|
23
|
+
// SECURITY: Verify Firebase Auth token first (if present)
|
|
24
|
+
// This allows identity_middleware to validate CID ownership
|
|
25
|
+
router.use(verifyFirebaseToken);
|
|
26
|
+
|
|
27
|
+
// Then resolve user identity with CID ownership validation
|
|
22
28
|
router.use(resolveUserIdentity);
|
|
23
29
|
|
|
24
30
|
router.use('/notifications', notificationRoutes);
|