📮 fix: Validate Invite Email Claims (#15542)

* 🔒 fix: Require the email getInvite claims to validate

`findToken` builds its query from the fields it is given, so an absent
email is a lookup by token alone: the invite matches, `checkInviteUser`
deletes it, and the registration then fails on the missing field. The
invite is consumed and no account is created.

* 🎨 style: Sort spec imports by line length

---------

Co-authored-by: lailson henrique <dev@iagiliza.com.br>
Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
Lailson Henrique 2026-09-03 23:27:49 -03:00 • committed by GitHub
parent bd31ecd129
commit 1c2796b978
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 58 additions and 0 deletions

View file

@ -0,0 +1,51 @@
import mongoose from 'mongoose';
import { MongoMemoryServer } from 'mongodb-memory-server';
import { createMethods, createModels } from '@librechat/data-schemas';
import { createInvite, getInvite } from './invite';
let mongoServer: MongoMemoryServer;
let deps: Parameters<typeof getInvite>[2];
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
await mongoose.connect(mongoServer.getUri());
createModels(mongoose);
const { createToken, findToken } = createMethods(mongoose);
deps = { createToken, findToken } as Parameters<typeof getInvite>[2];
});
afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});
beforeEach(async () => {
await mongoose.models.Token.deleteMany({});
});
describe('getInvite', () => {
it('returns the invite for the address it was issued to', async () => {
const token = (await createInvite('pedro@example.com', deps)) as string;
await expect(getInvite(token, 'pedro@example.com', deps)).resolves.toMatchObject({
email: 'pedro@example.com',
});
});
it('refuses an address the invite was not issued to', async () => {
const token = (await createInvite('pedro@example.com', deps)) as string;
await expect(getInvite(token, 'someone.else@example.com', deps)).resolves.toMatchObject({
error: true,
});
});
it('refuses a lookup carrying no address at all', async () => {
const token = (await createInvite('pedro@example.com', deps)) as string;
await expect(getInvite(token, undefined as unknown as string, deps)).resolves.toMatchObject({
error: true,
});
});
});

View file

@ -45,6 +45,13 @@ export async function getInvite(
deps: InviteDeps,
): Promise<unknown> {
try {
/** `findToken` builds its query from the fields it is given, so an absent email
* is a lookup by token alone — which would match the invite and let the caller
* consume it without ever proving the address. */
if (!email) {
throw new Error('Invite not found or email does not match');
}
const token = decodeURIComponent(encodedToken);
const hash = await hashToken(token);
const invite = await deps.findToken({ token: hash, email });