feat(calendar): store only token hash in database to enhance security
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m25s
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m25s
feat(federation): escape LIKE special characters in originDomain to prevent wildcard injection feat(oauth): redirect with token in hash fragment to avoid exposure in logs feat(OAuthCallback): retrieve token from hash fragment for improved security
This commit is contained in:
@@ -751,8 +751,9 @@ router.post('/caldav-tokens', authenticateToken, async (req, res) => {
|
||||
const token = crypto.randomBytes(32).toString('hex');
|
||||
const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
|
||||
const result = await db.run(
|
||||
'INSERT INTO caldav_tokens (user_id, token, token_hash, name) VALUES (?, ?, ?, ?)',
|
||||
[req.user.id, token, tokenHash, name.trim()],
|
||||
// Store only the hash — never the plaintext — to limit exposure on DB breach.
|
||||
'INSERT INTO caldav_tokens (user_id, token, token_hash, name) VALUES (?, NULL, ?, ?)',
|
||||
[req.user.id, tokenHash, name.trim()],
|
||||
);
|
||||
res.status(201).json({
|
||||
token: { id: result.lastInsertRowid, name: name.trim() },
|
||||
|
||||
@@ -577,6 +577,9 @@ router.post('/calendar-event-deleted', federationReceiveLimiter, async (req, res
|
||||
|
||||
const db = getDb();
|
||||
|
||||
// Escape LIKE special characters in originDomain to prevent wildcard injection.
|
||||
const safeDomain = originDomain.replace(/[\\%_]/g, '\\$&');
|
||||
|
||||
// Collect all affected users before deleting (for email notifications)
|
||||
let affectedUsers = [];
|
||||
try {
|
||||
@@ -585,16 +588,16 @@ router.post('/calendar-event-deleted', federationReceiveLimiter, async (req, res
|
||||
`SELECT u.email, u.name, u.language, ci.title, ci.from_user
|
||||
FROM calendar_invitations ci
|
||||
JOIN users u ON ci.to_user_id = u.id
|
||||
WHERE ci.event_uid = ? AND ci.from_user LIKE ?`,
|
||||
[event_uid, `%@${originDomain}`]
|
||||
WHERE ci.event_uid = ? AND ci.from_user LIKE ? ESCAPE '\\'`,
|
||||
[event_uid, `%@${safeDomain}`]
|
||||
);
|
||||
// Users who already accepted (event in their calendar)
|
||||
const calUsers = await db.all(
|
||||
`SELECT u.email, u.name, u.language, ce.title, ce.federated_from AS from_user
|
||||
FROM calendar_events ce
|
||||
JOIN users u ON ce.user_id = u.id
|
||||
WHERE ce.uid = ? AND ce.federated_from LIKE ?`,
|
||||
[event_uid, `%@${originDomain}`]
|
||||
WHERE ce.uid = ? AND ce.federated_from LIKE ? ESCAPE '\\'`,
|
||||
[event_uid, `%@${safeDomain}`]
|
||||
);
|
||||
// Merge, deduplicate by email
|
||||
const seen = new Set();
|
||||
@@ -609,15 +612,15 @@ router.post('/calendar-event-deleted', federationReceiveLimiter, async (req, res
|
||||
// Remove from calendar_invitations for all users on this instance
|
||||
await db.run(
|
||||
`DELETE FROM calendar_invitations
|
||||
WHERE event_uid = ? AND from_user LIKE ?`,
|
||||
[event_uid, `%@${originDomain}`]
|
||||
WHERE event_uid = ? AND from_user LIKE ? ESCAPE '\\'`,
|
||||
[event_uid, `%@${safeDomain}`]
|
||||
);
|
||||
|
||||
// Remove from calendar_events (accepted invitations) for all users on this instance
|
||||
await db.run(
|
||||
`DELETE FROM calendar_events
|
||||
WHERE uid = ? AND federated_from LIKE ?`,
|
||||
[event_uid, `%@${originDomain}`]
|
||||
WHERE uid = ? AND federated_from LIKE ? ESCAPE '\\'`,
|
||||
[event_uid, `%@${safeDomain}`]
|
||||
);
|
||||
|
||||
log.federation.info(`Calendar event ${event_uid} deleted (origin: ${originDomain})`);
|
||||
@@ -667,11 +670,13 @@ router.post('/room-deleted', federationReceiveLimiter, async (req, res) => {
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
// Escape LIKE special characters in originDomain to prevent wildcard injection.
|
||||
const safeDomain = originDomain.replace(/[\\%_]/g, '\\$&');
|
||||
// Mark all federated_rooms with this meet_id (room_uid) from this origin as deleted
|
||||
await db.run(
|
||||
`UPDATE federated_rooms SET deleted = 1, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE meet_id = ? AND from_user LIKE ?`,
|
||||
[room_uid, `%@${originDomain}`]
|
||||
WHERE meet_id = ? AND from_user LIKE ? ESCAPE '\\'`,
|
||||
[room_uid, `%@${safeDomain}`]
|
||||
);
|
||||
|
||||
log.federation.info(`Room ${room_uid} marked as deleted (origin: ${originDomain})`);
|
||||
|
||||
@@ -248,9 +248,10 @@ router.get('/callback', callbackLimiter, async (req, res) => {
|
||||
// Generate JWT
|
||||
const token = generateToken(user.id);
|
||||
|
||||
// Redirect to frontend callback page with token
|
||||
// Redirect to frontend callback page with token.
|
||||
// Use a hash fragment so the token is never sent to the server (not logged, not in Referer headers).
|
||||
const returnTo = stateData.return_to || '/dashboard';
|
||||
res.redirect(`${baseUrl}/oauth/callback?token=${encodeURIComponent(token)}&return_to=${encodeURIComponent(returnTo)}`);
|
||||
res.redirect(`${baseUrl}/oauth/callback#token=${encodeURIComponent(token)}&return_to=${encodeURIComponent(returnTo)}`);
|
||||
} catch (err) {
|
||||
log.auth.error(`OAuth callback error: ${err.message}`);
|
||||
const baseUrl = getBaseUrl(req);
|
||||
|
||||
Reference in New Issue
Block a user