VYPR
High severityNVD Advisory· Published Jan 23, 2024· Updated May 30, 2025

Label Studio XSS Vulnerability on Avatar Upload

CVE-2023-47115

Description

Label Studio is an a popular open source data labeling tool. Versions prior to 1.9.2 have a cross-site scripting (XSS) vulnerability that could be exploited when an authenticated user uploads a crafted image file for their avatar that gets rendered as a HTML file on the website. Executing arbitrary JavaScript could result in an attacker performing malicious actions on Label Studio users if they visit the crafted avatar image. For an example, an attacker can craft a JavaScript payload that adds a new Django Super Administrator user if a Django administrator visits the image.

The file users/functions.py lines 18-49 show that the only verification check is that the file is an image by extracting the dimensions from the file. Label Studio serves avatar images using Django's built-in serve view, which is not secure for production use according to Django's documentation. The issue with the Django serve view is that it determines the Content-Type of the response by the file extension in the URL path. Therefore, an attacker can upload an image that contains malicious HTML code and name the file with a .html extension to be rendered as a HTML page. The only file extension validation is performed on the client-side, which can be easily bypassed.

Version 1.9.2 fixes this issue. Other remediation strategies include validating the file extension on the server side, not in client-side code; removing the use of Django's serve view and implement a secure controller for viewing uploaded avatar images; saving file content in the database rather than on the filesystem to mitigate against other file related vulnerabilities; and avoiding trusting user controlled inputs.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
label-studioPyPI
< 1.9.21.9.2

Affected products

1

Patches

1
a7a71e594f32

