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""" - {item.get("description", "")} + {esc(str(item.get("description", "")))} {qty} {unit} {price} {fmt_currency(item.get("amount"))} - {item.get("need_by", "") or ""} + {esc(str(item.get("need_by", "") or ""))} """ items_html = f""" @@ -184,13 +186,13 @@ def render_po_detail(po): def render_po_list(purchase_orders): rows = "" for po in purchase_orders: - po_number = po.get("po_number", "") - supplier = (po.get("supplier") or {}).get("name", "") - site_code = po.get("site_code", "") - trade = po.get("trade", "") + po_number = esc(po.get("po_number", "")) + supplier = esc((po.get("supplier") or {}).get("name", "")) + site_code = esc(po.get("site_code", "")) + trade = esc(po.get("trade", "")) status = po.get("po_status", "") total = fmt_currency(po.get("total_amount")) - processed = (po.get("processed_at") or "")[:16] + processed = esc((po.get("processed_at") or "")[:16]) rows += f""" diff --git a/lambdas/wo/web_ui/handler.py b/lambdas/wo/web_ui/handler.py index 3b32d8d..59b6f9d 100644 --- a/lambdas/wo/web_ui/handler.py +++ b/lambdas/wo/web_ui/handler.py @@ -6,6 +6,7 @@ Accessed via Lambda Function URL. """ import os +from html import escape as esc import boto3 @@ -37,7 +38,7 @@ def get_comments(work_order_id): def render_badge(value, color_map): color = color_map.get(value, "#9ca3af") - label = value.replace("_", " ").title() + label = esc(value.replace("_", " ").title()) return f'{label}' @@ -65,9 +66,9 @@ def render_work_order_detail(wo, events): if events: for e in events: record_type = e.get("record_type", "unknown") - commenter = e.get("commenter", "") - created = e.get("created_at", "") - text = e.get("text", "") + commenter = esc(e.get("commenter", "")) + created = esc(e.get("created_at", "")) + text = esc(e.get("text", "")) badge = render_badge(record_type, RECORD_TYPE_COLORS) commenter_str = ( f"{commenter} — " if commenter else "" @@ -91,25 +92,25 @@ def render_work_order_detail(wo, events): else: events_html = '

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"""