Repository navigation
Conversation
- User roles and isTester are writable only by admin/superAdmin; any user could previously grant themselves superAdmin via PATCH /1/users/me. - User email, phone, roles and isTester are readable only by self and staff roles, so ?include=owner no longer leaks them. - Product create/update/delete require products.write. - /1/docs (unauthenticated writes to openapi.json) is mounted in development only.
- /auth/password/update only accepts access tokens minted for reset-password; unsubscribe-link tokens could previously reset the password and return a full session. - TOTP login always requires a password verified in the last 5 minutes; enableTotp never set isMfa, so TOTP alone was a full login. - Google/Apple login into an account whose email was never verified clears its existing authenticators and sessions, so a password set by whoever signed up first with that email stops working. Google sign-ups are now marked emailVerified.
Both prove ownership of the mailbox, so Google/Apple login no longer treats these accounts as unverified and clears their credentials.
Marking the email verified without clearing credentials let a squatter's passkey and sessions survive a reset and skip the later OAuth cleanup.
OTP login marked emailVerified based on the lookup field, so a code sent to an attacker's phone could verify a squatted email and exempt it from claimUnverifiedUser. Non-MFA email OTP login now also clears logins added before verification, like password reset.
Both fields were optional and findUser silently preferred phone, so a request could name one account and be resolved against another.
/otp/send defaulted to email even when the request named a phone, so an sms login emailed the code and never verified the phone. Also fixes test expectations: write-access violations return 401, and query params go in the request helper.
# Conflicts: # services/api/src/routes/auth/apple.js # services/api/src/routes/auth/google.js # services/api/src/routes/auth/otp.js # services/api/src/routes/auth/password.js # services/api/src/routes/auth/totp.js # services/api/src/routes/auth/utils.js # services/api/src/routes/index.js # services/api/src/routes/invites.js # services/api/src/routes/products.js # services/api/src/routes/products.test.js # services/api/src/utils/auth/login.js
Reflects the code login schemas and the user model scope change.
API ChangesOperations
|
This was referenced Sep 24, 2026
Collaborator
Author
|
Closing in favour of smaller PRs, one per fix:
Together they are identical to this PR's API source changes. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A security review of the API turned up seven issues, verified individually before fixing. Each fix has a regression test. All 411 API tests pass.
What was wrong
roleshad nowriteAccess, so any signed-up user couldPATCH /1/users/mewith{"roles":[{"role":"superAdmin","scope":"global"}]}and become superAdmin.isTesterwas writable the same way, and it makes the login code a fixed value the response returns.$staffscope, writable by admins only./auth/password/updateaccepted anyaccesstoken. Unsubscribe links in notification emails carry that same type, so a leaked unsubscribe URL could set the password and return a full session, with no MFA step.action === 'reset-password'.enableTotpnever setisMfa, so the recent-password check never ran and/auth/totp/loginwas a full login from email plus a 6-digit code.verifyTotpalways requires a password verified in the last 5 minutes.claimUnverifiedUserremoves logins added before the email was proven.fetchByParampassed?include=straight through, and useremail,phoneandroleshad noreadAccess.GET /1/uploads/<id>?include=ownerreturned the uploader's contact details without a login; upload ids are public in image URLs.authenticate(), so any user could change or delete any product, or add one to someone else's shop.products.write.PATCH /1/docsandPOST /1/docs/generateneeded no login and were mounted everywhere, writing arbitrary paths intoopenapi.json, which/openapi.jsonserves.Two more came out of reviewing the fixes themselves:
/otp/senddefaulted to email whatever identifier named the account, so an sms login emailed the code and never setphoneVerified. The channel now follows the identifier, and/otp/loginverifies the channel the code was actually sent to rather than the field used to look the user up./otp/send,/otp/loginand/totp/loginnow require exactly one of email or phone, whichfindUserpreviously resolved by silently preferring phone.Behaviour changes worth knowing
Not fixed here
Six lower-confidence findings were left out, including replayable invite tokens, admins being able to grant themselves superAdmin, and staging config with a hardcoded
JWT_SECRETand admin password. Happy to open a follow-up.