mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-03 22:32:42 +00:00
🧼 fix: Sanitize User Response Fields (#13421)
This commit is contained in:
parent
479e9d59b7
commit
75baa5b848
2 changed files with 137 additions and 9 deletions
|
|
@ -25,6 +25,37 @@ const { getAppConfig } = require('~/server/services/Config');
|
|||
const { getLogStores } = require('~/cache');
|
||||
const db = require('~/models');
|
||||
|
||||
const PUBLIC_USER_RESPONSE_FIELDS = [
|
||||
'_id',
|
||||
'id',
|
||||
'name',
|
||||
'username',
|
||||
'email',
|
||||
'emailVerified',
|
||||
'avatar',
|
||||
'provider',
|
||||
'role',
|
||||
'plugins',
|
||||
'twoFactorEnabled',
|
||||
'termsAccepted',
|
||||
'personalization',
|
||||
'favorites',
|
||||
'skillStates',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'tenantId',
|
||||
];
|
||||
|
||||
const sanitizeUserForResponse = (user) => {
|
||||
const source = user.toObject != null ? user.toObject() : user;
|
||||
return PUBLIC_USER_RESPONSE_FIELDS.reduce((userData, field) => {
|
||||
if (source[field] !== undefined) {
|
||||
userData[field] = source[field];
|
||||
}
|
||||
return userData;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const getUserController = async (req, res) => {
|
||||
const appConfig =
|
||||
req.config ??
|
||||
|
|
@ -34,14 +65,7 @@ const getUserController = async (req, res) => {
|
|||
tenantId: req.user?.tenantId,
|
||||
}));
|
||||
/** @type {IUser} */
|
||||
const userData = req.user.toObject != null ? req.user.toObject() : { ...req.user };
|
||||
/**
|
||||
* These fields should not exist due to secure field selection, but deletion
|
||||
* is done in case of alternate database incompatibility with Mongo API
|
||||
* */
|
||||
delete userData.password;
|
||||
delete userData.totpSecret;
|
||||
delete userData.backupCodes;
|
||||
const userData = sanitizeUserForResponse(req.user);
|
||||
if (appConfig.fileStrategy === FileSources.s3 && userData.avatar) {
|
||||
const avatarNeedsRefresh = needsRefresh(userData.avatar, 3600);
|
||||
if (!avatarNeedsRefresh) {
|
||||
|
|
|
|||
|
|
@ -108,10 +108,114 @@ afterEach(async () => {
|
|||
}
|
||||
});
|
||||
|
||||
const { deleteUserController } = require('./UserController');
|
||||
const { deleteUserController, getUserController } = require('./UserController');
|
||||
const { Group } = require('~/db/models');
|
||||
const { deleteConvos } = require('~/models');
|
||||
|
||||
describe('getUserController', () => {
|
||||
const mockRes = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
send: jest.fn().mockReturnThis(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should only expose public user response fields from the request user', async () => {
|
||||
const createdAt = new Date('2026-01-01T00:00:00.000Z');
|
||||
const updatedAt = new Date('2026-01-02T00:00:00.000Z');
|
||||
const req = {
|
||||
config: {},
|
||||
user: {
|
||||
id: 'user-id',
|
||||
_id: 'user-id',
|
||||
name: 'OpenID User',
|
||||
username: 'openid-user',
|
||||
email: 'openid@test.com',
|
||||
emailVerified: true,
|
||||
avatar: '/avatars/user-id.png',
|
||||
provider: 'openid',
|
||||
role: 'USER',
|
||||
plugins: ['web_search'],
|
||||
twoFactorEnabled: true,
|
||||
termsAccepted: true,
|
||||
personalization: { memories: false },
|
||||
favorites: [{ model: 'gpt-5', endpoint: 'openAI' }],
|
||||
skillStates: { skill_one: true },
|
||||
createdAt,
|
||||
updatedAt,
|
||||
tenantId: 'tenant-id',
|
||||
password: 'hashed-password',
|
||||
__v: 1,
|
||||
totpSecret: 'totp-secret',
|
||||
backupCodes: [{ codeHash: 'backup-code' }],
|
||||
pendingTotpSecret: 'pending-totp-secret',
|
||||
pendingBackupCodes: [{ codeHash: 'pending-backup-code' }],
|
||||
refreshToken: [{ refreshToken: 'legacy-refresh-token' }],
|
||||
googleId: 'google-id',
|
||||
openidId: 'openid-id',
|
||||
openidIssuer: 'openid-issuer',
|
||||
idOnTheSource: 'external-source-id',
|
||||
federatedTokens: {
|
||||
access_token: 'access-token',
|
||||
id_token: 'id-token',
|
||||
refresh_token: 'refresh-token',
|
||||
},
|
||||
openidTokens: {
|
||||
access_token: 'openid-access-token',
|
||||
refresh_token: 'openid-refresh-token',
|
||||
},
|
||||
tokenset: {
|
||||
access_token: 'tokenset-access-token',
|
||||
refresh_token: 'tokenset-refresh-token',
|
||||
},
|
||||
safeLookingRuntimeField: 'internal-value',
|
||||
},
|
||||
};
|
||||
|
||||
await getUserController(req, mockRes);
|
||||
|
||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||
const sentUser = mockRes.send.mock.calls[0][0];
|
||||
expect(sentUser).toMatchObject({
|
||||
id: 'user-id',
|
||||
_id: 'user-id',
|
||||
name: 'OpenID User',
|
||||
username: 'openid-user',
|
||||
email: 'openid@test.com',
|
||||
emailVerified: true,
|
||||
avatar: '/avatars/user-id.png',
|
||||
provider: 'openid',
|
||||
role: 'USER',
|
||||
plugins: ['web_search'],
|
||||
twoFactorEnabled: true,
|
||||
termsAccepted: true,
|
||||
personalization: { memories: false },
|
||||
favorites: [{ model: 'gpt-5', endpoint: 'openAI' }],
|
||||
skillStates: { skill_one: true },
|
||||
createdAt,
|
||||
updatedAt,
|
||||
tenantId: 'tenant-id',
|
||||
});
|
||||
expect(sentUser).not.toHaveProperty('password');
|
||||
expect(sentUser).not.toHaveProperty('__v');
|
||||
expect(sentUser).not.toHaveProperty('totpSecret');
|
||||
expect(sentUser).not.toHaveProperty('backupCodes');
|
||||
expect(sentUser).not.toHaveProperty('pendingTotpSecret');
|
||||
expect(sentUser).not.toHaveProperty('pendingBackupCodes');
|
||||
expect(sentUser).not.toHaveProperty('refreshToken');
|
||||
expect(sentUser).not.toHaveProperty('googleId');
|
||||
expect(sentUser).not.toHaveProperty('openidId');
|
||||
expect(sentUser).not.toHaveProperty('openidIssuer');
|
||||
expect(sentUser).not.toHaveProperty('idOnTheSource');
|
||||
expect(sentUser).not.toHaveProperty('federatedTokens');
|
||||
expect(sentUser).not.toHaveProperty('openidTokens');
|
||||
expect(sentUser).not.toHaveProperty('tokenset');
|
||||
expect(sentUser).not.toHaveProperty('safeLookingRuntimeField');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteUserController', () => {
|
||||
const mockRes = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue