From 78eb8de067475e864d36eeb1ffe314dfba67db29 Mon Sep 17 00:00:00 2001
From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com>
Date: Tue, 12 May 2026 14:32:01 -0400
Subject: [PATCH] Escape HTML in both web UI dashboards to prevent XSS
Both Function URLs are public (auth_type=NONE) and render
email-derived content via f-strings. Attacker-crafted emails
could inject scripts. Added html.escape() on all interpolated
values in both PO and WO dashboards.
---
lambdas/po/web_ui/handler.py | 68 +++++++++++++++++++-----------------
lambdas/wo/web_ui/handler.py | 45 ++++++++++++------------
2 files changed, 58 insertions(+), 55 deletions(-)
diff --git a/lambdas/po/web_ui/handler.py b/lambdas/po/web_ui/handler.py
index 90ef3c6..f9cc6a4 100644
--- a/lambdas/po/web_ui/handler.py
+++ b/lambdas/po/web_ui/handler.py
@@ -7,6 +7,7 @@ Accessed via Lambda Function URL.
import os
from decimal import Decimal
+from html import escape as esc
import boto3
@@ -31,7 +32,7 @@ def render_badge(value, color_map):
if not value:
value = "unknown"
color = color_map.get(value.lower(), "#9ca3af")
- label = value.replace("_", " ").title()
+ label = esc(value.replace("_", " ").title())
return f'{label}'
@@ -62,50 +63,51 @@ def fmt_currency(val):
def render_po_detail(po):
- po_number = po.get("po_number", "")
+ po_number = esc(po.get("po_number", ""))
fields = [
("PO Number", po_number),
("Status", render_badge(po.get("po_status", ""), STATUS_COLORS)),
("Email Type", render_badge(po.get("email_type", ""), EMAIL_TYPE_COLORS)),
("Total Amount", fmt_currency(po.get("total_amount"))),
- ("Currency", po.get("currency")),
- ("Supplier", (po.get("supplier") or {}).get("name")),
- ("Site Code", po.get("site_code")),
- ("State", po.get("state")),
- ("Trade", po.get("trade")),
- ("Fiscal Year", po.get("fiscal_year")),
- ("Coupa Category", po.get("coupa_category")),
- ("Submitted By", po.get("submitted_by")),
- ("On Behalf Of", po.get("on_behalf_of")),
- ("Order Date", po.get("order_date")),
- ("Revision Date", po.get("revision_date")),
- ("Payment Terms", po.get("payment_terms")),
- ("Requisition #", po.get("requisition_number")),
- ("Department", po.get("department")),
- ("Data Source", po.get("data_source")),
- ("Processed At", po.get("processed_at")),
+ ("Currency", esc(po.get("currency", "")) or None),
+ ("Supplier", esc((po.get("supplier") or {}).get("name", "")) or None),
+ ("Site Code", esc(po.get("site_code", "")) or None),
+ ("State", esc(po.get("state", "")) or None),
+ ("Trade", esc(po.get("trade", "")) or None),
+ ("Fiscal Year", esc(po.get("fiscal_year", "")) or None),
+ ("Coupa Category", esc(po.get("coupa_category", "")) or None),
+ ("Submitted By", esc(po.get("submitted_by", "")) or None),
+ ("On Behalf Of", esc(po.get("on_behalf_of", "")) or None),
+ ("Order Date", esc(po.get("order_date", "")) or None),
+ ("Revision Date", esc(po.get("revision_date", "")) or None),
+ ("Payment Terms", esc(po.get("payment_terms", "")) or None),
+ ("Requisition #", esc(po.get("requisition_number", "")) or None),
+ ("Department", esc(po.get("department", "")) or None),
+ ("Data Source", esc(po.get("data_source", "")) or None),
+ ("Processed At", esc(po.get("processed_at", "")) or None),
]
ship_to = po.get("ship_to") or {}
if any(ship_to.values()):
ship_parts = []
if ship_to.get("name"):
- ship_parts.append(ship_to["name"])
+ ship_parts.append(esc(ship_to["name"]))
if ship_to.get("address"):
- ship_parts.append(ship_to["address"])
+ ship_parts.append(esc(ship_to["address"]))
if ship_to.get("location_code"):
- ship_parts.append(f"Location: {ship_to['location_code']}")
+ ship_parts.append(f"Location: {esc(ship_to['location_code'])}")
if ship_to.get("attn"):
- ship_parts.append(f"Attn: {ship_to['attn']}")
+ ship_parts.append(f"Attn: {esc(ship_to['attn'])}")
fields.append(("Ship To", "
".join(ship_parts)))
view_url = po.get("view_order_url")
if view_url:
+ escaped_url = esc(view_url, quote=True)
fields.append(
(
"Coupa Link",
- f'View in Coupa',
+ f'View in Coupa',
)
)
@@ -124,17 +126,17 @@ def render_po_detail(po):
if line_items:
rows = ""
for item in line_items:
- qty = item.get("quantity", "") or ""
- unit = item.get("unit", "") or ""
- price = item.get("price", "") or ""
+ qty = esc(str(item.get("quantity", "") or ""))
+ unit = esc(str(item.get("unit", "") or ""))
+ price = esc(str(item.get("price", "") or ""))
rows += f"""
No events yet.
' - wo_id = wo.get("work_order_id", "") + wo_id = esc(wo.get("work_order_id", "")) fields = [ - ("Description", wo.get("description")), + ("Description", esc(wo.get("description", "")) or None), ("Status", render_badge(wo.get("wo_status", "unknown"), STATUS_COLORS)), ( "Record Type", render_badge(wo.get("record_type", "unknown"), RECORD_TYPE_COLORS), ), - ("Site Code", wo.get("site_code")), - ("Building", wo.get("building")), - ("Address", wo.get("address")), - ("Severity", wo.get("severity")), - ("Priority", wo.get("priority")), - ("Due Date", wo.get("due_date")), - ("Date Reported", wo.get("date_reported")), - ("Scheduled Start", wo.get("scheduled_start")), - ("Assigned To", wo.get("assigned_to")), - ("Created", wo.get("created_at")), - ("Last Updated", wo.get("updated_at")), + ("Site Code", esc(wo.get("site_code", "")) or None), + ("Building", esc(wo.get("building", "")) or None), + ("Address", esc(wo.get("address", "")) or None), + ("Severity", esc(wo.get("severity", "")) or None), + ("Priority", esc(wo.get("priority", "")) or None), + ("Due Date", esc(wo.get("due_date", "")) or None), + ("Date Reported", esc(wo.get("date_reported", "")) or None), + ("Scheduled Start", esc(wo.get("scheduled_start", "")) or None), + ("Assigned To", esc(wo.get("assigned_to", "")) or None), + ("Created", esc(wo.get("created_at", "")) or None), + ("Last Updated", esc(wo.get("updated_at", "")) or None), ] details_html = "" @@ -153,13 +154,13 @@ def render_work_order_detail(wo, events): def render_work_orders_list(work_orders): rows = "" for wo in work_orders: - wo_id = wo.get("work_order_id", "") - desc = wo.get("description", "") - site = wo.get("site_code", "") + wo_id = esc(wo.get("work_order_id", "")) + desc = esc(wo.get("description", "")) + site = esc(wo.get("site_code", "")) status = wo.get("wo_status", "unknown") record_type = wo.get("record_type", "unknown") - due = wo.get("due_date", "") - updated = wo.get("updated_at", "")[:16] + due = esc(wo.get("due_date", "")) + updated = esc(wo.get("updated_at", "")[:16]) rows += f"""