feat(security): enhance input validation and security measures across various routes
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m38s

This commit is contained in:
2026-03-04 08:39:29 +01:00
parent ba096a31a2
commit e22a895672
13 changed files with 222 additions and 29 deletions

View File

@@ -192,16 +192,28 @@ async function caldavAuth(req, res, next) {
res.set('WWW-Authenticate', 'Basic realm="Redlight CalDAV"');
return res.status(401).end();
}
// Hash the provided token with SHA-256 for constant-time comparison in SQL
const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
const tokenRow = await db.get(
'SELECT * FROM caldav_tokens WHERE user_id = ? AND token_hash = ?',
[user.id, tokenHash],
);
// Fallback: also check legacy plaintext tokens for backward compatibility
const tokenRowLegacy = !tokenRow ? await db.get(
'SELECT * FROM caldav_tokens WHERE user_id = ? AND token = ?',
[user.id, token],
);
if (!tokenRow) {
) : null;
const matchedToken = tokenRow || tokenRowLegacy;
if (!matchedToken) {
res.set('WWW-Authenticate', 'Basic realm="Redlight CalDAV"');
return res.status(401).end();
}
// Migrate legacy plaintext token to hashed version
if (tokenRowLegacy && !tokenRow) {
db.run('UPDATE caldav_tokens SET token_hash = ?, token = NULL WHERE id = ?', [tokenHash, matchedToken.id]).catch(() => {});
}
// Update last_used_at (fire and forget)
db.run('UPDATE caldav_tokens SET last_used_at = CURRENT_TIMESTAMP WHERE id = ?', [tokenRow.id]).catch(() => {});
db.run('UPDATE caldav_tokens SET last_used_at = CURRENT_TIMESTAMP WHERE id = ?', [matchedToken.id]).catch(() => {});
req.caldavUser = user;
next();
} catch (err) {
@@ -218,6 +230,15 @@ function setDAVHeaders(res) {
res.set('MS-Author-Via', 'DAV');
}
// ── CalDAV username authorization ──────────────────────────────────────────
// Ensures the :username param matches the authenticated user's email
function validateCalDAVUser(req, res, next) {
if (req.params.username && decodeURIComponent(req.params.username) !== req.caldavUser.email) {
return res.status(403).end();
}
next();
}
// ── Base URL helper ────────────────────────────────────────────────────────
function baseUrl(req) {
const proto = req.get('x-forwarded-proto') || req.protocol;
@@ -280,7 +301,7 @@ router.all('/', caldavAuth, async (req, res) => {
// ── PROPFIND /{username}/ ──────────────────────────────────────────────────
// User principal: tells the client where the calendar home is.
router.all('/:username/', caldavAuth, async (req, res) => {
router.all('/:username/', caldavAuth, validateCalDAVUser, async (req, res) => {
if (req.method !== 'PROPFIND') {
setDAVHeaders(res);
return res.status(405).end();
@@ -302,7 +323,7 @@ router.all('/:username/', caldavAuth, async (req, res) => {
});
// ── PROPFIND + REPORT /{username}/calendar/ ────────────────────────────────
router.all('/:username/calendar/', caldavAuth, async (req, res) => {
router.all('/:username/calendar/', caldavAuth, validateCalDAVUser, async (req, res) => {
const db = getDb();
const calendarHref = `/caldav/${encodeURIComponent(req.caldavUser.email)}/calendar/`;
@@ -411,7 +432,7 @@ router.all('/:username/calendar/', caldavAuth, async (req, res) => {
});
// ── GET /{username}/calendar/{uid}.ics ────────────────────────────────────
router.get('/:username/calendar/:filename', caldavAuth, async (req, res) => {
router.get('/:username/calendar/:filename', caldavAuth, validateCalDAVUser, async (req, res) => {
const uid = req.params.filename.replace(/\.ics$/, '');
const db = getDb();
const ev = await db.get(
@@ -426,7 +447,7 @@ router.get('/:username/calendar/:filename', caldavAuth, async (req, res) => {
});
// ── PUT /{username}/calendar/{uid}.ics — create or update ─────────────────
router.put('/:username/calendar/:filename', caldavAuth, async (req, res) => {
router.put('/:username/calendar/:filename', caldavAuth, validateCalDAVUser, async (req, res) => {
const uid = req.params.filename.replace(/\.ics$/, '');
const body = typeof req.body === 'string' ? req.body : '';
@@ -488,7 +509,7 @@ router.put('/:username/calendar/:filename', caldavAuth, async (req, res) => {
});
// ── DELETE /{username}/calendar/{uid}.ics ─────────────────────────────────
router.delete('/:username/calendar/:filename', caldavAuth, async (req, res) => {
router.delete('/:username/calendar/:filename', caldavAuth, validateCalDAVUser, async (req, res) => {
const uid = req.params.filename.replace(/\.ics$/, '');
const db = getDb();
const ev = await db.get(