parse-community / parse-community/parse-server
Verification and password reset emails are dispatched without a rejection handler, producing unhandled rejections
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 21.4k
- Forks
- 4.8k
- Avg merge
- 7h 45m
- Merged PRs (30d)
- 11
Description
New Issue Checklist
- Report security issues confidentially.
- Any contribution is under this license.
- Before posting search existing issues.
Issue Description
Verification and password reset emails are dispatched without awaiting the mail adapter and without a rejection handler anywhere in the chain. A mail adapter that rejects, which is what an outage at the email provider looks like, produces an unhandled promise rejection.
The adapter call itself is never awaited, in either flow and in both the adapter-specific and sendMail fallback branches:
// src/Controllers/UserController.js
if (this.adapter.sendVerificationEmail) {
this.adapter.sendVerificationEmail(options); // 181
} else {
this.adapter.sendMail(this.defaultVerificationEmail(options)); // 183
}
if (this.adapter.sendPasswordResetEmail) {
this.adapter.sendPasswordResetEmail(options); // 293
} else {
this.adapter.sendMail(this.defaultResetPasswordEmail(options)); // 295
}
Several callers of the surrounding controller methods are also not awaited:
| Location | Call | Runs on |
|---|---|---|
src/RestWrite.js:1214 |
userController.sendVerificationEmail(…) |
user signup |
src/Routers/UsersRouter.js:685 |
userController.sendVerificationEmail(user, req) |
POST /verificationEmailRequest |
src/Controllers/UserController.js:227 |
this.sendVerificationEmail(aUser, req) |
resend from the verification page |
UsersRouter.handleResetRequest at line 630 does await userController.sendPasswordResetEmail(email), but that await covers only token generation, since the adapter call inside resolves before delivery is attempted.
I want to separate two things here, because I think only one of them is unambiguously a defect.
The unhandled rejection is a defect. No .catch() exists at any level, so a rejecting mail adapter surfaces as an unhandled rejection with no indication of which email failed or for which user. Depending on the Node version and process configuration that either logs a bare warning or terminates the process, so an outage at the email provider can take down an otherwise healthy server. This is the same shape as #10634, where RedisCacheAdapter write methods reject into unawaited call sites.
Whether the client should learn about the failure is a design question, and I am not assuming the answer. The signup path is explicitly commented // Fire and forget! (src/RestWrite.js:1213), so the non-blocking dispatch is deliberate, and there are good reasons for it. Blocking signup on an email provider is a poor tradeoff, and for the password reset flow specifically, awaiting delivery leaks timing that helps an attacker distinguish registered from unregistered addresses, which is the enumeration exposure that resetPasswordSuccessOnInvalidEmail exists to close. So "return 200 regardless" may well be the intended contract.
What that leaves is a question for maintainers: should a failed send at minimum be logged with the adapter error and the target flow, so operators can detect a broken email pipeline, while delivery stays non-blocking? Today a completely dead mail adapter is silent in the logs, and the only signal is a stray unhandled rejection warning with no context.
Steps to reproduce
- Configure Parse Server with a mail adapter whose
sendVerificationEmailorsendPasswordResetEmailreturns a rejected promise. - Sign up a user with
verifyUserEmails: true, or callPOST /requestPasswordReset.
const server = await reconfigureServer({
verifyUserEmails: true,
publicServerURL: 'http://localhost:8378/1',
emailAdapter: {
sendVerificationEmail: () => Promise.reject(new Error('Email provider is down')),
sendPasswordResetEmail: () => Promise.reject(new Error('Email provider is down')),
sendMail: () => Promise.reject(new Error('Email provider is down')),
},
});
process.on('unhandledRejection', reason => console.log('unhandled:', reason.message));
const user = new Parse.User();
user.setUsername('zebra');
user.setPassword('password');
user.setEmail('zebra@example.com');
await user.signUp();
Actual Outcome
The request succeeds. unhandled: Email provider is down is printed. Nothing is written to the Parse Server log identifying the failed email, the flow it belonged to, or the user it was addressed to.
Expected Outcome
At minimum, the rejection is caught and logged with the adapter error and enough context to identify the flow, and no unhandled rejection is produced. Whether the response to the client should change is the maintainers' call, and I would not change it without direction, given the enumeration tradeoff on the password reset path.
Happy to open a PR once there is a decision on scope.
Environment
Server
- Parse Server version:
9.10.1-alpha.6 - Operating system:
macOS 15.5 - Local or remote host:
local
Database
- System (MongoDB or Postgres):
MongoDB - Database version:
8.0 - Local or remote host:
local
Client
- SDK (iOS, Android, JavaScript, PHP, Unity, etc):
JavaScript - SDK version:
as vendored by parse-server
Logs
No Parse Server log entry is produced for the failed send. The only output is the runtime's unhandled rejection warning.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the mail dispatch branches in src/Controllers/UserController.js and trace the listed callers in src/RestWrite.js and src/Routers/UsersRouter.js. Reproduce the rejected-adapter case from the issue and inspect existing logging and email tests before confirming the intended non-blocking behavior with maintainers. Done means the agreed scope is covered without an unhandled rejection and with the expected diagnostic context.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, node.js
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100