On 11 October 2023, ESET researchers watching a cyber-espionage group known as Winter Vivern saw it start exploiting a new flaw in Roundcube Webmail, the open-source webmail used by universities, hosting providers and government bodies across Europe. The attack was an email. The victim did not click a link, open an attachment or approve anything. Viewing the message in a browser was enough for the attacker's JavaScript to run inside the victim's Roundcube session, list their folders, and send their email to a server the group controlled.
The flaw is CVE-2023-5631. It was in a component whose only job was to make untrusted HTML safe to display, and it is in CISA's Known Exploited Vulnerabilities catalogue. Its CVSS rating is 5.4 on the National Vulnerability Database — medium. That gap between the score and what actually happened is the reason this post exists.
- CVE
- CVE-2023-5631
- Product
- Roundcube Webmail before 1.4.15, 1.5.x before 1.5.5, and 1.6.x before 1.6.4. Fixed in releases published 16 October 2023.
- Class
- Stored cross-site scripting (CWE-79)
- Severity
- CVSS 3.1 base score 5.4 (NVD) and 6.1 (ESET as CNA) — both Medium
- Disclosed
- Reported by ESET on 12 October 2023 after exploitation in the wild began on 11 October; added to CISA's Known Exploited Vulnerabilities catalogue on 26 October 2023
- Impact
- A crafted SVG inside an HTML email was not properly neutralised by the server-side sanitiser, so JavaScript ran in the victim's session when the message was viewed. In the observed campaign it was used to read and exfiltrate mailboxes belonging to European government bodies and a think tank.
The root cause, in one sentence
Data that an attacker controlled was handed to a browser in a context where the browser treats it as code.
Every variant of cross-site scripting reduces to that. The Roundcube case is instructive because the team had done the thing everybody recommends: email bodies did pass through a dedicated sanitiser before display. The sanitiser simply did not account for every way a document format — here, SVG embedded in HTML — can carry script. Sanitising is a model of what the browser will do with a string, and the browser is a far larger and stranger program than any allowlist written against it. The defences that hold are the ones that do not depend on that model being complete: encoding output for the exact context it lands in, keeping untrusted markup out of the application's origin, and a content security policy that refuses to run what nobody intended to ship.
No payloads appear here. As in the other posts in this series, the mechanism is described to the depth a defender needs and no further. If you run Roundcube, work from the project's release notice and make sure you are on a current release — Roundcube has had further XSS fixes since, and the same group exploited an older Roundcube XSS, CVE-2020-35730, in the months before this one.
The five shapes it takes
Reflected XSS from a search box is the version in every training course, and modern frameworks have largely closed it. The XSS we still report is in the places frameworks cannot reason about for you.
1. Rich content that has to be rendered
This is the Roundcube shape. Email bodies, comments with formatting, markdown, WYSIWYG editors, imported documents, support tickets that display what the customer pasted. The application must display untrusted markup, so it cannot simply escape it, and it relies on a sanitiser instead. The questions we ask are which sanitiser, which version, and configured how — and whether the content goes through a second transformation after sanitising, such as a markdown renderer or a template, which quietly undoes the cleaning. Where it can be done, the strongest answer is to render untrusted content in a sandboxed frame on a separate origin, so that even a sanitiser bypass runs in a place with nothing worth stealing.
2. Files the browser will interpret
SVG images, HTML exports, PDFs with active content, and uploads served back with a content type the browser decides to render. An avatar upload that accepts SVG and serves it from the main application domain is a stored XSS waiting for one attacker to notice. Files a user uploaded should be served from a separate domain, with a content type set by the server rather than inferred, and with headers that force download for anything that is not meant to be displayed inline.
3. The framework's escape hatch
React, Angular and Vue all encode output by default, and all of them provide a way to switch that off — for the marketing team's HTML snippet, the CMS field, the translated string with a link in it. Those escape hatches are where we find XSS in otherwise well-built single-page applications, together with values written into href and src attributes, where a correctly encoded string can still be a javascript: URL. A codebase search for the escape-hatch function names is one of the most productive half-hours in a review.
4. DOM sinks fed from the URL and from messages
DOM-based XSS never touches your server, so server-side scanning and logging never see it. The source is the URL fragment, a query parameter read by client-side code, a value from storage, or a postMessage from another window; the sink is anything that turns a string into markup or code. Third-party widgets and analytics snippets that listen for cross-window messages without checking where they came from are a recurring source. Finding this class means reading the JavaScript the application actually ships, not only the requests it sends.
5. Second-order and cross-surface rendering
A value is stored safely in one place and rendered unsafely somewhere else. A customer's display name is escaped on the customer-facing page and then rendered raw in the internal admin dashboard, the support tool, the PDF invoice generator or the email notification. This one is particularly damaging because the person viewing it is usually staff with higher privileges, so a script planted by an ordinary user runs as an administrator. We test every user-controllable field on every surface that later displays it, not just the page where it was entered.
The reason we argue about the rating. CVSS scores most XSS as medium because it requires a victim to view something and is assumed to have limited impact. But JavaScript running in a user's session can do anything that user can do: read their data, submit forms as them, change their email address and trigger a password reset, or use their open session to reach internal functions. CVE-2023-5631 needed no click beyond opening a message, and its impact was the contents of government mailboxes. We rate XSS by what the victim's session can reach in your application — and stored XSS that executes in an administrator's browser is rated accordingly.
How we test for it
- Map every place user-controllable data is displayed, not just where it is entered. We follow each field from where it is written to every page, export, email, notification and admin view that renders it. The second-order cases in shape five are only found this way.
- Identify the context before choosing an input. A value inside HTML text, inside an attribute, inside a URL, inside a script block and inside a style block each need different encoding. We establish where the value lands and how it is encoded there, then test whether that encoding is the right one for that context — a benign marker first, never a payload that does anything.
- Test the sanitiser as a component, not as a checkbox. For rich-content features we identify the sanitisation library and its version, check it against published bypasses, and test the formats it may not model: SVG, MathML, nested and malformed markup, and content that is transformed again after cleaning.
- Upload what the browser will render. Every upload feature gets files whose type, extension and declared content type disagree, to establish how the server stores them and how the browser treats them when they are served back.
- Read the shipped JavaScript. We trace sources such as the URL, storage and cross-window messages to sinks that create markup or execute code, in the application's own bundles and in the third-party scripts it loads. This is where DOM-based XSS lives.
- Assess the defences that limit the damage. A content security policy, cookie flags, a separate domain for user content, and framework settings. Where XSS is confirmed, we demonstrate impact with a harmless proof — that script ran, and what the session could reach — without taking any action as the victim, and within limits agreed before testing starts.
Each finding is written up with the request, the evidence, and the specific change that fixes it rather than a generic "sanitise input". That is the standard described in what's in a penetration test report.
Six things to check in your own application this week
- Search your front-end code for your framework's raw-HTML escape hatch, and for values written into
href,srcand event-handler attributes. Every hit needs a reason and an owner. - Find every feature that displays user-supplied rich content, and confirm which sanitiser it uses, that the library is current, and that nothing transforms the content after it has been cleaned.
- Serve user uploads from a separate domain, set the content type on the server, and force download for anything not meant to be displayed. Decide deliberately whether you accept SVG at all.
- Check the views your staff use — admin dashboards, support tools, generated PDFs and notification emails — for user-controlled fields rendered without the same encoding as the customer-facing pages.
- Deploy a content security policy that blocks inline script, starting in report-only mode so you can see what would break. A policy that allows inline script does little against XSS.
- Patch the third-party web applications you host — webmail, CMSs, help desks, wikis. CVE-2023-5631 was in a product, not in anybody's own code, and it was exploited within days of being found.
Cross-site scripting, access control and business logic all reward someone reasoning about how your application actually behaves rather than matching it against a signature list. That is the gap our testing is built around — fixed price, agreed before we start, with the retest included.
References
- NVD — CVE-2023-5631 detail
- Roundcube — Security update 1.6.4 released
- ESET Research — Winter Vivern exploits zero-day vulnerability in Roundcube Webmail servers
- CISA — Known Exploited Vulnerabilities catalogue
- MITRE — CWE-79: Improper neutralization of input during web page generation
- OWASP — Cross Site Scripting Prevention Cheat Sheet
- OWASP — DOM based XSS Prevention Cheat Sheet