fix(pos): escape html output in pos page templates (backport #55527) (#55528)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Diptanil Saha
2026-06-02 00:42:59 +05:30
committed by GitHub
parent 96bd97dd6d
commit 689a3f50ae
7 changed files with 76 additions and 54 deletions

View File

@@ -174,8 +174,8 @@ erpnext.PointOfSale.Controller = class {
set_opening_entry_status() { set_opening_entry_status() {
this.page.set_title_sub( this.page.set_title_sub(
`<span class="indicator orange"> `<span class="indicator orange">
<a class="text-muted" href="#Form/POS%20Opening%20Entry/${this.pos_opening}"> <a class="text-muted" href="#Form/POS%20Opening%20Entry/${encodeURIComponent(this.pos_opening)}">
Opened at ${frappe.datetime.str_to_user(this.pos_opening_time)} Opened at ${frappe.utils.escape_html(frappe.datetime.str_to_user(this.pos_opening_time))}
</a> </a>
</span>` </span>`
); );

View File

@@ -178,7 +178,7 @@ erpnext.PointOfSale.ItemCart = class {
me.$totals_section.find(".edit-cart-btn").click(); me.$totals_section.find(".edit-cart-btn").click();
} }
const item_row_name = unescape($cart_item.attr("data-row-name")); const item_row_name = $cart_item.attr("data-row-name");
me.events.cart_item_clicked({ name: item_row_name }); me.events.cart_item_clicked({ name: item_row_name });
this.numpad_value = ""; this.numpad_value = "";
}); });
@@ -453,10 +453,10 @@ erpnext.PointOfSale.ItemCart = class {
<div class="customer-display"> <div class="customer-display">
${this.get_customer_image()} ${this.get_customer_image()}
<div class="customer-name-desc"> <div class="customer-name-desc">
<div class="customer-name">${customer_name}</div> <div class="customer-name">${frappe.utils.escape_html(customer_name)}</div>
${get_customer_description()} ${get_customer_description()}
</div> </div>
<div class="reset-customer-btn" data-customer="${escape(customer)}"> <div class="reset-customer-btn" data-customer="${frappe.utils.escape_html(customer)}">
<svg width="32" height="32" viewBox="0 0 14 14" fill="none"> <svg width="32" height="32" viewBox="0 0 14 14" fill="none">
<path d="M4.93764 4.93759L7.00003 6.99998M9.06243 9.06238L7.00003 6.99998M7.00003 6.99998L4.93764 9.06238L9.06243 4.93759" stroke="#8D99A6"/> <path d="M4.93764 4.93759L7.00003 6.99998M9.06243 9.06238L7.00003 6.99998M7.00003 6.99998L4.93764 9.06238L9.06243 4.93759" stroke="#8D99A6"/>
</svg> </svg>
@@ -473,11 +473,13 @@ erpnext.PointOfSale.ItemCart = class {
if (!email_id && !mobile_no) { if (!email_id && !mobile_no) {
return `<div class="customer-desc">${__("Click to add email / phone")}</div>`; return `<div class="customer-desc">${__("Click to add email / phone")}</div>`;
} else if (email_id && !mobile_no) { } else if (email_id && !mobile_no) {
return `<div class="customer-desc">${email_id}</div>`; return `<div class="customer-desc">${frappe.utils.escape_html(email_id)}</div>`;
} else if (mobile_no && !email_id) { } else if (mobile_no && !email_id) {
return `<div class="customer-desc">${mobile_no}</div>`; return `<div class="customer-desc">${frappe.utils.escape_html(mobile_no)}</div>`;
} else { } else {
return `<div class="customer-desc">${email_id} - ${mobile_no}</div>`; return `<div class="customer-desc">${frappe.utils.escape_html(
email_id
)} - ${frappe.utils.escape_html(mobile_no)}</div>`;
} }
} }
} }
@@ -485,9 +487,13 @@ erpnext.PointOfSale.ItemCart = class {
get_customer_image() { get_customer_image() {
const { customer, image } = this.customer_info || {}; const { customer, image } = this.customer_info || {};
if (image) { if (image) {
return `<div class="customer-image"><img src="${image}" alt="${image}""></div>`; return `<div class="customer-image"><img src="${frappe.utils.escape_html(
image
)}" alt="${frappe.utils.escape_html(image)}"></div>`;
} else { } else {
return `<div class="customer-image customer-abbr">${frappe.get_abbr(customer)}</div>`; return `<div class="customer-image customer-abbr">${frappe.utils.escape_html(
frappe.get_abbr(customer)
)}</div>`;
} }
} }
@@ -549,10 +555,10 @@ erpnext.PointOfSale.ItemCart = class {
if (t.tax_amount_after_discount_amount == 0.0) return; if (t.tax_amount_after_discount_amount == 0.0) return;
// if tax rate is 0, don't print it. // if tax rate is 0, don't print it.
const description = /[0-9]+/.test(t.description) const description = /[0-9]+/.test(t.description)
? t.description ? frappe.utils.escape_html(t.description)
: t.rate != 0 : t.rate != 0
? `${t.description} @ ${t.rate}%` ? `${frappe.utils.escape_html(t.description)} @ ${t.rate}%`
: t.description; : frappe.utils.escape_html(t.description);
return `<div class="tax-row"> return `<div class="tax-row">
<div class="tax-label">${description}</div> <div class="tax-label">${description}</div>
<div class="tax-value">${format_currency(t.tax_amount_after_discount_amount, currency)}</div> <div class="tax-value">${format_currency(t.tax_amount_after_discount_amount, currency)}</div>
@@ -566,8 +572,9 @@ erpnext.PointOfSale.ItemCart = class {
} }
get_cart_item({ name }) { get_cart_item({ name }) {
const item_selector = `.cart-item-wrapper[data-row-name="${escape(name)}"]`; return this.$cart_items_wrapper.find(".cart-item-wrapper").filter(function () {
return this.$cart_items_wrapper.find(item_selector); return $(this).attr("data-row-name") === name;
});
} }
get_item_from_frm(item) { get_item_from_frm(item) {
@@ -597,7 +604,9 @@ erpnext.PointOfSale.ItemCart = class {
if (!$item_to_update.length) { if (!$item_to_update.length) {
this.$cart_items_wrapper.append( this.$cart_items_wrapper.append(
`<div class="cart-item-wrapper" data-row-name="${escape(item_data.name)}"></div> `<div class="cart-item-wrapper" data-row-name="${frappe.utils.escape_html(
item_data.name
)}"></div>
<div class="seperator"></div>` <div class="seperator"></div>`
); );
$item_to_update = this.get_cart_item(item_data); $item_to_update = this.get_cart_item(item_data);
@@ -607,7 +616,7 @@ erpnext.PointOfSale.ItemCart = class {
`${get_item_image_html()} `${get_item_image_html()}
<div class="item-name-desc"> <div class="item-name-desc">
<div class="item-name"> <div class="item-name">
${item_data.item_name} ${frappe.utils.escape_html(item_data.item_name)}
</div> </div>
${get_description_html()} ${get_description_html()}
</div> </div>
@@ -636,7 +645,7 @@ erpnext.PointOfSale.ItemCart = class {
if (item_data.rate && item_data.amount && item_data.rate !== item_data.amount) { if (item_data.rate && item_data.amount && item_data.rate !== item_data.amount) {
return ` return `
<div class="item-qty-rate"> <div class="item-qty-rate">
<div class="item-qty"><span>${item_data.qty || 0} ${item_data.uom}</span></div> <div class="item-qty"><span>${item_data.qty || 0} ${frappe.utils.escape_html(item_data.uom)}</span></div>
<div class="item-rate-amount"> <div class="item-rate-amount">
<div class="item-rate">${format_currency(item_data.amount, currency)}</div> <div class="item-rate">${format_currency(item_data.amount, currency)}</div>
<div class="item-amount">${format_currency(item_data.rate, currency)}</div> <div class="item-amount">${format_currency(item_data.rate, currency)}</div>
@@ -645,7 +654,7 @@ erpnext.PointOfSale.ItemCart = class {
} else { } else {
return ` return `
<div class="item-qty-rate"> <div class="item-qty-rate">
<div class="item-qty"><span>${item_data.qty || 0} ${item_data.uom}</span></div> <div class="item-qty"><span>${item_data.qty || 0} ${frappe.utils.escape_html(item_data.uom)}</span></div>
<div class="item-rate-amount"> <div class="item-rate-amount">
<div class="item-rate">${format_currency(item_data.rate, currency)}</div> <div class="item-rate">${format_currency(item_data.rate, currency)}</div>
</div> </div>
@@ -666,7 +675,7 @@ erpnext.PointOfSale.ItemCart = class {
} }
} }
item_data.description = frappe.ellipsis(item_data.description, 45); item_data.description = frappe.ellipsis(item_data.description, 45);
return `<div class="item-desc">${item_data.description}</div>`; return `<div class="item-desc">${frappe.utils.escape_html(item_data.description)}</div>`;
} }
return ``; return ``;
} }
@@ -678,22 +687,26 @@ erpnext.PointOfSale.ItemCart = class {
<div class="item-image"> <div class="item-image">
<img <img
onerror="cur_pos.cart.handle_broken_image(this)" onerror="cur_pos.cart.handle_broken_image(this)"
src="${image}" alt="${frappe.get_abbr(item_name)}""> src="${frappe.utils.escape_html(image)}" alt="${frappe.utils.escape_html(frappe.get_abbr(item_name))}">
</div>`; </div>`;
} else { } else {
return `<div class="item-image item-abbr">${frappe.get_abbr(item_name)}</div>`; return `<div class="item-image item-abbr">${frappe.utils.escape_html(
frappe.get_abbr(item_name)
)}</div>`;
} }
} }
} }
handle_broken_image($img) { handle_broken_image($img) {
const item_abbr = $($img).attr("alt"); const item_abbr = $($img).attr("alt");
$($img).parent().replaceWith(`<div class="item-image item-abbr">${item_abbr}</div>`); $($img)
.parent()
.replaceWith(`<div class="item-image item-abbr">${frappe.utils.escape_html(item_abbr)}</div>`);
} }
update_selector_value_in_cart_item(selector, value, item) { update_selector_value_in_cart_item(selector, value, item) {
const $item_to_update = this.get_cart_item(item); const $item_to_update = this.get_cart_item(item);
$item_to_update.attr(`data-${selector}`, escape(value)); $item_to_update.attr(`data-${selector}`, value);
} }
toggle_checkout_btn(show_checkout) { toggle_checkout_btn(show_checkout) {
@@ -892,8 +905,8 @@ erpnext.PointOfSale.ItemCart = class {
<div class="customer-display"> <div class="customer-display">
${this.get_customer_image()} ${this.get_customer_image()}
<div class="customer-name-desc"> <div class="customer-name-desc">
<div class="customer-name">${customer_name}</div> <div class="customer-name">${frappe.utils.escape_html(customer_name)}</div>
<div class="customer-desc">${customer}</div> <div class="customer-desc">${frappe.utils.escape_html(customer)}</div>
</div> </div>
</div> </div>
<div class="customer-fields-container"> <div class="customer-fields-container">
@@ -1030,9 +1043,11 @@ erpnext.PointOfSale.ItemCart = class {
}; };
transaction_container.append( transaction_container.append(
`<div class="invoice-wrapper" data-invoice-name="${escape(invoice.name)}"> `<div class="invoice-wrapper" data-invoice-name="${frappe.utils.escape_html(
invoice.name
)}">
<div class="invoice-name-date"> <div class="invoice-name-date">
<div class="invoice-name">${invoice.name}</div> <div class="invoice-name">${frappe.utils.escape_html(invoice.name)}</div>
<div class="invoice-date">${posting_datetime}</div> <div class="invoice-date">${posting_datetime}</div>
</div> </div>
<div class="invoice-total-status"> <div class="invoice-total-status">
@@ -1040,7 +1055,7 @@ erpnext.PointOfSale.ItemCart = class {
${format_currency(invoice.grand_total, invoice.currency, frappe.sys_defaults.currency_precision) || 0} ${format_currency(invoice.grand_total, invoice.currency, frappe.sys_defaults.currency_precision) || 0}
</div> </div>
<div class="invoice-status"> <div class="invoice-status">
<span class="indicator-pill whitespace-nowrap ${indicator_color[invoice.status]}"> <span class="indicator-pill whitespace-nowrap ${indicator_color[invoice.status] || ""}">
<span>${__(invoice.status)}</span> <span>${__(invoice.status)}</span>
</span> </span>
</div> </div>

View File

@@ -128,25 +128,27 @@ erpnext.PointOfSale.ItemDetails = class {
return ``; return ``;
} }
this.$item_name.html(item_name); this.$item_name.html(frappe.utils.escape_html(item_name));
this.$item_description.html(get_description_html()); this.$item_description.html(get_description_html());
this.$item_price.html(format_currency(price_list_rate, this.currency)); this.$item_price.html(format_currency(price_list_rate, this.currency));
if (!this.hide_images && image) { if (!this.hide_images && image) {
this.$item_image.html( this.$item_image.html(
`<img `<img
onerror="cur_pos.item_details.handle_broken_image(this)" onerror="cur_pos.item_details.handle_broken_image(this)"
class="h-full" src="${image}" class="h-full" src="${frappe.utils.escape_html(image)}"
alt="${frappe.get_abbr(item_name)}" alt="${frappe.utils.escape_html(frappe.get_abbr(item_name))}"
style="object-fit: cover;">` style="object-fit: cover;">`
); );
} else { } else {
this.$item_image.html(`<div class="item-abbr">${frappe.get_abbr(item_name)}</div>`); this.$item_image.html(
`<div class="item-abbr">${frappe.utils.escape_html(frappe.get_abbr(item_name))}</div>`
);
} }
} }
handle_broken_image($img) { handle_broken_image($img) {
const item_abbr = $($img).attr("alt"); const item_abbr = $($img).attr("alt");
$($img).replaceWith(`<div class="item-abbr">${item_abbr}</div>`); $($img).replaceWith(`<div class="item-abbr">${frappe.utils.escape_html(item_abbr)}</div>`);
} }
render_discount_dom(item) { render_discount_dom(item) {

View File

@@ -134,9 +134,9 @@ erpnext.PointOfSale.ItemSelector = class {
<div class="item-name"> <div class="item-name">
${frappe.utils.escape_html(frappe.ellipsis(item.item_name, 18))} ${frappe.utils.escape_html(frappe.ellipsis(item.item_name, 18))}
</div> </div>
<div class="item-rate">${frappe.utils.escape_html( <div class="item-rate">${
format_currency(price_list_rate, item.currency, precision) || 0 format_currency(price_list_rate, item.currency, precision) || 0
)} / ${frappe.utils.escape_html(uom)}</div> } / ${frappe.utils.escape_html(uom)}</div>
</div> </div>
</div>`; </div>`;
} }
@@ -265,7 +265,6 @@ erpnext.PointOfSale.ItemSelector = class {
let rate = $item.attr("data-rate"); let rate = $item.attr("data-rate");
let stock_uom = $item.attr("data-stock-uom"); let stock_uom = $item.attr("data-stock-uom");
// escape(undefined) returns "undefined" then unescape returns "undefined"
batch_no = batch_no === "undefined" ? undefined : batch_no; batch_no = batch_no === "undefined" ? undefined : batch_no;
serial_no = serial_no === "undefined" ? undefined : serial_no; serial_no = serial_no === "undefined" ? undefined : serial_no;
uom = uom === "undefined" ? undefined : uom; uom = uom === "undefined" ? undefined : uom;

View File

@@ -38,7 +38,7 @@ erpnext.PointOfSale.PastOrderList = class {
}); });
const me = this; const me = this;
this.$invoices_container.on("click", ".invoice-wrapper", function () { this.$invoices_container.on("click", ".invoice-wrapper", function () {
const invoice_name = unescape($(this).attr("data-invoice-name")); const invoice_name = $(this).attr("data-invoice-name");
me.events.open_invoice_data(invoice_name); me.events.open_invoice_data(invoice_name);
}); });
@@ -99,14 +99,14 @@ erpnext.PointOfSale.PastOrderList = class {
const posting_datetime = frappe.datetime.str_to_user( const posting_datetime = frappe.datetime.str_to_user(
invoice.posting_date + " " + invoice.posting_time invoice.posting_date + " " + invoice.posting_time
); );
return `<div class="invoice-wrapper" data-invoice-name="${escape(invoice.name)}"> return `<div class="invoice-wrapper" data-invoice-name="${frappe.utils.escape_html(invoice.name)}">
<div class="invoice-name-date"> <div class="invoice-name-date">
<div class="invoice-name">${invoice.name}</div> <div class="invoice-name">${frappe.utils.escape_html(invoice.name)}</div>
<div class="invoice-date"> <div class="invoice-date">
<svg class="mr-2" width="12" height="12" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"> <svg class="mr-2" width="12" height="12" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/> <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
</svg> </svg>
${frappe.ellipsis(invoice.customer_name, 20)} ${frappe.utils.escape_html(frappe.ellipsis(invoice.customer_name, 20))}
</div> </div>
</div> </div>
<div class="invoice-total-status"> <div class="invoice-total-status">

View File

@@ -81,23 +81,27 @@ erpnext.PointOfSale.PastOrderSummary = class {
return `<div class="left-section"> return `<div class="left-section">
<div class="customer-section"> <div class="customer-section">
<div class="customer-name">${doc.customer_name}</div> <div class="customer-name">${frappe.utils.escape_html(doc.customer_name)}</div>
${is_customer_naming_by_customer_name ? `<div class="customer-code">${doc.customer}</div>` : ""} ${
<div class="customer-email">${this.customer_email}</div> is_customer_naming_by_customer_name
? `<div class="customer-code">${frappe.utils.escape_html(doc.customer)}</div>`
: ""
}
<div class="customer-email">${frappe.utils.escape_html(this.customer_email)}</div>
</div> </div>
<div class="cashier">${__("Sold by")}: ${doc.owner}</div> <div class="cashier">${__("Sold by")}: ${frappe.utils.escape_html(doc.owner)}</div>
</div> </div>
<div class="right-section"> <div class="right-section">
<div class="paid-amount">${format_currency(doc.paid_amount, doc.currency)}</div> <div class="paid-amount">${format_currency(doc.paid_amount, doc.currency)}</div>
<div class="invoice-name">${doc.name}</div> <div class="invoice-name">${frappe.utils.escape_html(doc.name)}</div>
<span class="indicator-pill whitespace-nowrap ${indicator_color}"><span>${__(doc.status)}</span></span> <span class="indicator-pill whitespace-nowrap ${indicator_color}"><span>${__(doc.status)}</span></span>
</div>`; </div>`;
} }
get_item_html(doc, item_data) { get_item_html(doc, item_data) {
return `<div class="item-row-wrapper"> return `<div class="item-row-wrapper">
<div class="item-name">${item_data.item_name}</div> <div class="item-name">${frappe.utils.escape_html(item_data.item_name)}</div>
<div class="item-qty">${item_data.qty || 0} ${item_data.uom}</div> <div class="item-qty">${item_data.qty || 0} ${frappe.utils.escape_html(item_data.uom)}</div>
<div class="item-rate-disc">${get_rate_discount_html()}</div> <div class="item-rate-disc">${get_rate_discount_html()}</div>
</div>`; </div>`;
@@ -139,10 +143,10 @@ erpnext.PointOfSale.PastOrderSummary = class {
.map((t) => { .map((t) => {
// if tax rate is 0, don't print it. // if tax rate is 0, don't print it.
const description = /[0-9]+/.test(t.description) const description = /[0-9]+/.test(t.description)
? t.description ? frappe.utils.escape_html(t.description)
: t.rate != 0 : t.rate != 0
? `${t.description} @ ${t.rate}%` ? `${frappe.utils.escape_html(t.description)} @ ${t.rate}%`
: t.description; : frappe.utils.escape_html(t.description);
return ` return `
<div class="tax-row"> <div class="tax-row">
<div class="tax-label">${description}</div> <div class="tax-label">${description}</div>

View File

@@ -408,8 +408,10 @@ erpnext.PointOfSale.Payment = class {
return ` return `
<div class="payment-mode-wrapper"> <div class="payment-mode-wrapper">
<div class="mode-of-payment" data-mode="${mode}" data-payment-type="${payment_type}"> <div class="mode-of-payment" data-mode="${mode}" data-payment-type="${frappe.utils.escape_html(
${p.mode_of_payment} payment_type
)}">
${frappe.utils.escape_html(p.mode_of_payment)}
<div class="${mode}-amount pay-amount">${amount}</div> <div class="${mode}-amount pay-amount">${amount}</div>
<div class="${mode} mode-of-payment-control"></div> <div class="${mode} mode-of-payment-control"></div>
</div> </div>
@@ -544,7 +546,7 @@ erpnext.PointOfSale.Payment = class {
<div class="mode-of-payment loyalty-card" data-mode="loyalty-amount" data-payment-type="loyalty-amount"> <div class="mode-of-payment loyalty-card" data-mode="loyalty-amount" data-payment-type="loyalty-amount">
Redeem Loyalty Points Redeem Loyalty Points
<div class="loyalty-amount-amount pay-amount">${amount}</div> <div class="loyalty-amount-amount pay-amount">${amount}</div>
<div class="loyalty-amount-name">${loyalty_program}</div> <div class="loyalty-amount-name">${frappe.utils.escape_html(loyalty_program)}</div>
<div class="loyalty-amount mode-of-payment-control"></div> <div class="loyalty-amount mode-of-payment-control"></div>
</div> </div>
</div>` </div>`