@7365admin1/core 3.48.1-staging.186 → 3.48.1-staging.187

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.
@@ -0,0 +1,52 @@
1
+ ---
2
+ "@7365admin1/core": minor
3
+ ---
4
+
5
+ Stop the e-mail account lookup handing out the password hash and session id.
6
+
7
+ `GET /api/users/user/:email` (`user.route.ts:25`) has no guard on it at all, and
8
+ `GET /api/users/email/:email` next to it is the same handler behind
9
+ `requireAuth`. Both answered `res.json(user)` on the raw stored document, and
10
+ the users collection stores `password` — the hash — and `sid` — that account's
11
+ CURRENT session id.
12
+
13
+ So anyone at all, with no session, who knew or guessed an e-mail address got
14
+ that account's password hash back in the response body, to crack offline at
15
+ leisure, plus a live session id to reuse. On the authenticated twin, any signed
16
+ in account could read the same two fields for any other account, including an
17
+ administrator's.
18
+
19
+ The unguarded route cannot simply be guarded. Its only callers anywhere in the
20
+ organisation are pre-login signup screens — the resident app's e-mail check
21
+ (`user-registration.ts:103`) and the four `web-app-main/pages/sign-up-invite/*`
22
+ pages through `layer-common`'s `getUserByEmailV2` — so a session check would
23
+ break registration. The fix is therefore not authentication, it is not handing
24
+ out more than signup asks for.
25
+
26
+ All five of those callers read NO field off the reply. The mobile check does
27
+ `res !== null`; the four web pages do `if (response)`. So without a session the
28
+ handler no longer loads the document: a new `userExistsByEmail` does a
29
+ `findOne` with an explicit allow-list projection of `_id` alone and returns a
30
+ boolean, and the route answers `{ exists: true }` or `null` exactly as before.
31
+ An allow-list, deliberately — a deny-list would silently start leaking the next
32
+ credential-shaped field somebody adds to the collection.
33
+
34
+ `getUserByEmail` itself is UNCHANGED and still returns the whole document,
35
+ because signing in genuinely needs the hash to compare against. Putting the
36
+ projection on that function instead of beside it would have broken every login.
37
+
38
+ With a session the reply keeps its existing shape, minus `password` and `sid`,
39
+ via a small `withoutCredentials`. `_id` is the only field its callers read
40
+ (`web-app-main pages/verify/email`, `web-app-org onboarding/getting-started`)
41
+ and it survives. That path is narrowed by removal rather than by allow-list on
42
+ purpose: it is consumed by screens across web and mobile that cannot all be
43
+ enumerated from here, and removing two credential fields cannot break a
44
+ legitimate one, because no screen renders a password hash or a session id.
45
+
46
+ The v2 twin `GET /api/users/v2/email/:email` had the same leak — it is behind
47
+ `requireAuth`, so it was never anonymous, but "signed in" is not "entitled to
48
+ another account's credentials" — and is stripped the same way. v2 has no
49
+ anonymous `/user/:email` route and no pre-login caller.
50
+
51
+ Covered by `test/e2e/user-email-lookup-leak.e2e.test.mjs`, which asserts on the
52
+ absence of the key and never on its value.
package/dist/index.d.ts CHANGED
@@ -168,6 +168,7 @@ declare function useUserRepo(): {
168
168
  pageRange: string;
169
169
  }>;
170
170
  getUserByEmail: (email: string) => Promise<TUser | null>;
171
+ userExistsByEmail: (email: string) => Promise<boolean>;
171
172
  getUserByReferralCode: (referralCode: string) => Promise<TUser | null>;
172
173
  getByEmailApp: (email: string, app: string) => Promise<TUser | null>;
173
174
  getUserById: (_id: string | ObjectId) => Promise<TUser & {
package/dist/index.js CHANGED
@@ -8973,6 +8973,17 @@ function useUserRepo() {
8973
8973
  throw new import_node_server_utils11.InternalServerError("Failed to get user by email.");
8974
8974
  }
8975
8975
  }
8976
+ async function userExistsByEmail(email) {
8977
+ try {
8978
+ const data = await collection.findOne(
8979
+ { email: { $regex: `^${email}$`, $options: "i" } },
8980
+ { projection: { _id: 1 } }
8981
+ );
8982
+ return Boolean(data);
8983
+ } catch (error) {
8984
+ throw new import_node_server_utils11.InternalServerError("Failed to get user by email.");
8985
+ }
8986
+ }
8976
8987
  async function getUserByEmailStatus(email) {
8977
8988
  try {
8978
8989
  const cacheOptions = {
@@ -9624,6 +9635,7 @@ function useUserRepo() {
9624
9635
  getAcceptedTerms,
9625
9636
  listTermsAcceptance,
9626
9637
  getUserByEmail,
9638
+ userExistsByEmail,
9627
9639
  getUserByReferralCode,
9628
9640
  getByEmailApp,
9629
9641
  getUserById,
@@ -18238,11 +18250,20 @@ async function requirePlatformOwner(req) {
18238
18250
  return id;
18239
18251
  }
18240
18252
 
18253
+ // src/utils/user-response.util.ts
18254
+ function withoutCredentials(user) {
18255
+ if (!user || typeof user !== "object")
18256
+ return user;
18257
+ const { password, sid, ...rest } = user;
18258
+ return rest;
18259
+ }
18260
+
18241
18261
  // src/controllers/user.controller.ts
18242
18262
  function useUserController() {
18243
18263
  const {
18244
18264
  getUserById: _getById,
18245
18265
  getUserByEmail: _getByEmail,
18266
+ userExistsByEmail: _existsByEmail,
18246
18267
  getUsers: _getUsers,
18247
18268
  updateBirthday: _updateBirthday,
18248
18269
  updateUserFieldById: _updateUserFieldById,
@@ -18320,8 +18341,13 @@ function useUserController() {
18320
18341
  return;
18321
18342
  }
18322
18343
  try {
18344
+ if (!callerId(req)) {
18345
+ const exists = await _existsByEmail(email);
18346
+ res.json(exists ? { exists: true } : null);
18347
+ return;
18348
+ }
18323
18349
  const user = await _getByEmail(email);
18324
- res.json(user);
18350
+ res.json(withoutCredentials(user));
18325
18351
  return;
18326
18352
  } catch (error2) {
18327
18353
  import_node_server_utils42.logger.log({ level: "error", message: `${error2.message}` });
@@ -82167,7 +82193,7 @@ function useUserControllerV2() {
82167
82193
  return rejectValidation(error, next);
82168
82194
  try {
82169
82195
  const user = await _getUserByEmail(value);
82170
- res.json(user);
82196
+ res.json(withoutCredentials(user));
82171
82197
  return;
82172
82198
  } catch (err) {
82173
82199
  import_node_server_utils267.logger.log({ level: "error", message: `${err.message}` });