Version: v0.8.1-rc1 Repository: github.com/danny-avila/LibreChat Last Updated: 2025-11-24
- Overview
- Authentication
- Authorization & Access Control
- Input Validation & Sanitization
- API Security
- Data Encryption
- Session Management
- Rate Limiting & DDoS Protection
- Privacy & Data Protection
- GDPR Compliance
- Security Configuration
- Security Assessment
🏗️ For overall system architecture and component interactions, see: LibreChat Architecture Documentation
LibreChat implements a comprehensive multi-layered security architecture with:
- 7 authentication methods (Local, JWT, OAuth, LDAP, SAML, OpenID, 2FA)
- Role-based access control (RBAC) with granular permissions
- Modern encryption (AES-256-CTR)
- GDPR-compliant data handling
- Rate limiting across all sensitive endpoints
- Input validation with injection prevention
graph TB
subgraph "Authentication Layer"
A1[Local Auth]
A2[OAuth 2.0]
A3[LDAP/SAML]
A4[OpenID Connect]
A5[2FA/TOTP]
end
subgraph "Authorization Layer"
B1[RBAC System]
B2[Permission Service]
B3[ACL Entries]
B4[Group Management]
end
subgraph "Data Protection"
C1[AES-256-CTR Encryption]
C2[Password Hashing bcrypt]
C3[Secure Sessions]
C4[HttpOnly Cookies]
end
subgraph "Input Security"
D1[Schema Validation]
D2[MongoDB Sanitize]
D3[File Upload Validation]
D4[XSS Prevention]
end
subgraph "Rate Limiting"
E1[Login Limiter]
E2[Registration Limiter]
E3[Message Limiter]
E4[Auto-Ban System]
end
A1 & A2 & A3 & A4 --> A5
A5 --> B1
B1 --> B2 --> B3
B2 --> B4
C1 & C2 & C3 & C4 --> D1
D1 & D2 & D3 & D4 --> E1
E1 & E2 & E3 --> E4
File: api/strategies/localStrategy.js
Hashing: api/server/services/AuthService.js#L245-L249
// bcrypt with salt rounds = 10
const salt = bcrypt.genSaltSync(10);
const hashedPassword = bcrypt.hashSync(password, salt);Validation: api/strategies/localStrategy.js#L21-L45
// Password comparison with timing attack mitigation
if (!user || !bcrypt.compareSync(password, user.password)) {
return done(null, false, {
message: 'Email or password is incorrect.'
});
}Password Policy: api/strategies/validators.js#L29-L35
password: z
.string()
.min(MIN_PASSWORD_LENGTH) // Default: 8, configurable
.max(128)
.refine(val => val.trim() !== '', {
message: 'Password cannot be whitespace only'
})Verification Flow: api/server/services/AuthService.js#L278-L297
- Cryptographically secure token generation via
webcrypto.getRandomValues() - Token hashing before storage with SHA-256
- 15-minute expiration (
expiresIn: 900) - One-time use enforcement
Configuration:
ALLOW_EMAIL_LOGIN=true # Enable email/password auth
ALLOW_UNVERIFIED_EMAIL_LOGIN=false # Require email verificationFile: api/strategies/jwtStrategy.js
Access Token: packages/api/src/crypto/jwt.ts#L15-L28
// Short-lived token (default: 15 minutes)
const payload = {
id: userId,
username,
provider,
email
};
const token = jwt.sign(payload, jwtSecret, {
expiresIn: SESSION_EXPIRY
});Refresh Token: api/server/services/AuthService.js#L99-L124
// Long-lived token (default: 7 days)
const refreshToken = jwt.sign(
{ id: session._id },
jwtRefreshSecret,
{ expiresIn: REFRESH_TOKEN_EXPIRY }
);Strategy: api/strategies/jwtStrategy.js#L8-L33
// Bearer token extraction from Authorization header
passport.use(new JwtStrategy({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET
}, async (payload, done) => {
// Validate user exists and is active
}));Configuration:
JWT_SECRET=your-secret-key # Access token secret
JWT_REFRESH_SECRET=your-refresh-key # Refresh token secret
SESSION_EXPIRY=900 # 15 minutes
REFRESH_TOKEN_EXPIRY=604800 # 7 daysSupported Providers:
- Google:
api/strategies/googleStrategy.js - GitHub:
api/strategies/githubStrategy.js - Discord:
api/strategies/discordStrategy.js - Facebook:
api/strategies/facebookStrategy.js - Apple:
api/strategies/appleStrategy.js
Security Features:
- State parameter for CSRF protection
- Callback URL validation
- Scope limitation to necessary permissions
- User profile mapping with validation
Configuration:
ALLOW_SOCIAL_LOGIN=true
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_CALLBACK_URL=/oauth/google/callbackFile: api/strategies/ldapStrategy.js
TLS/SSL: ldapStrategy.js#L20-L35
{
url: process.env.LDAP_URL,
tlsOptions: {
rejectUnauthorized: LDAP_TLS_REJECT_UNAUTHORIZED,
ca: [fs.readFileSync(LDAP_CA_CERT_PATH)]
},
starttls: LDAP_STARTTLS === 'true'
}Features:
- START TLS support
- Certificate validation
- Custom CA certificate support
- LDAP search filter configuration
- Attribute mapping (email, username, name, ID)
Configuration:
LDAP_URL=ldap://your-ldap-server:389
LDAP_BIND_DN=cn=admin,dc=example,dc=com
LDAP_BIND_CREDENTIALS=password
LDAP_USER_SEARCH_BASE=ou=users,dc=example,dc=com
LDAP_SEARCH_FILTER=(uid={{username}})
LDAP_STARTTLS=true
LDAP_TLS_REJECT_UNAUTHORIZED=true
LDAP_CA_CERT_PATH=/path/to/ca-cert.pemFile: api/strategies/samlStrategy.js
Certificate Validation: samlStrategy.js#L25-L35
// Supports RFC7468 format or Base64 encoded
const cert = process.env.SAML_CERT;
const formattedCert = cert.includes('BEGIN CERTIFICATE')
? cert
: `-----BEGIN CERTIFICATE-----\n${cert}\n-----END CERTIFICATE-----`;Signature Validation:
- Assertion-signed authentication
- Response-signed authentication
- Configurable claim extraction
Configuration:
SAML_ENTRY_POINT=https://idp.example.com/sso
SAML_ISSUER=https://your-app.com
SAML_CALLBACK_URL=https://your-app.com/auth/saml/callback
SAML_CERT=MIIDXTCCAkWgAwIBAgIJAKoSdj...File: api/strategies/openidStrategy.js
PKCE Support: openidStrategy.js#L45-L50
// Proof Key for Code Exchange (for public clients)
{
usePKCE: process.env.OPENID_USE_PKCE === 'true',
state: true,
nonce: true
}Clock Tolerance: openidStrategy.js#L42
- Configurable clock skew tolerance (default: 300 seconds)
- Prevents timing-based authentication failures
Token Exchange: openidStrategy.js#L85-L120
- On-behalf-of flow for Microsoft Graph
- Secure token storage in cookies
- Access token refresh capability
Required Roles: openidStrategy.js#L65-L75
// Validate user has required role
if (REQUIRED_ROLE && !userRoles.includes(REQUIRED_ROLE)) {
throw new Error('User does not have required role');
}Configuration:
OPENID_ISSUER=https://accounts.google.com
OPENID_CLIENT_ID=your-client-id
OPENID_CLIENT_SECRET=your-client-secret
OPENID_CALLBACK_URL=/oauth/openid/callback
OPENID_SCOPE=openid profile email
OPENID_USE_PKCE=false
OPENID_REQUIRED_ROLE=adminFile: api/server/services/twoFactorService.js
Secret Generation: twoFactorService.js#L10-L18
// 80 bits of randomness, Base32 encoded
const buffer = new Uint8Array(10);
crypto.getRandomValues(buffer);
const secret = encode(buffer).toString();Verification: twoFactorService.js#L25-L40
// HMAC-SHA1, 30-second time step, ±1 window
const isValid = authenticator.verify({
token: userToken,
secret: totpSecret
});Specifications:
- Algorithm: HMAC-SHA1
- Time step: 30 seconds
- Code length: 6 digits
- Window: ±1 step (60-second acceptance)
Generation: api/server/controllers/TwoFactorController.js#L105-L120
// 10 codes, 8-character hex strings
const backupCodes = Array.from({ length: 10 }, () =>
Array.from(crypto.getRandomValues(new Uint8Array(4)))
.map(b => b.toString(16).padStart(2, '0'))
.join('')
);
// Store SHA-256 hashed
const hashedCodes = backupCodes.map(code =>
createHash('sha256').update(code).digest('hex')
);Validation: api/server/controllers/TwoFactorController.js#L185-L205
- One-time use enforcement
- Marked as used with timestamp
- Cannot be reused
Temporary Token: api/server/controllers/auth/LoginController.js#L65-L75
// 5-minute temporary token for 2FA challenge
const tempToken = jwt.sign(
{ userId: user._id, temp: true },
JWT_SECRET,
{ expiresIn: 300 }
);Token Upgrade: api/server/controllers/TwoFactorController.js#L225-L245
- Validate TOTP or backup code
- Upgrade to full authentication token
- Create session
File: packages/api/src/app/permissions.ts
enum SystemRoles {
ADMIN = 'ADMIN',
USER = 'USER' // Default
}First User Admin: api/server/services/AuthService.js#L265-L270
// First registered user automatically becomes ADMIN
const isFirstUser = (await User.countDocuments()) === 0;
const role = isFirstUser ? SystemRoles.ADMIN : SystemRoles.USER;File: api/server/middleware/roles/admin.js
Enforcement: admin.js#L5-L15
function requireAdminRole(req, res, next) {
if (req.user.role !== SystemRoles.ADMIN) {
return res.status(403).json({
message: 'Admin access required'
});
}
next();
}File: api/server/services/PermissionService.js
enum PermissionTypes {
// Resource types
AGENTS = 'agents',
PROMPTS = 'prompts',
FILES = 'files',
CONVERSATIONS = 'conversations',
// Principal types
USER = 'USER',
GROUP = 'GROUP',
ROLE = 'ROLE',
PUBLIC = 'PUBLIC'
}enum AccessRoles {
VIEWER = 'VIEWER', // Read-only
EDITOR = 'EDITOR', // Read-write
OWNER = 'OWNER' // Full control
}Check Access: PermissionService.js#L45-L85
async checkPermission({
userId,
resourceType,
resourceId,
requiredRole = 'VIEWER'
}) {
// Check direct user permission
// Check group permissions
// Check role permissions
// Check public permissions
return hasPermission;
}File: api/models/AccessControl.js
{
principalType: 'USER|GROUP|ROLE|PUBLIC',
principalId: ObjectId,
resourceType: 'agents|prompts|files|conversations',
resourceId: ObjectId,
role: 'VIEWER|EDITOR|OWNER',
permissions: {
read: Boolean,
write: Boolean,
delete: Boolean,
share: Boolean
}
}File: packages/api/src/app/permissions.ts#L15-L35
Configurable Features:
interface InterfacePermissions {
prompts: boolean;
bookmarks: boolean;
memories: boolean;
multiConvo: boolean;
agents: boolean;
temporaryChat: boolean;
runCode: boolean;
webSearch: boolean;
peoplePicker: boolean;
marketplace: boolean;
fileSearch: boolean;
fileCitations: boolean;
}Permission Enforcement:
- Loaded from configuration
- Checked at API level
- UI elements conditionally rendered
File: api/strategies/validators.js
Schema: validators.js#L50-L75
const registerSchema = z.object({
email: z.string().email(),
password: z.string()
.min(MIN_PASSWORD_LENGTH)
.max(128)
.refine(val => val.trim() !== ''),
name: z.string().min(3).max(80),
username: z.string()
.min(2).max(80)
.regex(usernamePattern)
.refine(detectInjection)
.refine(val => val.trim() !== '')
});Pattern: validators.js#L12-L25
// Allowed character sets
const allowedCharSets = [
'\\p{Script=Latin}', // Latin
'\\p{Script=Cyrillic}', // Cyrillic
'\\p{Script=Devanagari}', // Devanagari
'\\p{Script=Han}', // Chinese
'\\p{Script=Arabic}', // Arabic
'\\p{Script=Hiragana}', // Japanese
'\\p{Script=Katakana}', // Japanese
'\\p{Script=Hangul}' // Korean
];
const usernamePattern = new RegExp(
`^[${allowedCharSets.join('')}0-9_.-]+$`,
'u'
);Pattern: validators.js#L30-L45
const injectionPatterns = [
"'", "--", // SQL injection
"$ne", "$gt", "$lt", "$or", // NoSQL injection
"{", "}", // Object injection
"*", ";", // Wildcard/command injection
"<", ">", "/", // XSS attempts
"=" // Assignment attempts
];
function detectInjection(value) {
return !injectionPatterns.some(pattern =>
value.includes(pattern)
);
}File: api/server/index.js#L84
app.use(mongoSanitize());Protection:
- Removes
$operators from input - Removes
.from keys - Prevents MongoDB operator injection
File: api/server/routes/files/multer.js
Function: multer.js#L15-L25
function sanitizeFilename(filename) {
return filename
.replace(/[^a-zA-Z0-9._-]/g, '_') // Remove special chars
.replace(/\.{2,}/g, '.') // Prevent path traversal
.substring(0, 255); // Limit length
}Configuration: multer.js#L35-L55
const allowedMimeTypes = {
images: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
documents: ['application/pdf', 'text/plain', 'application/msword'],
audio: ['audio/mpeg', 'audio/wav', 'audio/ogg']
};
fileFilter: (req, file, cb) => {
const endpoint = req.body.endpoint;
const allowed = allowedMimeTypes[endpoint] || [];
if (allowed.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Invalid file type'), false);
}
}Per Endpoint: multer.js#L60-L75
limits: {
fileSize: endpointConfig.fileSizeLimit || 20 * 1024 * 1024 // 20MB default
}File: api/server/services/AuthService.js#L45-L65
const cookieOptions = {
httpOnly: true, // Prevents JavaScript access
secure: isProduction, // HTTPS only in production
sameSite: 'strict', // CSRF protection
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
};
res.cookie('refreshToken', token, cookieOptions);User Data: api/server/controllers/UserController.js#L25-L35
// Exclude sensitive fields from responses
user.select('-password -totpSecret -backupCodes');Middleware: api/server/middleware/requireJwtAuth.js
Token Verification: requireJwtAuth.js#L10-L30
function requireJwtAuth(req, res, next) {
passport.authenticate('jwt', { session: false }, (err, user) => {
if (err || !user) {
return res.status(401).json({
message: 'Unauthorized'
});
}
req.user = user;
next();
})(req, res, next);
}Protected Routes:
/api/user/*- User profile and settings/api/messages/*- Message operations/api/convos/*- Conversation management/api/agents/*- Agent management/api/files/*- File operations- All routes except
/api/auth/*and/api/config
File: api/server/services/UserService.js
Storage: UserService.js#L85-L105
const encryptedKey = encrypt(apiKey);
await UserKey.create({
userId,
key: encryptedKey,
expiresAt: expirationDate
});Retrieval: UserService.js#L120-L135
const userKeys = await UserKey.find({
userId,
expiresAt: { $gt: new Date() }
});
return userKeys.map(k => ({
...k.toObject(),
key: decrypt(k.key)
}));- Configurable expiration dates
- Automatic cleanup of expired keys
- Per-user key isolation
File: api/server/index.js#L85
app.use(cors());Headers:
- Default CORS policy (no restrictions in development)
- Production should configure specific origins
- Credentials support enabled for cookie-based auth
File: packages/api/src/crypto/encryption.ts
Algorithm: AES-256-CTR
Implementation: encryption.ts#L85-L115
function encryptV3(text: string): string {
// Validate 32-byte key (64 hex chars)
if (CREDS_KEY.length !== 64) {
throw new Error('CREDS_KEY must be 64 hex chars (32 bytes)');
}
// Generate random 16-byte IV
const iv = webcrypto.getRandomValues(new Uint8Array(16));
// Create cipher
const cipher = createCipheriv(
'aes-256-ctr',
Buffer.from(CREDS_KEY, 'hex'),
iv
);
// Encrypt
const encrypted = Buffer.concat([
cipher.update(text, 'utf8'),
cipher.final()
]);
// Return format: v3:IV:encrypted_data
return `v3:${iv.toString('hex')}:${encrypted.toString('hex')}`;
}Decryption: encryption.ts#L120-L145
function decryptV3(encryptedText: string): string {
// Parse format: v3:IV:encrypted_data
const [version, ivHex, encryptedHex] = encryptedText.split(':');
const decipher = createDecipheriv(
'aes-256-ctr',
Buffer.from(CREDS_KEY, 'hex'),
Buffer.from(ivHex, 'hex')
);
const decrypted = Buffer.concat([
decipher.update(Buffer.from(encryptedHex, 'hex')),
decipher.final()
]);
return decrypted.toString('utf8');
}Features:
- Random IV per encryption
- 256-bit key strength
- CTR mode (parallelizable)
- Version prefix for migration support
Algorithm: AES-CBC with random IV
Format: IV:encrypted_data (hex-encoded)
Algorithm: AES-CBC with fixed IV Security Risk: Fixed IV allows pattern analysis Recommendation: Migrate to v3
Configuration:
CREDS_KEY=64_character_hex_string # 32 bytes for AES-256
CREDS_IV=32_character_hex_string # 16 bytes (only for v1/v2)Key Validation: encryption.ts#L20-L30
if (!CREDS_KEY || !CREDS_IV) {
throw new Error('CREDS_KEY and CREDS_IV must be set');
}
// v3 requires 32-byte (64 hex char) key
if (useV3 && CREDS_KEY.length !== 64) {
throw new Error('v3 encryption requires 64-character hex key');
}What's Encrypted:
- API keys (OpenAI, Anthropic, etc.)
- OAuth tokens
- Refresh tokens
- User credentials for external services
- LDAP/SAML service account credentials
Storage:
- MongoDB with encrypted fields
- Decrypted only when needed
- Never logged or exposed in responses
File: api/server/services/AuthService.js
Session Model: AuthService.js#L75-L95
const session = await Session.create({
userId: user._id,
expiresAt: new Date(Date.now() + SESSION_EXPIRY * 1000)
});
const refreshToken = jwt.sign(
{ id: session._id },
jwtRefreshSecret,
{ expiresIn: REFRESH_TOKEN_EXPIRY }
);Flow: AuthService.js#L150-L190
async refreshToken(refreshToken) {
// Verify refresh token
const decoded = jwt.verify(refreshToken, jwtRefreshSecret);
// Validate session exists and not expired
const session = await Session.findById(decoded.id);
if (!session || session.expiresAt < new Date()) {
throw new Error('Session expired');
}
// Generate new access token
const newToken = jwt.sign(
{ id: session.userId },
JWT_SECRET,
{ expiresIn: SESSION_EXPIRY }
);
return newToken;
}Logout: AuthService.js#L195-L215
async logoutUser(sessionId) {
// Delete session from database
await Session.deleteOne({ _id: sessionId });
// Clear cookies
res.clearCookie('refreshToken');
res.clearCookie('token_provider');
res.clearCookie('openid_access_token');
res.clearCookie('openid_user_id');
}Automatic Cleanup:
- MongoDB TTL index on
expiresAtfield - Expired sessions automatically deleted
- Orphaned sessions cleaned on user deletion
File: api/server/middleware/limiters/loginLimiter.js
Configuration: loginLimiter.js#L8-L25
const loginLimiter = rateLimit({
windowMs: LOGIN_WINDOW * 60 * 1000, // Default: 5 minutes
max: LOGIN_MAX, // Default: 7 attempts
message: 'Too many login attempts',
standardHeaders: true,
legacyHeaders: false,
handler: async (req, res) => {
await logViolation(req, {
user_id: req.body.email,
type: ViolationTypes.LOGINS,
score: LOGIN_VIOLATION_SCORE
});
res.status(429).json({ message: 'Too many login attempts' });
}
});Environment Variables:
LOGIN_MAX=7 # Max attempts per window
LOGIN_WINDOW=5 # Window in minutes
LOGIN_VIOLATION_SCORE=1 # Severity scoreFile: api/server/middleware/limiters/registerLimiter.js
const registerLimiter = rateLimit({
windowMs: REGISTER_WINDOW * 60 * 1000, // Default: 60 minutes
max: REGISTER_MAX, // Default: 5 attempts
message: 'Too many registration attempts'
});Configuration:
REGISTER_MAX=5
REGISTER_WINDOW=60 # 60 minutesFile: api/server/middleware/limiters/messageLimiters.js
Dual Strategy: messageLimiters.js#L15-L55
// IP-based limiting
if (LIMIT_MESSAGE_IP) {
app.use('/api/messages', ipMessageLimiter);
}
// User-based limiting
if (LIMIT_MESSAGE_USER) {
app.use('/api/messages', userMessageLimiter);
}Configuration:
LIMIT_MESSAGE_IP=true
LIMIT_MESSAGE_USER=true
MESSAGE_MAX=40
MESSAGE_WINDOW=5 # minutes
MESSAGE_VIOLATION_SCORE=1File: api/cache/banViolation.js
Trigger: banViolation.js#L25-L45
// Ban when violations reach multiples of BAN_INTERVAL
if (BAN_VIOLATIONS && violationCount % BAN_INTERVAL === 0) {
await banUser({
userId,
ipAddress: req.ip,
duration: BAN_DURATION
});
}Middleware: api/server/middleware/checkBan.js
Check: checkBan.js#L15-L45
async function checkBan(req, res, next) {
// Check user ban
const userBan = await Ban.findOne({
userId: req.user.id,
expiresAt: { $gt: new Date() }
});
// Check IP ban
const ipBan = await Ban.findOne({
ipAddress: req.ip,
expiresAt: { $gt: new Date() }
});
if (userBan || ipBan) {
return res.status(403).json({
message: 'Account temporarily banned',
expiresAt: (userBan || ipBan).expiresAt
});
}
next();
}Configuration:
BAN_VIOLATIONS=true # Enable auto-ban
BAN_INTERVAL=20 # Violations before ban
BAN_DURATION=600000 # 10 minutes in msFile Upload: api/server/middleware/limiters/uploadLimiters.js
- Per-IP and per-user limits
- Separate limits for different file types
Password Reset: api/server/middleware/limiters/resetPasswordLimiter.js
- Prevents brute-force password reset attacks
Email Verification: api/server/middleware/limiters/verifyEmailLimiter.js
- Rate limits verification attempts
Tool Calls: api/server/middleware/limiters/toolCallLimiter.js
- Prevents excessive tool/plugin usage
TTS/STT: api/server/middleware/limiters/ttsLimiters.js
- Rate limits speech synthesis/recognition
Registration Data: api/server/services/AuthService.js#L245-L270
const user = await User.create({
email,
password: hashedPassword,
name,
username,
provider: 'local',
role: isFirstUser ? 'ADMIN' : 'USER',
emailVerified: false
});OAuth Data: api/strategies/googleStrategy.js#L35-L55
// Only essential OAuth profile data
{
email: profile.emails[0].value,
name: profile.displayName,
username: profile.username || email,
picture: profile.photos[0].value,
emailVerified: profile.emails[0].verified,
provider: 'google'
}User Queries: api/server/controllers/UserController.js#L25
const user = await User.findById(userId)
.select('-password -totpSecret -backupCodes -__v');Excluded Fields:
password- Never returned in responsestotpSecret- 2FA secretbackupCodes- Hashed backup codes__v- MongoDB version key
Logging: api/config/parsers.js#L8-L15
const redactionPatterns = [
/sk-[^\s]+/g, // OpenAI keys
/Bearer\s+[^\s]+/gi, // Bearer tokens
/apikey[=:]\s*[^\s&]+/gi, // API keys
/key=[^&\s]+/gi // Query keys
];
function redactSensitiveData(text) {
return redactionPatterns.reduce(
(str, pattern) => str.replace(pattern, '[REDACTED]'),
text
);
}File: librechat.example.yaml
Configuration:
# Terms of Service
termsOfService:
modalAcceptance: true
modalTitle: "Terms of Service"
modalContent: |
# Terms of Service
By using this service, you agree to...
# Privacy Policy
privacyPolicy:
externalUrl: "https://example.com/privacy"
openNewTab: trueAcceptance Tracking: api/server/controllers/UserController.js#L185-L205
async acceptTerms(req, res) {
const user = await User.findByIdAndUpdate(
req.user.id,
{ termsAccepted: true, termsAcceptedAt: new Date() },
{ new: true }
);
res.json({ termsAccepted: true });
}Configuration: librechat.example.yaml#L45-L55
registration:
allowedDomains:
- "example.com"
- "company.com"Validation: Domain checked during registration Purpose: Restrict registrations to authorized domains
- Transactional emails only (verification, password reset)
- No marketing emails without explicit opt-in
- Email service configurable
- Essential cookies only (authentication, session)
- No tracking cookies
- HttpOnly, Secure, SameSite flags set
File: api/server/controllers/UserController.js
Complete Data Deletion: UserController.js#L45-L125
async deleteUserController(req, res) {
const userId = req.user.id;
// 1. Delete all user messages
await Message.deleteMany({ user: userId });
// 2. Delete all sessions
await Session.deleteMany({ userId });
// 3. Delete transactions
await Transaction.deleteMany({ user: userId });
// 4. Delete API keys
await UserKey.deleteMany({ userId });
// 5. Delete balance records
await Balance.deleteMany({ user: userId });
// 6. Delete presets
await Preset.deleteMany({ user: userId });
// 7. Delete conversations
await Conversation.deleteMany({ user: userId });
// 8. Delete plugin auth
await PluginAuth.deleteMany({ userId });
// 9. Delete shared links
await SharedLink.deleteMany({ userId });
// 10. Delete files from storage
await deleteUserFiles(userId);
// 11. Delete file records
await File.deleteMany({ userId });
// 12. Delete tool calls
await ToolCall.deleteMany({ userId });
// 13. Delete agents
await Agent.deleteMany({ author: userId });
// 14. Delete assistants
await Assistant.deleteMany({ author: userId });
// 15. Delete conversation tags
await ConversationTag.deleteMany({ user: userId });
// 16. Delete memory entries
await Memory.deleteMany({ userId });
// 17. Delete prompts
await Prompt.deleteMany({ author: userId });
// 18. Delete actions
await Action.deleteMany({ userId });
// 19. Delete OAuth tokens
await OAuthToken.deleteMany({ userId });
// 20. Remove from groups
await Group.updateMany(
{ members: userId },
{ $pull: { members: userId } }
);
// 21. Delete ACL entries
await AccessControl.deleteMany({ principalId: userId });
// 22. Delete user account
await User.deleteOne({ _id: userId });
// 23. Log deletion
logger.info(`User deleted`, {
email: req.user.email,
id: userId
});
res.json({ message: 'Account and all data deleted' });
}User Data Export: api/server/controllers/UserController.js#L135-L165
async getUserData(req, res) {
const user = await User.findById(req.user.id)
.select('-password -totpSecret -backupCodes');
const conversations = await Conversation.find({ user: req.user.id });
const messages = await Message.find({ user: req.user.id });
const agents = await Agent.find({ author: req.user.id });
const prompts = await Prompt.find({ author: req.user.id });
res.json({
user,
conversations,
messages,
agents,
prompts,
exportedAt: new Date()
});
}Profile Update: api/server/controllers/UserController.js#L170-L185
async updateUserProfile(req, res) {
const allowedUpdates = ['name', 'username', 'avatar'];
const updates = Object.keys(req.body)
.filter(key => allowedUpdates.includes(key))
.reduce((obj, key) => {
obj[key] = req.body[key];
return obj;
}, {});
const user = await User.findByIdAndUpdate(
req.user.id,
updates,
{ new: true, runValidators: true }
).select('-password -totpSecret -backupCodes');
res.json(user);
}Token Expiration:
// Email verification: 15 minutes
expiresIn: 900
// Password reset: 15 minutes
expiresIn: 900
// Access tokens: 15 minutes (configurable)
SESSION_EXPIRY: 900
// Refresh tokens: 7 days (configurable)
REFRESH_TOKEN_EXPIRY: 604800
// Invite tokens: 7 days
expiresIn: 604800Automatic Cleanup:
- MongoDB TTL indexes on expiring documents
- Session cleanup on logout
- Expired token cleanup via TTL
What Data is Collected:
- Account Data: Email, name, username, password (hashed)
- Usage Data: Conversations, messages, token usage
- Authentication Data: Sessions, OAuth tokens
- Preferences: Settings, model selections
- Security Data: Login attempts, violations, bans
What Data is NOT Collected:
- Browsing history
- Third-party analytics (unless GTM explicitly configured)
- Marketing data
- Geolocation (except IP for rate limiting)
Third-Party Sharing:
// No data shared with third parties except:
// 1. AI providers (OpenAI, Anthropic, etc.) - only messages
// 2. OAuth providers (for authentication only)
// 3. Email service (for transactional emails only)
// 4. Storage providers (S3, Firebase) - only filesNo Selling:
- User data is never sold
- No advertising partnerships
- No data brokering
# JWT Secrets (REQUIRED)
JWT_SECRET=your-strong-random-secret-here
JWT_REFRESH_SECRET=another-strong-random-secret
# Encryption Keys (REQUIRED)
CREDS_KEY=64_character_hex_string # 32 bytes for AES-256
CREDS_IV=32_character_hex_string # 16 bytes (legacy only)
# Session Configuration
SESSION_EXPIRY=900 # 15 minutes
REFRESH_TOKEN_EXPIRY=604800 # 7 days
# Password Policy
MIN_PASSWORD_LENGTH=8
# Rate Limiting
LOGIN_MAX=7
LOGIN_WINDOW=5
REGISTER_MAX=5
REGISTER_WINDOW=60
MESSAGE_MAX=40
MESSAGE_WINDOW=5
# Auto-Ban
BAN_VIOLATIONS=true
BAN_INTERVAL=20
BAN_DURATION=600000 # 10 minutes
# Security Features
ALLOW_PASSWORD_RESET=false
ALLOW_EMAIL_LOGIN=true
ALLOW_UNVERIFIED_EMAIL_LOGIN=false
ALLOW_SOCIAL_LOGIN=true
# Trust Proxy (for reverse proxies)
TRUST_PROXY=1
# Email (for verification and password reset)
EMAIL_SERVICE=gmail
[email protected]
EMAIL_PASSWORD=your-app-password
[email protected]Recommended Headers (not all implemented):
// Implement in production:
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));Production:
- HTTPS enforced via
secure: truecookies - TLS 1.2+ recommended
- Strong cipher suites
- HSTS header (via Helmet)
Development:
- HTTP allowed
secure: falsecookies- Self-signed certificates acceptable
-
Multi-Factor Authentication
- 7 authentication methods
- TOTP-based 2FA with backup codes
- Secure token generation
-
Modern Encryption
- AES-256-CTR (recommended v3)
- Random IV per encryption
- Secure key management
-
Comprehensive Rate Limiting
- 10+ specialized rate limiters
- Auto-ban system
- Violation tracking
-
GDPR Compliance
- Complete data deletion
- Data export capability
- Terms of service tracking
-
Input Validation
- Zod schema validation
- MongoDB sanitization
- Injection detection
-
Session Security
- Short-lived access tokens
- Secure refresh tokens
- HttpOnly, Secure, SameSite cookies
-
RBAC & Permissions
- Role-based access control
- Granular permissions
- ACL system
-
CSRF Protection
- Current: Relies on SameSite cookies
- Recommendation: Add explicit CSRF tokens for state-changing operations
-
Security Headers
- Missing: Content-Security-Policy (CSP)
- Missing: X-Frame-Options
- Missing: X-Content-Type-Options
- Recommendation: Implement Helmet.js with full configuration
-
Rate Limit Configuration
- Current: Static configuration
- Recommendation: Dynamic rate limits based on user reputation
-
Audit Logging
- Current: Basic logging
- Recommendation: Comprehensive audit trail for sensitive operations
-
Password Reset
- Current: Disabled by default (good!)
- Recommendation: Add additional verification steps when enabled
-
API Key Rotation
- Current: Manual rotation only
- Recommendation: Automated key rotation policies
-
Penetration Testing
- Recommendation: Regular security audits
- Recommendation: Bug bounty program
- ✅ Password hashing with bcrypt (salt rounds = 10)
- ✅ Token-based authentication (JWT)
- ✅ Secure session management
- ✅ Input validation and sanitization
- ✅ Rate limiting on sensitive endpoints
- ✅ MongoDB injection prevention
- ✅ XSS prevention via HttpOnly cookies
- ✅ Encrypted credential storage
- ✅ HTTPS support in production
- ✅ Secure file upload handling
- ✅ OAuth 2.0 implementation
- ✅ Two-factor authentication
- ✅ RBAC and permissions
- ✅ Auto-ban for violations
LibreChat demonstrates a strong security posture with:
- 7 authentication methods (Local, JWT, OAuth, LDAP, SAML, OpenID, 2FA)
- Modern cryptographic implementations
- Secure token lifecycle management
- Role-based access control (RBAC)
- Granular permission system
- Access control lists (ACL)
- AES-256-CTR encryption for credentials
- bcrypt password hashing
- HttpOnly, Secure, SameSite cookies
- PII protection and redaction
- Comprehensive rate limiting
- Auto-ban system for violations
- Input validation with injection prevention
- Secure file upload handling
- Session management with token rotation
- GDPR-compliant data deletion
- Data export capability
- Terms of service tracking
- Minimal data collection
- No third-party data sharing (except necessary services)
- Add explicit CSRF tokens
- Implement full CSP headers
- Add comprehensive audit logging
- Enable automated key rotation
- Conduct regular security audits
Overall Security Rating: ⭐⭐⭐⭐☆ (4/5)
- Strong foundation with modern practices
- Minor enhancements needed for enterprise-grade security
- Excellent privacy and GDPR compliance
Last Updated: 2025-11-24 Security Contact: See SECURITY.md Vulnerability Reporting: GitHub Security Advisories