feat(security): enhance input validation and security measures across various routes
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m38s
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m38s
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user