fix: OPTIC-179: Properly ensure content is escaped (#4926)

11 files changed · +76 17
  • label_studio/frontend/dist/react-app/index.js+1 1 modified
  • label_studio/frontend/dist/react-app/index.js.LICENSE.txt+7 0 modified
    @@ -4,6 +4,13 @@ object-assign
     @license MIT
     */
     
    +/*!
    + * is-plain-object <https://github.com/jonschlinkert/is-plain-object>
    + *
    + * Copyright (c) 2014-2017, Jon Schlinkert.
    + * Released under the MIT License.
    + */
    +
     /*! *****************************************************************************
     Copyright (c) Microsoft Corporation.
     
    
  • label_studio/frontend/dist/react-app/index.js.map+1 1 modified
  • label_studio/frontend/package.json+2 0 modified
    @@ -48,6 +48,7 @@
         "@types/react-dom": "^17.0.1",
         "@types/react-router-dom": "^5.1.7",
         "@types/rimraf": "^3.0.0",
    +    "@types/sanitize-html": "^2.9.3",
         "@types/strman": "^2.0.0",
         "@typescript-eslint/eslint-plugin": "^4.15.2",
         "@typescript-eslint/parser": "^4.15.2",
    @@ -159,6 +160,7 @@
       "dependencies": {
         "@radix-ui/react-toast": "^1.1.4",
         "lodash.clonedeep": "^4.5.0",
    +    "sanitize-html": "^2.11.0",
         "webpack-dev-server": "^4.13.3"
       }
     }
    
  • label_studio/frontend/src/app/StaticContent/StaticContent.js+2 1 modified
    @@ -14,6 +14,7 @@ const parseContent = (id, source, children, parse) => {
         if (parse) {
           const parsed = parseHTML(templateHTML);
           const childResult = children instanceof Function ? children(parsed) : false;
    +
           result = childResult || parsed;
         } else {
           const childResult = children instanceof Function ? children(template) : false;
    @@ -65,7 +66,7 @@ const StaticContentDrawer = React.forwardRef(({
         ? <React.Fragment children={content.children}/>
         : React.createElement(tagName ?? 'div', {
           ...props,
    -      ref: rootRef
    +      ref: rootRef,
         });
     });
     
    
  • label_studio/frontend/src/components/Error/Error.js+8 5 modified
    @@ -1,4 +1,5 @@
     import React, { Fragment, useCallback, useMemo, useState } from 'react';
    +import sanitizeHtml from 'sanitize-html';
     import { LsSlack } from '../../assets/icons';
     import { Block, Elem } from '../../utils/bem';
     import { absoluteURL, copyText } from '../../utils/helpers';
    @@ -37,13 +38,15 @@ export const ErrorWrapper = ({title, message, errorId, stacktrace, validation, v
             <Elem name="title">{title}</Elem>
           )}
     
    -      {message && <Elem name="detail"dangerouslySetInnerHTML={{
    -        __html: String(message),
    -      }}/>}
    +      {message && (
    +        <Elem name="detail" dangerouslySetInnerHTML={{
    +          __html: sanitizeHtml(String(message)),
    +        }}/>
    +      )}
     
           {preparedStackTrace && (
             <Elem name="stracktrace" dangerouslySetInnerHTML={{
    -          __html: preparedStackTrace.replace(/(\n)/g, '<br>'),
    +          __html: sanitizeHtml(preparedStackTrace.replace(/(\n)/g, '<br>')),
             }}/>
           )}
     
    @@ -57,7 +60,7 @@ export const ErrorWrapper = ({title, message, errorId, stacktrace, validation, v
                       tag="li"
                       key={i}
                       name="message"
    -                  dangerouslySetInnerHTML={{__html: err}}
    +                  dangerouslySetInnerHTML={{ __html: sanitizeHtml(err) }}
                     />
                   ))}
                 </Fragment>
    
  • label_studio/frontend/yarn.lock+55 2 modified
    @@ -3033,6 +3033,13 @@
         "@types/glob" "*"
         "@types/node" "*"
     
    +"@types/sanitize-html@^2.9.3":
    +  version "2.9.3"
    +  resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-2.9.3.tgz#eb31abeb496838719014b094b9e647dd7937ce7d"
    +  integrity sha512-1rsSdEJLV7utAG+Fms2uP+nSmmYmOhUUSSZvUz4wF2wlA0M5/A/gVgnpWZ7EKaPWsrrxWiSuNJqSBW8dh2isBA==
    +  dependencies:
    +    htmlparser2 "^8.0.0"
    +
     "@types/serve-index@^1.9.1":
       version "1.9.1"
       resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.1.tgz#1b5e85370a192c01ec6cec4735cf2917337a6278"
    @@ -5050,6 +5057,11 @@ escape-string-regexp@^2.0.0:
       resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344"
       integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==
     
    +escape-string-regexp@^4.0.0:
    +  version "4.0.0"
    +  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
    +  integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
    +
     eslint-plugin-prettier@^3.3.1:
       version "3.3.1"
       resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.3.1.tgz#7079cfa2497078905011e6f82e8dd8453d1371b7"
    @@ -5890,6 +5902,16 @@ htmlparser2@^6.1.0:
         domutils "^2.5.2"
         entities "^2.0.0"
     
    +htmlparser2@^8.0.0:
    +  version "8.0.2"
    +  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21"
    +  integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==
    +  dependencies:
    +    domelementtype "^2.3.0"
    +    domhandler "^5.0.3"
    +    domutils "^3.0.1"
    +    entities "^4.4.0"
    +
     http-deceiver@^1.2.7:
       version "1.2.7"
       resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87"
    @@ -6257,6 +6279,11 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4:
       dependencies:
         isobject "^3.0.1"
     
    +is-plain-object@^5.0.0:
    +  version "5.0.0"
    +  resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
    +  integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==
    +
     is-regex@^1.0.5, is-regex@^1.1.0, is-regex@^1.1.1, is-regex@^1.1.2:
       version "1.1.2"
       resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251"
    @@ -7397,7 +7424,7 @@ multicast-dns@^7.2.5:
         dns-packet "^5.2.2"
         thunky "^1.0.2"
     
    -nanoid@^3.1.23:
    +nanoid@^3.1.23, nanoid@^3.3.6:
       version "3.3.6"
       resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
       integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
    @@ -7738,6 +7765,11 @@ parse-passwd@^1.0.0:
       resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
       integrity sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==
     
    +parse-srcset@^1.0.2:
    +  version "1.0.2"
    +  resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1"
    +  integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==
    +
     parse5-htmlparser2-tree-adapter@^6.0.1:
       version "6.0.1"
       resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6"
    @@ -8109,6 +8141,15 @@ postcss@^8.1.4, postcss@^8.3.5:
         nanoid "^3.1.23"
         source-map-js "^0.6.2"
     
    +postcss@^8.3.11:
    +  version "8.4.31"
    +  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
    +  integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
    +  dependencies:
    +    nanoid "^3.3.6"
    +    picocolors "^1.0.0"
    +    source-map-js "^1.0.2"
    +
     prelude-ls@^1.2.1:
       version "1.2.1"
       resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
    @@ -8732,6 +8773,18 @@ sane@^4.0.3:
         minimist "^1.1.1"
         walker "~1.0.5"
     
    +sanitize-html@^2.11.0:
    +  version "2.11.0"
    +  resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.11.0.tgz#9a6434ee8fcaeddc740d8ae7cd5dd71d3981f8f6"
    +  integrity sha512-BG68EDHRaGKqlsNjJ2xUB7gpInPA8gVx/mvjO743hZaeMCZ2DwzW7xvsqZ+KNU4QKwj86HJ3uu2liISf2qBBUA==
    +  dependencies:
    +    deepmerge "^4.2.2"
    +    escape-string-regexp "^4.0.0"
    +    htmlparser2 "^8.0.0"
    +    is-plain-object "^5.0.0"
    +    parse-srcset "^1.0.2"
    +    postcss "^8.3.11"
    +
     sax@~1.2.4:
       version "1.2.4"
       resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
    @@ -9043,7 +9096,7 @@ source-map-js@^0.6.2:
       resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e"
       integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==
     
    -source-map-js@^1.0.1:
    +source-map-js@^1.0.1, source-map-js@^1.0.2:
       version "1.0.2"
       resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
       integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
    
  • label_studio/templates/401.html+0 2 modified
    @@ -12,9 +12,7 @@
       <h1>Authentication Error</h1>
       <img class="ls-global-error__heidi" src="{{ settings.HOSTNAME }}{% static 'images/opossum_broken.svg' %}" height="111" alt="Heidi's down" />
       <h2>Authentication credentials were not provided</h2>
    -  {% autoescape off %}
       <div class="ls-global-error__details">{{ exception }}</div>
    -  {% endautoescape %}
       <p class="ls-global-error__actions">
         <a class="ls-global-error__slack button" target="_blank" href="https://slack.labelstud.io/?source=product-401-error">
           <img src="{{ settings.HOSTNAME }}{% static 'images/slack.png' %}" width="16" />
    
  • label_studio/templates/403.html+0 2 modified
    @@ -12,9 +12,7 @@
       <h1>Server error</h1>
       <img class="ls-global-error__heidi" src="{{ settings.HOSTNAME }}{% static 'images/opossum_broken.svg' %}" height="111" alt="Heidi's down" />
       <h2>403 Forbidden</h2>
    -  {% autoescape off %}
       <div class="ls-global-error__details">{{ exception }}</div>
    -  {% endautoescape %}
       <p class="ls-global-error__actions">
         <a class="ls-global-error__slack button" target="_blank" href="https://slack.labelstud.io/?source=product-403-error">
           <img src="{{ settings.HOSTNAME }}{% static 'images/slack.png' %}" width="16" />
    
  • label_studio/templates/404.html+0 1 modified
    @@ -12,7 +12,6 @@
       <h1>Server error</h1>
       <img class="ls-global-error__heidi" src="{{ settings.HOSTNAME }}{% static 'images/opossum_broken.svg' %}" height="111" alt="Heidi's down" />
       <h2>404 Not Found</h2>
    -{#  <div class="ls-global-error__details">{{ exception }}</div>#}
       <p class="ls-global-error__actions">
         <a class="ls-global-error__slack button" target="_blank" href="https://slack.labelstud.io/?source=product-404-error">
           <img src="{{ settings.HOSTNAME }}{% static 'images/slack.png' %}" width="16" />
    
  • label_studio/templates/500.html+0 2 modified
    @@ -12,9 +12,7 @@
       <h1>Server error</h1>
       <img class="ls-global-error__heidi" src="{{ settings.HOSTNAME }}{% static 'images/opossum_broken.svg' %}" height="111" alt="Heidi's down" />
       <h2>500 Something went wrong</h2>
    -  {% autoescape off %}
       <div class="ls-global-error__details">{{ exception }}</div>
    -  {% endautoescape  %}
       <p class="ls-global-error__actions">
         <a class="ls-global-error__slack button" target="_blank" href="https://slack.labelstud.io/?source=product-500-error">
           <img src="{{ settings.HOSTNAME }}{% static 'images/slack.png' %}" width="16" />
    

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

8

News mentions

0

No linked articles in our index yet.