VYPR
Moderate severityNVD Advisory· Published Feb 25, 2022· Updated Apr 23, 2025

Cross-site Scripting in Weblate

CVE-2022-24710

Description

Weblate is a copyleft software web-based continuous localization system. Versions prior to 4.11 do not properly neutralize user input used in user name and language fields. Due to this improper neutralization it is possible to perform cross-site scripting via these fields. The issues were fixed in the 4.11 release. Users unable to upgrade are advised to add their own neutralize logic.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
WeblatePyPI
< 4.114.11

Affected products

1

Patches

3
f6753a1a1c63

translate: Add missing escaping to language name

https://github.com/WeblateOrg/weblateMichal ČihařFeb 22, 2022via ghsa
1 file changed · +2 1
  • weblate/trans/forms.py+2 1 modified
    @@ -37,6 +37,7 @@
     from django.template.loader import render_to_string
     from django.urls import reverse
     from django.utils import timezone
    +from django.utils.html import escape
     from django.utils.http import urlencode
     from django.utils.safestring import mark_safe
     from django.utils.translation import gettext
    @@ -318,7 +319,7 @@ def render(self, name, value, attrs=None, renderer=None, **kwargs):
                 # Render textare
                 textarea = super().render(fieldname, val, attrs, renderer, **kwargs)
                 # Label for plural
    -            label = str(unit.translation.language)
    +            label = escape(unit.translation.language)
                 if len(values) != 1:
                     label = f"{label}, {plural.get_plural_label(idx)}"
                 ret.append(
    
9e19a8414337

js: Add missing escaping to username completion

https://github.com/WeblateOrg/weblateMichal ČihařFeb 22, 2022via ghsa
1 file changed · +3 1
  • weblate/static/loader-bootstrap.js+3 1 modified
    @@ -1131,7 +1131,9 @@ $(function () {
           return "";
         },
         menuItemTemplate: function (item) {
    -      return `<a>${item.string}</a>`;
    +      let link = document.createElement("a");
    +      link.innerText = item.string;
    +      return link.outerHTML;
         },
         values: (text, callback) => {
           $.ajax({
    
22d577b1f1e8

reports: Escape user names in generated reports

https://github.com/WeblateOrg/weblateMichal ČihařFeb 20, 2022via ghsa
2 files changed · +29 11
  • weblate/trans/tests/test_reports.py+21 6 modified
    @@ -31,7 +31,7 @@
             "count": 1,
             "count_edit": 0,
             "count_new": 1,
    -        "name": "Weblate Test",
    +        "name": "Weblate <b>Test</b>",
             "words": 2,
             "words_edit": 0,
             "words_new": 2,
    @@ -62,7 +62,9 @@ class BaseReportsTest(ViewTestCase):
         def setUp(self):
             super().setUp()
             self.user.is_superuser = True
    +        self.user.full_name = "Weblate <b>Test</b>"
             self.user.save()
    +        self.maxDiff = None
     
         def add_change(self):
             self.edit_unit("Hello, world!\n", "Nazdar svete!\n")
    @@ -87,7 +89,14 @@ def test_credits_one(self, expected_count=1):
                 translation__component=self.component,
             )
             self.assertEqual(
    -            data, [{"Czech": [("weblate@example.org", "Weblate Test", expected_count)]}]
    +            data,
    +            [
    +                {
    +                    "Czech": [
    +                        ("weblate@example.org", "Weblate <b>Test</b>", expected_count)
    +                    ]
    +                }
    +            ],
             )
     
         def test_credits_more(self):
    @@ -126,15 +135,21 @@ def test_credits_view_json(self):
             self.assertEqual(response.status_code, 200)
             self.assertJSONEqual(
                 response.content.decode(),
    -            [{"Czech": [["weblate@example.org", "Weblate Test", 1]]}],
    +            [{"Czech": [["weblate@example.org", "Weblate <b>Test</b>", 1]]}],
             )
     
         def test_credits_view_rst(self):
             response = self.get_credits("rst")
             self.assertEqual(response.status_code, 200)
             self.assertEqual(
                 response.content.decode(),
    -            "\n\n* Czech\n\n    * Weblate Test <weblate@example.org> (1)\n\n",
    +            """
    +
    +* Czech
    +
    +    * Weblate &lt;b&gt;Test&lt;/b&gt; <weblate@example.org> (1)
    +
    +""",
             )
     
         def test_credits_view_html(self):
    @@ -145,7 +160,7 @@ def test_credits_view_html(self):
                 "<table>\n"
                 "<tr>\n<th>Czech</th>\n"
                 '<td><ul><li><a href="mailto:weblate@example.org">'
    -            "Weblate Test</a> (1)</li></ul></td>\n</tr>\n"
    +            "Weblate &lt;b&gt;Test&lt;/b&gt;</a> (1)</li></ul></td>\n</tr>\n"
                 "</table>",
             )
     
    @@ -231,7 +246,7 @@ def test_counts_view_html(self):
             <th>Target chars edited</th>
         </tr>
         <tr>
    -        <td>Weblate Test</td>
    +        <td>Weblate &lt;b&gt;Test&lt;/b&gt;</td>
             <td>weblate@example.org</td>
             <td>1</td>
             <td>14</td>
    
  • weblate/trans/views/reports.py+8 5 modified
    @@ -17,9 +17,9 @@
     # along with this program.  If not, see <https://www.gnu.org/licenses/>.
     #
     
    -
     from django.contrib.auth.decorators import login_required
     from django.http import HttpResponse, JsonResponse
    +from django.utils.html import escape
     from django.views.decorators.http import require_POST
     
     from weblate.lang.models import Language
    @@ -109,10 +109,13 @@ def get_credits(request, project=None, component=None):
         for language in data:
             name, translators = language.popitem()
             result.append(row_start)
    -        result.append(language_format.format(name))
    +        result.append(language_format.format(escape(name)))
             result.append(
                 translator_start
    -            + "\n".join(translator_format.format(*t) for t in translators)
    +            + "\n".join(
    +                translator_format.format(escape(t[0]), escape(t[1]), t[2])
    +                for t in translators
    +            )
                 + translator_end
             )
             result.append(row_end)
    @@ -288,8 +291,8 @@ def get_counts(request, project=None, component=None):
             result.append(
                 "".join(
                     (
    -                    cell_name.format(item["name"] or "Anonymous"),
    -                    cell_name.format(item["email"] or ""),
    +                    cell_name.format(escape(item["name"]) or "Anonymous"),
    +                    cell_name.format(escape(item["email"]) or ""),
                         cell_count.format(item["count"]),
                         cell_count.format(item["edits"]),
                         cell_count.format(item["words"]),
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.