Files
Wallos/endpoints/subscription/exportcalendar.php
Miguel Ribeiro 800241a265 Fix GHSA-q2r8-m9wm-5547: escape iCal property values to prevent CRLF injection
Raw CRLF in subscription name/notes/url passes through validate()
untouched (it only escapes HTML metacharacters) and was interpolated
directly into .ics VEVENT properties, letting an unescaped newline
terminate the current property and inject arbitrary calendar events.

Adds includes/ical_helper.php::icalEscape() (RFC 5545 value escaping)
and applies it to every user-controlled value embedded in both
iCal export endpoints, rather than stripping newlines in the shared
validate() sanitizer (which would break legitimate multi-line notes
used elsewhere in the app).
2026-07-11 23:27:59 +02:00

84 lines
3.2 KiB
PHP

<?php
require_once '../../includes/connect_endpoint.php';
require_once '../../includes/validate_endpoint.php';
require_once '../../includes/getdbkeys.php';
require_once '../../includes/ical_helper.php';
$postData = file_get_contents("php://input");
$data = json_decode($postData, true);
$id = $data['id'];
$stmt = $db->prepare('SELECT * FROM subscriptions WHERE id = :id AND user_id = :userId');
$stmt->bindParam(':id', $id, SQLITE3_INTEGER);
$stmt->bindParam(':userId', $_SESSION['userId'], SQLITE3_INTEGER);
$result = $stmt->execute();
if ($result === false) {
die(json_encode([
'success' => false,
'message' => "Subscription not found"
]));
}
$subscription = $result->fetchArray(SQLITE3_ASSOC); // Fetch the subscription details as an associative array
if ($subscription) {
$subscription['payer_user'] = $members[$subscription['payer_user_id']]['name'];
$subscription['category'] = $categories[$subscription['category_id']]['name'];
$subscription['payment_method'] = $payment_methods[$subscription['payment_method_id']]['name'];
$subscription['currency'] = $currencies[$subscription['currency_id']]['symbol'];
$subscription['trigger'] = $subscription['notify_days_before'] ? $subscription['notify_days_before'] : 1;
$subscription['price'] = number_format($subscription['price'], 2);
// Create ICS from subscription information
$uid = 'wallos-subscription-' . $subscription['id'] . '@wallos';
$summary = icalEscape(html_entity_decode($subscription['name'], ENT_QUOTES, 'UTF-8'));
$notes = icalEscape(html_entity_decode($subscription['notes'], ENT_QUOTES, 'UTF-8'));
$category = icalEscape($subscription['category']);
$paymentMethod = icalEscape($subscription['payment_method']);
$payer = icalEscape($subscription['payer_user']);
$description = "Price: {$subscription['currency']}{$subscription['price']}\\nCategory: {$category}\\nPayment Method: {$paymentMethod}\\nPayer: {$payer}\\n\\nNotes: {$notes}";
$dtstamp = gmdate('Ymd\THis\Z');
$dtstart = (new DateTime($subscription['next_payment']))->format('Ymd');
$dtend = (new DateTime($subscription['next_payment']))->format('Ymd');
$location = icalEscape(isset($subscription['url']) ? $subscription['url'] : '');
$alarm_trigger = '-P' . $subscription['trigger'] . 'D';
$icsContent = <<<ICS
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Your Organization//Your Application//EN
CALSCALE:GREGORIAN
METHOD:PUBLISH
BEGIN:VEVENT
UID:$uid
DTSTAMP:$dtstamp
SUMMARY:$summary
DESCRIPTION:$description
DTSTART;VALUE=DATE:$dtstart
DTEND;VALUE=DATE:$dtend
LOCATION:$location
STATUS:CONFIRMED
TRANSP:OPAQUE
BEGIN:VALARM
ACTION:DISPLAY
DESCRIPTION:Reminder
TRIGGER:$alarm_trigger
END:VALARM
END:VEVENT
END:VCALENDAR
ICS;
echo json_encode([
'success' => true,
'ics' => $icsContent,
'name' => $subscription['name']
]);
} else {
echo json_encode([
'success' => false,
'message' => "Subscription not found"
]);
}